X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Ffanfix%2Freader%2FLocalReaderFrame.java;h=7d5e5ddcca45ab18b0fc36cf87b8cf2e7f3fcdec;hb=373da363323d3a9263aa6ebd392ca3272b23b412;hp=f196f95e1b8caa38d50f0ed2a2f13e94615f3d37;hpb=a6395bef99a8e917f67341ef1906917b87df24a4;p=fanfix.git diff --git a/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java b/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java index f196f95..7d5e5dd 100644 --- a/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java +++ b/src/be/nikiroo/fanfix/reader/LocalReaderFrame.java @@ -1,55 +1,532 @@ package be.nikiroo.fanfix.reader; -import java.awt.Desktop; -import java.awt.FlowLayout; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.WindowEvent; +import java.io.File; import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; -import javax.swing.JButton; +import javax.swing.JFileChooser; import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.Library; +import be.nikiroo.fanfix.bundles.UiConfig; import be.nikiroo.fanfix.data.MetaData; -import be.nikiroo.fanfix.supported.BasicSupport.SupportType; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.output.BasicOutput.OutputType; +import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener; +import be.nikiroo.utils.Progress; +import be.nikiroo.utils.ui.ProgressBar; +import be.nikiroo.utils.ui.WrapLayout; +/** + * A {@link Frame} that will show a {@link LocalReaderBook} item for each + * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a + * way to copy them to the {@link LocalReader} cache ({@link LocalReader#lib}), + * read them, delete them... + * + * @author niki + */ class LocalReaderFrame extends JFrame { private static final long serialVersionUID = 1L; private LocalReader reader; + private List stories; + private List books; + private JPanel bookPane; + private String type; + private Color color; + private ProgressBar pgBar; + private JMenuBar bar; + private LocalReaderBook selectedBook; - public LocalReaderFrame(LocalReader reader, SupportType type) { - super("HTML reader"); + /** + * Create a new {@link LocalReaderFrame}. + * + * @param reader + * the associated {@link LocalReader} to forward some commands + * and access its {@link Library} + * @param type + * the type of {@link Story} to load, or NULL for all types + */ + public LocalReaderFrame(LocalReader reader, String type) { + super("Fanfix Library"); this.reader = reader; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(800, 600); - setLayout(new FlowLayout()); + setLayout(new BorderLayout()); - // TODO: list all stories, list all TMP stories (and format?) + books = new ArrayList(); + bookPane = new JPanel(new WrapLayout(WrapLayout.LEADING, 5, 5)); - List stories = Instance.getLibrary().getList(type); - for (MetaData story : stories) { - JButton button = new JButton(story.getTitle()); - final String luid = story.getLuid(); - button.addActionListener(new ActionListener() { + color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR); + + if (color != null) { + setBackground(color); + bookPane.setBackground(color); + } + + JScrollPane scroll = new JScrollPane(bookPane); + scroll.getVerticalScrollBar().setUnitIncrement(16); + add(scroll, BorderLayout.CENTER); + + pgBar = new ProgressBar(); + add(pgBar, BorderLayout.SOUTH); + + refreshBooks(type); + setJMenuBar(createMenu()); + + setVisible(true); + } + + /** + * Refresh the list of {@link LocalReaderBook}s from disk. + * + * @param type + * the type of {@link Story} to load, or NULL for all types + */ + private void refreshBooks(String type) { + this.type = type; + stories = Instance.getLibrary().getList(type); + books.clear(); + bookPane.invalidate(); + bookPane.removeAll(); + for (MetaData meta : stories) { + LocalReaderBook book = new LocalReaderBook(meta, + reader.isCached(meta.getLuid())); + if (color != null) { + book.setBackground(color); + } + + books.add(book); + + book.addActionListener(new BookActionListener() { + public void select(LocalReaderBook book) { + selectedBook = book; + for (LocalReaderBook abook : books) { + abook.setSelected(abook == book); + } + } + + public void popupRequested(LocalReaderBook book, MouseEvent e) { + JPopupMenu popup = new JPopupMenu(); + popup.add(createMenuItemOpenBook()); + popup.addSeparator(); + popup.add(createMenuItemExport()); + popup.add(createMenuItemRefresh()); + popup.addSeparator(); + popup.add(createMenuItemDelete()); + popup.show(e.getComponent(), e.getX(), e.getY()); + } + + public void action(final LocalReaderBook book) { + openBook(book); + } + }); + + bookPane.add(book); + } + + bookPane.validate(); + bookPane.repaint(); + } + + /** + * Create the main menu bar. + * + * @return the bar + */ + private JMenuBar createMenu() { + bar = new JMenuBar(); + + JMenu file = new JMenu("File"); + file.setMnemonic(KeyEvent.VK_F); + + JMenuItem imprt = new JMenuItem("Import URL", KeyEvent.VK_U); + imprt.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + imprt(true); + } + }); + JMenuItem imprtF = new JMenuItem("Import File", KeyEvent.VK_F); + imprtF.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + imprt(false); + } + }); + JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X); + exit.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + LocalReaderFrame.this.dispatchEvent(new WindowEvent( + LocalReaderFrame.this, WindowEvent.WINDOW_CLOSING)); + } + }); + + file.add(createMenuItemOpenBook()); + file.add(createMenuItemExport()); + file.addSeparator(); + file.add(imprt); + file.add(imprtF); + file.addSeparator(); + file.add(exit); + + bar.add(file); + + JMenu edit = new JMenu("Edit"); + edit.setMnemonic(KeyEvent.VK_E); + + edit.add(createMenuItemRefresh()); + edit.addSeparator(); + edit.add(createMenuItemDelete()); + + bar.add(edit); + + JMenu view = new JMenu("View"); + view.setMnemonic(KeyEvent.VK_V); + + List tt = Instance.getLibrary().getTypes(); + tt.add(0, null); + for (final String type : tt) { + JMenuItem item = new JMenuItem(type == null ? "All books" : type); + item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + refreshBooks(type); + } + }); + view.add(item); + + if (type == null) { + view.addSeparator(); + } + } + + bar.add(view); + + return bar; + } + + /** + * Create the export menu item. + * + * @return the item + */ + private JMenuItem createMenuItemExport() { + final JFileChooser fc = new JFileChooser(); + fc.setAcceptAllFileFilterUsed(false); + + final Map filters = new HashMap(); + for (OutputType type : OutputType.values()) { + String ext = type.getDefaultExtension(false); + String desc = type.getDesc(false); + if (ext == null || ext.isEmpty()) { + filters.put(createAllFilter(desc), type); + } else { + filters.put(new FileNameExtensionFilter(desc, ext), type); + } + } + + // First the "ALL" filters, then, the extension filters + for (Entry entry : filters.entrySet()) { + if (!(entry.getKey() instanceof FileNameExtensionFilter)) { + fc.addChoosableFileFilter(entry.getKey()); + } + } + for (Entry entry : filters.entrySet()) { + if (entry.getKey() instanceof FileNameExtensionFilter) { + fc.addChoosableFileFilter(entry.getKey()); + } + } + // + + JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S); + export.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (selectedBook != null) { + fc.showDialog(LocalReaderFrame.this, "Save"); + final OutputType type = filters.get(fc.getFileFilter()); + final String path = fc.getSelectedFile().getAbsolutePath() + + type.getDefaultExtension(false); + final Progress pg = new Progress(); + outOfUi(pg, new Runnable() { + public void run() { + try { + Instance.getLibrary().export( + selectedBook.getLuid(), type, path, pg); + } catch (IOException e) { + Instance.syserr(e); + } + } + }); + } + } + }); + + return export; + } + + /** + * Create a {@link FileFilter} that accepts all files and return the given + * description. + * + * @param desc + * the description + * + * @return the filter + */ + private FileFilter createAllFilter(final String desc) { + return new FileFilter() { + @Override + public String getDescription() { + return desc; + } + + @Override + public boolean accept(File f) { + return true; + } + }; + } + + /** + * Create the refresh (delete cache) menu item. + * + * @return the item + */ + private JMenuItem createMenuItemRefresh() { + JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C); + refresh.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (selectedBook != null) { + outOfUi(null, new Runnable() { + public void run() { + reader.refresh(selectedBook.getLuid()); + selectedBook.setCached(false); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + selectedBook.repaint(); + } + }); + } + }); + } + } + }); + + return refresh; + } + + /** + * Create the delete menu item. + * + * @return the item + */ + private JMenuItem createMenuItemDelete() { + JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D); + delete.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (selectedBook != null) { + outOfUi(null, new Runnable() { + public void run() { + reader.delete(selectedBook.getLuid()); + selectedBook = null; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + refreshBooks(type); + } + }); + } + }); + } + } + }); + + return delete; + } + + /** + * Create the open menu item. + * + * @return the item + */ + private JMenuItem createMenuItemOpenBook() { + JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O); + open.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (selectedBook != null) { + openBook(selectedBook); + } + } + }); + + return open; + } + + /** + * Open a {@link LocalReaderBook} item. + * + * @param book + * the {@link LocalReaderBook} to open + */ + private void openBook(final LocalReaderBook book) { + final Progress pg = new Progress(); + outOfUi(pg, new Runnable() { + public void run() { + try { + reader.open(book.getLuid(), pg); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + book.setCached(true); + } + }); + } catch (IOException e) { + // TODO: error message? + Instance.syserr(e); + } + } + }); + } + + /** + * Process the given action out of the Swing UI thread and link the given + * {@link ProgressBar} to the action. + *

+ * The code will make sure that the {@link ProgressBar} (if not NULL) is set + * to done when the action is done. + * + * @param pg + * the {@link ProgressBar} or NULL + * @param run + * the action to run + */ + private void outOfUi(final Progress pg, final Runnable run) { + pgBar.setProgress(pg); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + setEnabled(false); + pgBar.addActioListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + pgBar.setProgress(null); + setEnabled(true); + } + }); + } + }); + + new Thread(new Runnable() { + public void run() { + run.run(); + if (pg == null) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + setEnabled(true); + } + }); + } else if (!pg.isDone()) { + pg.setProgress(pg.getMax()); + } + } + }).start(); + } + + /** + * Import a {@link Story} into the main {@link Library}. + * + * @param askUrl + * TRUE for an {@link URL}, false for a {@link File} + */ + private void imprt(boolean askUrl) { + JFileChooser fc = new JFileChooser(); + + final String url; + if (askUrl) { + url = JOptionPane.showInputDialog(LocalReaderFrame.this, + "url of the story to import?", "Importing from URL", + JOptionPane.QUESTION_MESSAGE); + } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) { + url = fc.getSelectedFile().getAbsolutePath(); + } else { + url = null; + } + + if (url != null && !url.isEmpty()) { + final Progress pg = new Progress("Importing " + url); + outOfUi(pg, new Runnable() { + public void run() { + Exception ex = null; try { - // TODO: config option (image, non image): TXT, - // custom-HTML, CBZ, EPUB - Desktop.getDesktop().browse( - LocalReaderFrame.this.reader.getTarget(luid) - .toURI()); - } catch (IOException e1) { - e1.printStackTrace(); + Instance.getLibrary() + .imprt(BasicReader.getUrl(url), pg); + } catch (IOException e) { + ex = e; } + + final Exception e = ex; + + final boolean ok = (e == null); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (!ok) { + JOptionPane.showMessageDialog( + LocalReaderFrame.this, e.getMessage(), + "Cannot import: " + url, + JOptionPane.ERROR_MESSAGE); + } else { + refreshBooks(type); + } + } + }); } }); + } + } - add(button); + /** + * Enables or disables this component, depending on the value of the + * parameter b. An enabled component can respond to user input + * and generate events. Components are enabled initially by default. + *

+ * Disabling this component will also affect its children. + * + * @param b + * If true, this component is enabled; otherwise + * this component is disabled + */ + @Override + public void setEnabled(boolean b) { + for (LocalReaderBook book : books) { + book.setEnabled(b); + book.repaint(); } - setVisible(true); + bar.setEnabled(b); + bookPane.setEnabled(b); + bookPane.repaint(); + + super.setEnabled(b); + repaint(); } }