gui: code cleanup
authorNiki Roo <niki@nikiroo.be>
Tue, 19 Mar 2019 17:48:56 +0000 (18:48 +0100)
committerNiki Roo <niki@nikiroo.be>
Tue, 19 Mar 2019 17:48:56 +0000 (18:48 +0100)
src/be/nikiroo/fanfix/reader/ui/GuiReaderFrame.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderMainPanel.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/ui/GuiReaderPropertiesFrame.java [new file with mode: 0644]

index d28f941e933e9d2c53e2cbd3f5db4491cad82ec6..f7fb09f205c3546402b6acad8a719125a9c66e15 100644 (file)
@@ -1,41 +1,25 @@
 package be.nikiroo.fanfix.reader.ui;
 
 import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Font;
 import java.awt.Frame;
-import java.awt.Toolkit;
-import java.awt.datatransfer.DataFlavor;
 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.net.UnknownHostException;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import javax.swing.BorderFactory;
-import javax.swing.BoxLayout;
-import javax.swing.ImageIcon;
 import javax.swing.JFileChooser;
 import javax.swing.JFrame;
-import javax.swing.JLabel;
 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.JTextArea;
-import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.filechooser.FileFilter;
 import javax.swing.filechooser.FileNameExtensionFilter;
@@ -45,16 +29,14 @@ import be.nikiroo.fanfix.bundles.Config;
 import be.nikiroo.fanfix.bundles.UiConfig;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.BasicLibrary;
-import be.nikiroo.fanfix.library.BasicLibrary.Status;
 import be.nikiroo.fanfix.library.LocalLibrary;
 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
 import be.nikiroo.fanfix.reader.BasicReader;
-import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
+import be.nikiroo.fanfix.reader.ui.GuiReaderMainPanel.FrameHelper;
+import be.nikiroo.fanfix.reader.ui.GuiReaderMainPanel.StoryRunnable;
 import be.nikiroo.utils.Progress;
 import be.nikiroo.utils.Version;
 import be.nikiroo.utils.ui.ConfigEditor;
-import be.nikiroo.utils.ui.ProgressBar;
 
 /**
  * A {@link Frame} that will show a {@link GuiReaderBook} item for each
@@ -64,31 +46,13 @@ import be.nikiroo.utils.ui.ProgressBar;
  * 
  * @author niki
  */
-class GuiReaderFrame extends JFrame {
+class GuiReaderFrame extends JFrame implements FrameHelper {
        private static final long serialVersionUID = 1L;
        private GuiReader reader;
-       private Map<GuiReaderGroup, String> booksByType;
-       private Map<GuiReaderGroup, String> booksByAuthor;
-       private JPanel pane;
-       private Color color;
-       private ProgressBar pgBar;
-       private JMenuBar bar;
-       private GuiReaderBook selectedBook;
-       private boolean words; // words or authors (secondary info on books)
+       private GuiReaderMainPanel helpee;
 
-       /**
-        * A {@link Runnable} with a {@link Story} parameter.
-        * 
-        * @author niki
-        */
-       private interface StoryRunnable {
-               /**
-                * Run the action.
-                * 
-                * @param story
-                *            the story
-                */
-               public void run(Story story);
+       private enum MoveAction {
+               SOURCE, TITLE, AUTHOR
        }
 
        /**
@@ -105,296 +69,48 @@ class GuiReaderFrame extends JFrame {
 
                this.reader = reader;
 
+               // TODO: should we still have that??
                setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-               setSize(800, 600);
-               setLayout(new BorderLayout());
-
-               pane = new JPanel();
-               pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
-
-               Integer icolor = Instance.getUiConfig().getColor(
-                               UiConfig.BACKGROUND_COLOR);
-               if (icolor != null) {
-                       color = new Color(icolor);
-                       setBackground(color);
-                       pane.setBackground(color);
-               }
-
-               JScrollPane scroll = new JScrollPane(pane);
-               scroll.getVerticalScrollBar().setUnitIncrement(16);
-               add(scroll, BorderLayout.CENTER);
-
-               String message = reader.getLibrary().getLibraryName();
-               if (!message.isEmpty()) {
-                       JLabel name = new JLabel(message, SwingConstants.CENTER);
-                       add(name, BorderLayout.NORTH);
-               }
-
-               pgBar = new ProgressBar();
-               add(pgBar, BorderLayout.SOUTH);
-
-               pgBar.addActionListener(new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               invalidate();
-                               pgBar.setProgress(null);
-                               validate();
-                               setEnabled(true);
-                       }
-               });
-
-               pgBar.addUpdateListener(new ActionListener() {
-                       @Override
-                       public void actionPerformed(ActionEvent e) {
-                               invalidate();
-                               validate();
-                               repaint();
-                       }
-               });
 
-               booksByType = new HashMap<GuiReaderGroup, String>();
-               booksByAuthor = new HashMap<GuiReaderGroup, String>();
+               helpee = new GuiReaderMainPanel(this, type);
 
-               pane.setVisible(false);
-               final Progress pg = new Progress();
-               final String typeF = type;
-               outOfUi(pg, new Runnable() {
-                       @Override
-                       public void run() {
-                               BasicLibrary lib = GuiReaderFrame.this.reader.getLibrary();
-                               Status status = lib.getStatus();
-
-                               if (status == Status.READY) {
-                                       lib.refresh(pg);
-                                       invalidate();
-                                       setJMenuBar(createMenu(true));
-                                       if (typeF == null) {
-                                               addBookPane(true, false);
-                                       } else {
-                                               addBookPane(typeF, true);
-                                       }
-                                       refreshBooks();
-                                       validate();
-                                       pane.setVisible(true);
-                               } else {
-                                       invalidate();
-                                       setJMenuBar(createMenu(false));
-                                       validate();
-
-                                       String err = lib.getLibraryName() + "\n";
-                                       switch (status) {
-                                       case INVALID:
-                                               err += "Library not valid";
-                                               break;
-
-                                       case UNAUTORIZED:
-                                               err += "You are not allowed to access this library";
-                                               break;
-
-                                       case UNAVAILABLE:
-                                               err += "Library currently unavailable";
-                                               break;
-
-                                       default:
-                                               err += "An error occured when contacting the library";
-                                               break;
-                                       }
-
-                                       error(err, "Library error", null);
-                               }
-                       }
-               });
+               setSize(800, 600);
+               setLayout(new BorderLayout());
+               add(helpee);
 
                setVisible(true);
        }
 
-       private void addListPane(String name, List<String> values,
-                       final boolean type) {
-               // Sources -> i18n
-               GuiReaderGroup bookPane = new GuiReaderGroup(reader, name, color);
-
-               List<MetaData> metas = new ArrayList<MetaData>();
-               for (String source : values) {
-                       MetaData mSource = new MetaData();
-                       mSource.setLuid(null);
-                       mSource.setTitle(source);
-                       mSource.setSource(source);
-                       metas.add(mSource);
-               }
-
-               bookPane.refreshBooks(metas, false);
-
-               this.invalidate();
-               pane.invalidate();
-               pane.add(bookPane);
-               pane.validate();
-               this.validate();
-
-               bookPane.setActionListener(new BookActionListener() {
-                       @Override
-                       public void select(GuiReaderBook book) {
-                               selectedBook = book;
-                       }
-
-                       @Override
-                       public void popupRequested(GuiReaderBook book, MouseEvent e) {
-                               JPopupMenu popup = new JPopupMenu();
-                               popup.add(createMenuItemOpenBook());
-                               popup.show(e.getComponent(), e.getX(), e.getY());
-                       }
-
-                       @Override
-                       public void action(final GuiReaderBook book) {
-                               removeBookPanes();
-                               addBookPane(book.getMeta().getSource(), type);
-                               refreshBooks();
-                       }
-               });
-       }
-
-       /**
-        * Add a new {@link GuiReaderGroup} on the frame to display all the
-        * sources/types or all the authors, or a listing of all the books sorted
-        * either by source or author.
-        * <p>
-        * A display of all the sources/types or all the authors will show one icon
-        * per source/type or author.
-        * <p>
-        * A listing of all the books sorted by source/type or author will display
-        * all the books.
-        * 
-        * @param type
-        *            TRUE for type/source, FALSE for author
-        * @param listMode
-        *            TRUE to get a listing of all the sources or authors, FALSE to
-        *            get one icon per source or author
-        */
-       private void addBookPane(boolean type, boolean listMode) {
-               if (type) {
-                       if (!listMode) {
-                               addListPane("Sources", reader.getLibrary().getSources(), type);
-                       } else {
-                               for (String tt : reader.getLibrary().getSources()) {
-                                       if (tt != null) {
-                                               addBookPane(tt, type);
-                                       }
-                               }
-                       }
-               } else {
-                       if (!listMode) {
-                               addListPane("Authors", reader.getLibrary().getAuthors(), type);
-                       } else {
-                               for (String tt : reader.getLibrary().getAuthors()) {
-                                       if (tt != null) {
-                                               addBookPane(tt, type);
-                                       }
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Add a new {@link GuiReaderGroup} on the frame to display the books of the
-        * selected type or author.
-        * 
-        * @param value
-        *            the author or the type, or NULL to get all the
-        *            authors-or-types
-        * @param type
-        *            TRUE for type/source, FALSE for author
-        * 
-        */
-       private void addBookPane(String value, boolean type) {
-               GuiReaderGroup bookPane = new GuiReaderGroup(reader, value, color);
-               if (type) {
-                       booksByType.put(bookPane, value);
-               } else {
-                       booksByAuthor.put(bookPane, value);
-               }
-
-               this.invalidate();
-               pane.invalidate();
-               pane.add(bookPane);
-               pane.validate();
-               this.validate();
-
-               bookPane.setActionListener(new BookActionListener() {
-                       @Override
-                       public void select(GuiReaderBook book) {
-                               selectedBook = book;
-                       }
-
-                       @Override
-                       public void popupRequested(GuiReaderBook book, MouseEvent e) {
-                               JPopupMenu popup = new JPopupMenu();
-                               popup.add(createMenuItemOpenBook());
-                               popup.addSeparator();
-                               popup.add(createMenuItemExport());
-                               popup.add(createMenuItemMoveTo(true));
-                               popup.add(createMenuItemSetCover());
-                               popup.add(createMenuItemClearCache());
-                               popup.add(createMenuItemRedownload());
-                               popup.addSeparator();
-                               popup.add(createMenuItemRename(true));
-                               popup.add(createMenuItemSetAuthor(true));
-                               popup.addSeparator();
-                               popup.add(createMenuItemDelete());
-                               popup.addSeparator();
-                               popup.add(createMenuItemProperties());
-                               popup.show(e.getComponent(), e.getX(), e.getY());
-                       }
-
-                       @Override
-                       public void action(final GuiReaderBook book) {
-                               openBook(book);
-                       }
-               });
-       }
-
-       /**
-        * Clear the pane from any book that may be present, usually prior to adding
-        * new ones.
-        */
-       private void removeBookPanes() {
-               booksByType.clear();
-               booksByAuthor.clear();
-               pane.invalidate();
-               this.invalidate();
-               pane.removeAll();
-               pane.validate();
-               this.validate();
+       @Override
+       public JPopupMenu createBookPopup() {
+               JPopupMenu popup = new JPopupMenu();
+               popup.add(createMenuItemOpenBook());
+               popup.addSeparator();
+               popup.add(createMenuItemExport());
+               popup.add(createMenuItemMoveTo(true));
+               popup.add(createMenuItemSetCover());
+               popup.add(createMenuItemClearCache());
+               popup.add(createMenuItemRedownload());
+               popup.addSeparator();
+               popup.add(createMenuItemRename(true));
+               popup.add(createMenuItemSetAuthor(true));
+               popup.addSeparator();
+               popup.add(createMenuItemDelete());
+               popup.addSeparator();
+               popup.add(createMenuItemProperties());
+               return popup;
        }
 
-       /**
-        * Refresh the list of {@link GuiReaderBook}s from disk.
-        */
-       private void refreshBooks() {
-               for (GuiReaderGroup group : booksByType.keySet()) {
-                       List<MetaData> stories = reader.getLibrary().getListBySource(
-                                       booksByType.get(group));
-                       group.refreshBooks(stories, words);
-               }
-
-               for (GuiReaderGroup group : booksByAuthor.keySet()) {
-                       List<MetaData> stories = reader.getLibrary().getListByAuthor(
-                                       booksByAuthor.get(group));
-                       group.refreshBooks(stories, words);
-               }
-
-               pane.repaint();
-               this.repaint();
+       @Override
+       public JPopupMenu createSourcePopup() {
+               JPopupMenu popup = new JPopupMenu();
+               popup.add(createMenuItemOpenBook());
+               return popup;
        }
 
-       /**
-        * Create the main menu bar.
-        * 
-        * @param libOk
-        *            the library can be queried
-        * 
-        * @return the bar
-        */
-       private JMenuBar createMenu(boolean libOk) {
-               bar = new JMenuBar();
+       @Override
+       public void createMenu(boolean libOk) {
+               JMenuBar bar = new JMenuBar();
 
                JMenu file = new JMenu("File");
                file.setMnemonic(KeyEvent.VK_F);
@@ -403,14 +119,14 @@ class GuiReaderFrame extends JFrame {
                imprt.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               imprt(true);
+                               helpee.imprt(true);
                        }
                });
                JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
                imprtF.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               imprt(false);
+                               helpee.imprt(false);
                        }
                });
                JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
@@ -453,8 +169,8 @@ class GuiReaderFrame extends JFrame {
                vauthors.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               words = false;
-                               refreshBooks();
+                               helpee.setWords(false);
+                               helpee.refreshBooks();
                        }
                });
                view.add(vauthors);
@@ -463,8 +179,8 @@ class GuiReaderFrame extends JFrame {
                vwords.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               words = true;
-                               refreshBooks();
+                               helpee.setWords(true);
+                               helpee.refreshBooks();
                        }
                });
                view.add(vwords);
@@ -494,7 +210,7 @@ class GuiReaderFrame extends JFrame {
                options.add(createMenuItemUiConfig());
                bar.add(options);
 
-               return bar;
+               setJMenuBar(bar);
        }
 
        // "" = [unknown]
@@ -564,9 +280,9 @@ class GuiReaderFrame extends JFrame {
                return new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               removeBookPanes();
-                               addBookPane(source, type);
-                               refreshBooks();
+                               helpee.removeBookPanes();
+                               helpee.addBookPane(source, type);
+                               helpee.refreshBooks();
                        }
                };
        }
@@ -576,9 +292,9 @@ class GuiReaderFrame extends JFrame {
                return new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               removeBookPanes();
-                               addBookPane(type, listMode);
-                               refreshBooks();
+                               helpee.removeBookPanes();
+                               helpee.addBookPane(type, listMode);
+                               helpee.refreshBooks();
                        }
                };
        }
@@ -673,6 +389,7 @@ class GuiReaderFrame extends JFrame {
                export.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
                                        fc.showDialog(GuiReaderFrame.this, "Save");
                                        if (fc.getSelectedFile() != null) {
@@ -681,7 +398,7 @@ class GuiReaderFrame extends JFrame {
                                                                .getAbsolutePath()
                                                                + type.getDefaultExtension(false);
                                                final Progress pg = new Progress();
-                                               outOfUi(pg, new Runnable() {
+                                               helpee.outOfUi(pg, new Runnable() {
                                                        @Override
                                                        public void run() {
                                                                try {
@@ -734,8 +451,9 @@ class GuiReaderFrame extends JFrame {
                refresh.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
-                                       outOfUi(null, new Runnable() {
+                                       helpee.outOfUi(null, new Runnable() {
                                                @Override
                                                public void run() {
                                                        reader.clearLocalReaderCache(selectedBook.getMeta()
@@ -776,7 +494,7 @@ class GuiReaderFrame extends JFrame {
                }
 
                JMenuItem item = new JMenuItem("New type...");
-               item.addActionListener(createMoveAction("SOURCE", null));
+               item.addActionListener(createMoveAction(MoveAction.SOURCE, null));
                changeTo.add(item);
                changeTo.addSeparator();
 
@@ -784,7 +502,7 @@ class GuiReaderFrame extends JFrame {
                        List<String> list = groupedSources.get(type);
                        if (list.size() == 1 && list.get(0).isEmpty()) {
                                item = new JMenuItem(type);
-                               item.addActionListener(createMoveAction("SOURCE", type));
+                               item.addActionListener(createMoveAction(MoveAction.SOURCE, type));
                                changeTo.add(item);
                        } else {
                                JMenu dir = new JMenu(type);
@@ -797,7 +515,7 @@ class GuiReaderFrame extends JFrame {
                                        }
 
                                        item = new JMenuItem(itemName);
-                                       item.addActionListener(createMoveAction("SOURCE",
+                                       item.addActionListener(createMoveAction(MoveAction.SOURCE,
                                                        actualType));
                                        dir.add(item);
                                }
@@ -824,7 +542,7 @@ class GuiReaderFrame extends JFrame {
                JMenuItem newItem = new JMenuItem("New author...");
                changeTo.add(newItem);
                changeTo.addSeparator();
-               newItem.addActionListener(createMoveAction("AUTHOR", null));
+               newItem.addActionListener(createMoveAction(MoveAction.AUTHOR, null));
 
                // Existing authors
                if (libOk) {
@@ -836,7 +554,8 @@ class GuiReaderFrame extends JFrame {
                                        JMenu group = new JMenu(key);
                                        for (String value : groupedAuthors.get(key)) {
                                                JMenuItem item = new JMenuItem(value);
-                                               item.addActionListener(createMoveAction("AUTHOR", value));
+                                               item.addActionListener(createMoveAction(
+                                                               MoveAction.AUTHOR, value));
                                                group.add(item);
                                        }
                                        changeTo.add(group);
@@ -844,7 +563,8 @@ class GuiReaderFrame extends JFrame {
                        } else if (groupedAuthors.size() == 1) {
                                for (String value : groupedAuthors.values().iterator().next()) {
                                        JMenuItem item = new JMenuItem(value);
-                                       item.addActionListener(createMoveAction("AUTHOR", value));
+                                       item.addActionListener(createMoveAction(MoveAction.AUTHOR,
+                                                       value));
                                        changeTo.add(item);
                                }
                        }
@@ -865,23 +585,25 @@ class GuiReaderFrame extends JFrame {
                        @SuppressWarnings("unused") boolean libOk) {
                JMenuItem changeTo = new JMenuItem("Rename...");
                changeTo.setMnemonic(KeyEvent.VK_R);
-               changeTo.addActionListener(createMoveAction("TITLE", null));
+               changeTo.addActionListener(createMoveAction(MoveAction.TITLE, null));
                return changeTo;
        }
 
-       private ActionListener createMoveAction(final String what, final String type) {
+       private ActionListener createMoveAction(final MoveAction what,
+                       final String type) {
                return new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
                                        String changeTo = type;
                                        if (type == null) {
                                                String init = "";
-                                               if (what.equals("SOURCE")) {
+                                               if (what == MoveAction.SOURCE) {
                                                        init = selectedBook.getMeta().getSource();
-                                               } else if (what.equals("TITLE")) {
+                                               } else if (what == MoveAction.TITLE) {
                                                        init = selectedBook.getMeta().getTitle();
-                                               } else if (what.equals("AUTHOR")) {
+                                               } else if (what == MoveAction.AUTHOR) {
                                                        init = selectedBook.getMeta().getAuthor();
                                                }
 
@@ -898,7 +620,7 @@ class GuiReaderFrame extends JFrame {
                                        }
 
                                        final String fChangeTo = changeTo;
-                                       outOfUi(null, new Runnable() {
+                                       helpee.outOfUi(null, new Runnable() {
                                                @Override
                                                public void run() {
                                                        if (what.equals("SOURCE")) {
@@ -912,12 +634,12 @@ class GuiReaderFrame extends JFrame {
                                                                                .getLuid(), fChangeTo);
                                                        }
 
-                                                       selectedBook = null;
+                                                       helpee.unsetSelectedBook();
 
                                                        SwingUtilities.invokeLater(new Runnable() {
                                                                @Override
                                                                public void run() {
-                                                                       setJMenuBar(createMenu(true));
+                                                                       createMenu(true);
                                                                }
                                                        });
                                                }
@@ -937,13 +659,14 @@ class GuiReaderFrame extends JFrame {
                refresh.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
                                        final MetaData meta = selectedBook.getMeta();
-                                       imprt(meta.getUrl(), new StoryRunnable() {
+                                       helpee.imprt(meta.getUrl(), new StoryRunnable() {
                                                @Override
                                                public void run(Story story) {
                                                        reader.delete(meta.getLuid());
-                                                       GuiReaderFrame.this.selectedBook = null;
+                                                       helpee.unsetSelectedBook();
                                                        MetaData newMeta = story.getMeta();
                                                        if (!newMeta.getSource().equals(meta.getSource())) {
                                                                reader.changeSource(newMeta.getLuid(),
@@ -968,12 +691,13 @@ class GuiReaderFrame extends JFrame {
                delete.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
-                                       outOfUi(null, new Runnable() {
+                                       helpee.outOfUi(null, new Runnable() {
                                                @Override
                                                public void run() {
                                                        reader.delete(selectedBook.getMeta().getLuid());
-                                                       selectedBook = null;
+                                                       helpee.unsetSelectedBook();
                                                }
                                        });
                                }
@@ -993,91 +717,13 @@ class GuiReaderFrame extends JFrame {
                delete.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
-                                       outOfUi(null, new Runnable() {
+                                       helpee.outOfUi(null, new Runnable() {
                                                @Override
                                                public void run() {
-                                                       final MetaData meta = selectedBook.getMeta();
-                                                       new JFrame() {
-                                                               private static final long serialVersionUID = 1L;
-                                                               @SuppressWarnings("unused")
-                                                               private Object init = init();
-
-                                                               private Object init() {
-                                                                       // Borders
-                                                                       int top = 20;
-                                                                       int space = 10;
-
-                                                                       // Image
-                                                                       ImageIcon img = GuiReaderCoverImager
-                                                                                       .generateCoverIcon(
-                                                                                                       reader.getLibrary(), meta);
-
-                                                                       // frame
-                                                                       setTitle(meta.getLuid() + ": "
-                                                                                       + meta.getTitle());
-
-                                                                       setSize(800, img.getIconHeight() + 2 * top);
-                                                                       setLayout(new BorderLayout());
-
-                                                                       // Main panel
-                                                                       JPanel mainPanel = new JPanel(
-                                                                                       new BorderLayout());
-                                                                       JPanel mainPanelKeys = new JPanel();
-                                                                       mainPanelKeys.setLayout(new BoxLayout(
-                                                                                       mainPanelKeys, BoxLayout.Y_AXIS));
-                                                                       JPanel mainPanelValues = new JPanel();
-                                                                       mainPanelValues.setLayout(new BoxLayout(
-                                                                                       mainPanelValues, BoxLayout.Y_AXIS));
-
-                                                                       mainPanel.add(mainPanelKeys,
-                                                                                       BorderLayout.WEST);
-                                                                       mainPanel.add(mainPanelValues,
-                                                                                       BorderLayout.CENTER);
-
-                                                                       List<Entry<String, String>> infos = BasicReader
-                                                                                       .getMetaDesc(meta);
-
-                                                                       Color trans = new Color(0, 0, 0, 1);
-                                                                       for (Entry<String, String> info : infos) {
-                                                                               JTextArea key = new JTextArea(info
-                                                                                               .getKey());
-                                                                               key.setFont(new Font(key.getFont()
-                                                                                               .getFontName(), Font.BOLD, key
-                                                                                               .getFont().getSize()));
-                                                                               key.setEditable(false);
-                                                                               key.setLineWrap(false);
-                                                                               key.setBackground(trans);
-                                                                               mainPanelKeys.add(key);
-
-                                                                               JTextArea value = new JTextArea(info
-                                                                                               .getValue());
-                                                                               value.setEditable(false);
-                                                                               value.setLineWrap(false);
-                                                                               value.setBackground(trans);
-                                                                               mainPanelValues.add(value);
-                                                                       }
-
-                                                                       // Image
-                                                                       JLabel imgLabel = new JLabel(img);
-                                                                       imgLabel.setVerticalAlignment(JLabel.TOP);
-
-                                                                       // Borders
-                                                                       mainPanelKeys.setBorder(BorderFactory
-                                                                                       .createEmptyBorder(top, space, 0, 0));
-                                                                       mainPanelValues.setBorder(BorderFactory
-                                                                                       .createEmptyBorder(top, space, 0, 0));
-                                                                       imgLabel.setBorder(BorderFactory
-                                                                                       .createEmptyBorder(0, space, 0, 0));
-
-                                                                       // Add all
-                                                                       add(imgLabel, BorderLayout.WEST);
-                                                                       add(mainPanel, BorderLayout.CENTER);
-
-                                                                       return null;
-                                                               }
-
-                                                       }.setVisible(true);
+                                                       new GuiReaderPropertiesFrame(reader, selectedBook
+                                                                       .getMeta()).setVisible(true);
                                                }
                                        });
                                }
@@ -1088,22 +734,24 @@ class GuiReaderFrame extends JFrame {
        }
 
        /**
-        * Create the open menu item for a book or a source (no LUID).
+        * Create the open menu item for a book or a source/type (no LUID).
         * 
         * @return the item
         */
-       private JMenuItem createMenuItemOpenBook() {
+       public JMenuItem createMenuItemOpenBook() {
                JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
                open.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
                                        if (selectedBook.getMeta().getLuid() == null) {
-                                               removeBookPanes();
-                                               addBookPane(selectedBook.getMeta().getSource(), true);
-                                               refreshBooks();
+                                               helpee.removeBookPanes();
+                                               helpee.addBookPane(selectedBook.getMeta().getSource(),
+                                                               true);
+                                               helpee.refreshBooks();
                                        } else {
-                                               openBook(selectedBook);
+                                               helpee.openBook(selectedBook);
                                        }
                                }
                        }
@@ -1123,6 +771,7 @@ class GuiReaderFrame extends JFrame {
                open.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = helpee.getSelectedBook();
                                if (selectedBook != null) {
                                        reader.getLibrary().setSourceCover(
                                                        selectedBook.getMeta().getSource(),
@@ -1137,197 +786,6 @@ class GuiReaderFrame extends JFrame {
                return open;
        }
 
-       /**
-        * Open a {@link GuiReaderBook} item.
-        * 
-        * @param book
-        *            the {@link GuiReaderBook} to open
-        */
-       private void openBook(final GuiReaderBook book) {
-               final Progress pg = new Progress();
-               outOfUi(pg, new Runnable() {
-                       @Override
-                       public void run() {
-                               try {
-                                       reader.read(book.getMeta().getLuid(), false, pg);
-                                       SwingUtilities.invokeLater(new Runnable() {
-                                               @Override
-                                               public void run() {
-                                                       book.setCached(true);
-                                               }
-                                       });
-                               } catch (IOException e) {
-                                       // TODO: error message?
-                                       Instance.getTraceHandler().error(e);
-                               }
-                       }
-               });
-       }
-
-       /**
-        * Process the given action out of the Swing UI thread and link the given
-        * {@link ProgressBar} to the action.
-        * <p>
-        * The code will make sure that the {@link ProgressBar} (if not NULL) is set
-        * to done when the action is done.
-        * 
-        * @param progress
-        *            the {@link ProgressBar} or NULL
-        * @param run
-        *            the action to run
-        */
-       private void outOfUi(Progress progress, final Runnable run) {
-               final Progress pg = new Progress();
-               final Progress reload = new Progress("Reload books");
-               if (progress == null) {
-                       progress = new Progress();
-               }
-
-               pg.addProgress(progress, 90);
-               pg.addProgress(reload, 10);
-
-               invalidate();
-               pgBar.setProgress(pg);
-               validate();
-               setEnabled(false);
-
-               new Thread(new Runnable() {
-                       @Override
-                       public void run() {
-                               try {
-                                       run.run();
-                                       refreshBooks();
-                               } finally {
-                                       reload.done();
-                                       if (!pg.isDone()) {
-                                               // will trigger pgBar ActionListener:
-                                               pg.done();
-                                       }
-                               }
-                       }
-               }, "outOfUi thread").start();
-       }
-
-       /**
-        * Import a {@link Story} into the main {@link LocalLibrary}.
-        * <p>
-        * Should be called inside the UI thread.
-        * 
-        * @param askUrl
-        *            TRUE for an {@link URL}, false for a {@link File}
-        */
-       private void imprt(boolean askUrl) {
-               JFileChooser fc = new JFileChooser();
-
-               Object url;
-               if (askUrl) {
-                       String clipboard = "";
-                       try {
-                               clipboard = ("" + Toolkit.getDefaultToolkit()
-                                               .getSystemClipboard().getData(DataFlavor.stringFlavor))
-                                               .trim();
-                       } catch (Exception e) {
-                               // No data will be handled
-                       }
-
-                       if (clipboard == null || !clipboard.startsWith("http")) {
-                               clipboard = "";
-                       }
-
-                       url = JOptionPane.showInputDialog(GuiReaderFrame.this,
-                                       "url of the story to import?", "Importing from URL",
-                                       JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
-               } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
-                       url = fc.getSelectedFile().getAbsolutePath();
-               } else {
-                       url = null;
-               }
-
-               if (url != null && !url.toString().isEmpty()) {
-                       imprt(url.toString(), null, null);
-               }
-       }
-
-       /**
-        * Actually import the {@link Story} into the main {@link LocalLibrary}.
-        * <p>
-        * Should be called inside the UI thread.
-        * 
-        * @param url
-        *            the {@link Story} to import by {@link URL}
-        * @param onSuccess
-        *            Action to execute on success
-        */
-       private void imprt(final String url, final StoryRunnable onSuccess,
-                       String onSuccessPgName) {
-               final Progress pg = new Progress();
-               final Progress pgImprt = new Progress();
-               final Progress pgOnSuccess = new Progress(onSuccessPgName);
-               pg.addProgress(pgImprt, 95);
-               pg.addProgress(pgOnSuccess, 5);
-
-               outOfUi(pg, new Runnable() {
-                       @Override
-                       public void run() {
-                               Exception ex = null;
-                               Story story = null;
-                               try {
-                                       story = reader.getLibrary().imprt(BasicReader.getUrl(url),
-                                                       pgImprt);
-                               } catch (IOException e) {
-                                       ex = e;
-                               }
-
-                               final Exception e = ex;
-
-                               final boolean ok = (e == null);
-
-                               pgOnSuccess.setProgress(0);
-                               if (!ok) {
-                                       if (e instanceof UnknownHostException) {
-                                               error("URL not supported: " + url, "Cannot import URL",
-                                                               null);
-                                       } else {
-                                               error("Failed to import " + url + ": \n"
-                                                               + e.getMessage(), "Cannot import URL", e);
-                                       }
-                               } else {
-                                       if (onSuccess != null) {
-                                               onSuccess.run(story);
-                                       }
-                               }
-                               pgOnSuccess.done();
-                       }
-               });
-       }
-
-       /**
-        * Enables or disables this component, depending on the value of the
-        * parameter <code>b</code>. An enabled component can respond to user input
-        * and generate events. Components are enabled initially by default.
-        * <p>
-        * Disabling this component will also affect its children.
-        * 
-        * @param b
-        *            If <code>true</code>, this component is enabled; otherwise
-        *            this component is disabled
-        */
-       @Override
-       public void setEnabled(boolean b) {
-               if (bar != null) {
-                       bar.setEnabled(b);
-               }
-
-               for (GuiReaderGroup group : booksByType.keySet()) {
-                       group.setEnabled(b);
-               }
-               for (GuiReaderGroup group : booksByAuthor.keySet()) {
-                       group.setEnabled(b);
-               }
-               super.setEnabled(b);
-               repaint();
-       }
-
        /**
         * Display an error message and log the linked {@link Exception}.
         * 
@@ -1338,7 +796,7 @@ class GuiReaderFrame extends JFrame {
         * @param e
         *            the exception to log if any
         */
-       private void error(final String message, final String title, Exception e) {
+       public void error(final String message, final String title, Exception e) {
                Instance.getTraceHandler().error(title + ": " + message);
                if (e != null) {
                        Instance.getTraceHandler().error(e);
@@ -1352,4 +810,9 @@ class GuiReaderFrame extends JFrame {
                        }
                });
        }
+
+       @Override
+       public GuiReader getReader() {
+               return reader;
+       }
 }
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderMainPanel.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderMainPanel.java
new file mode 100644 (file)
index 0000000..a53d572
--- /dev/null
@@ -0,0 +1,625 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Frame;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.BoxLayout;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JMenuBar;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.bundles.UiConfig;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.data.Story;
+import be.nikiroo.fanfix.library.BasicLibrary;
+import be.nikiroo.fanfix.library.BasicLibrary.Status;
+import be.nikiroo.fanfix.library.LocalLibrary;
+import be.nikiroo.fanfix.reader.BasicReader;
+import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
+import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.ui.ProgressBar;
+
+/**
+ * A {@link Frame} that will show a {@link GuiReaderBook} item for each
+ * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
+ * way to copy them to the {@link GuiReader} cache (
+ * {@link BasicReader#getLibrary()}), read them, delete them...
+ * 
+ * @author niki
+ */
+class GuiReaderMainPanel extends JPanel {
+       private static final long serialVersionUID = 1L;
+       private FrameHelper helper;
+       private Map<GuiReaderGroup, String> booksByType;
+       private Map<GuiReaderGroup, String> booksByAuthor;
+       private JPanel pane;
+       private Color color;
+       private ProgressBar pgBar;
+       private JMenuBar bar;
+       private GuiReaderBook selectedBook;
+       private boolean words; // words or authors (secondary info on books)
+
+       /**
+        * An object that offers some helper methods to access the frame that host
+        * it and the Fanfix-related functions.
+        * 
+        * @author niki
+        */
+       public interface FrameHelper {
+               /**
+                * Return the reader associated to this {@link FrameHelper}.
+                * 
+                * @return the reader
+                */
+               public GuiReader getReader();
+
+               /**
+                * Create the main menu bar.
+                * 
+                * @param libOk
+                *            the library can be queried
+                * 
+                * @return the bar
+                */
+               public void createMenu(boolean b);
+
+               /**
+                * Create a popup menu for a {@link GuiReaderBook} that represents a
+                * story.
+                * 
+                * @return the popup menu to display
+                */
+               public JPopupMenu createBookPopup();
+
+               /**
+                * Create a popup menu for a {@link GuiReaderBook} that represents a
+                * source/type (no LUID).
+                * 
+                * @return the popup menu to display
+                */
+               public JPopupMenu createSourcePopup();
+       }
+
+       /**
+        * A {@link Runnable} with a {@link Story} parameter.
+        * 
+        * @author niki
+        */
+       public interface StoryRunnable {
+               /**
+                * Run the action.
+                * 
+                * @param story
+                *            the story
+                */
+               public void run(Story story);
+       }
+
+       /**
+        * Create a new {@link GuiReaderMainPanel}.
+        * 
+        * @param reader
+        *            the associated {@link GuiReader} to forward some commands and
+        *            access its {@link LocalLibrary}
+        * @param type
+        *            the type of {@link Story} to load, or NULL for all types
+        */
+       public GuiReaderMainPanel(FrameHelper parent, String type) {
+               super(new BorderLayout(), true);
+
+               this.helper = parent;
+
+               pane = new JPanel();
+               pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
+
+               Integer icolor = Instance.getUiConfig().getColor(
+                               UiConfig.BACKGROUND_COLOR);
+               if (icolor != null) {
+                       color = new Color(icolor);
+                       setBackground(color);
+                       pane.setBackground(color);
+               }
+
+               JScrollPane scroll = new JScrollPane(pane);
+               scroll.getVerticalScrollBar().setUnitIncrement(16);
+               add(scroll, BorderLayout.CENTER);
+
+               String message = parent.getReader().getLibrary().getLibraryName();
+               if (!message.isEmpty()) {
+                       JLabel name = new JLabel(message, SwingConstants.CENTER);
+                       add(name, BorderLayout.NORTH);
+               }
+
+               pgBar = new ProgressBar();
+               add(pgBar, BorderLayout.SOUTH);
+
+               pgBar.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               invalidate();
+                               pgBar.setProgress(null);
+                               validate();
+                               setEnabled(true);
+                       }
+               });
+
+               pgBar.addUpdateListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               invalidate();
+                               validate();
+                               repaint();
+                       }
+               });
+
+               booksByType = new HashMap<GuiReaderGroup, String>();
+               booksByAuthor = new HashMap<GuiReaderGroup, String>();
+
+               pane.setVisible(false);
+               final Progress pg = new Progress();
+               final String typeF = type;
+               outOfUi(pg, new Runnable() {
+                       @Override
+                       public void run() {
+                               BasicLibrary lib = helper.getReader().getLibrary();
+                               Status status = lib.getStatus();
+
+                               if (status == Status.READY) {
+                                       lib.refresh(pg);
+                                       invalidate();
+                                       helper.createMenu(true);
+                                       if (typeF == null) {
+                                               addBookPane(true, false);
+                                       } else {
+                                               addBookPane(typeF, true);
+                                       }
+                                       refreshBooks();
+                                       validate();
+                                       pane.setVisible(true);
+                               } else {
+                                       invalidate();
+                                       helper.createMenu(false);
+                                       validate();
+
+                                       String err = lib.getLibraryName() + "\n";
+                                       switch (status) {
+                                       case INVALID:
+                                               err += "Library not valid";
+                                               break;
+
+                                       case UNAUTORIZED:
+                                               err += "You are not allowed to access this library";
+                                               break;
+
+                                       case UNAVAILABLE:
+                                               err += "Library currently unavailable";
+                                               break;
+
+                                       default:
+                                               err += "An error occured when contacting the library";
+                                               break;
+                                       }
+
+                                       error(err, "Library error", null);
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Add a new {@link GuiReaderGroup} on the frame to display all the
+        * sources/types or all the authors, or a listing of all the books sorted
+        * either by source or author.
+        * <p>
+        * A display of all the sources/types or all the authors will show one icon
+        * per source/type or author.
+        * <p>
+        * A listing of all the books sorted by source/type or author will display
+        * all the books.
+        * 
+        * @param type
+        *            TRUE for type/source, FALSE for author
+        * @param listMode
+        *            TRUE to get a listing of all the sources or authors, FALSE to
+        *            get one icon per source or author
+        */
+       public void addBookPane(boolean type, boolean listMode) {
+               BasicLibrary lib = helper.getReader().getLibrary();
+               if (type) {
+                       if (!listMode) {
+                               addListPane("Sources", lib.getSources(), type);
+                       } else {
+                               for (String tt : lib.getSources()) {
+                                       if (tt != null) {
+                                               addBookPane(tt, type);
+                                       }
+                               }
+                       }
+               } else {
+                       if (!listMode) {
+                               addListPane("Authors", lib.getAuthors(), type);
+                       } else {
+                               for (String tt : lib.getAuthors()) {
+                                       if (tt != null) {
+                                               addBookPane(tt, type);
+                                       }
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Add a new {@link GuiReaderGroup} on the frame to display the books of the
+        * selected type or author.
+        * 
+        * @param value
+        *            the author or the type, or NULL to get all the
+        *            authors-or-types
+        * @param type
+        *            TRUE for type/source, FALSE for author
+        * 
+        */
+       public void addBookPane(String value, boolean type) {
+               GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), value,
+                               color);
+               if (type) {
+                       booksByType.put(bookPane, value);
+               } else {
+                       booksByAuthor.put(bookPane, value);
+               }
+
+               this.invalidate();
+               pane.invalidate();
+               pane.add(bookPane);
+               pane.validate();
+               this.validate();
+
+               bookPane.setActionListener(new BookActionListener() {
+                       @Override
+                       public void select(GuiReaderBook book) {
+                               selectedBook = book;
+                       }
+
+                       @Override
+                       public void popupRequested(GuiReaderBook book, MouseEvent e) {
+                               JPopupMenu popup = helper.createBookPopup();
+                               popup.show(e.getComponent(), e.getX(), e.getY());
+                       }
+
+                       @Override
+                       public void action(final GuiReaderBook book) {
+                               openBook(book);
+                       }
+               });
+       }
+
+       /**
+        * Clear the pane from any book that may be present, usually prior to adding
+        * new ones.
+        */
+       public void removeBookPanes() {
+               booksByType.clear();
+               booksByAuthor.clear();
+               pane.invalidate();
+               this.invalidate();
+               pane.removeAll();
+               pane.validate();
+               this.validate();
+       }
+
+       /**
+        * Refresh the list of {@link GuiReaderBook}s from disk.
+        */
+       public void refreshBooks() {
+               BasicLibrary lib = helper.getReader().getLibrary();
+               for (GuiReaderGroup group : booksByType.keySet()) {
+                       List<MetaData> stories = lib
+                                       .getListBySource(booksByType.get(group));
+                       group.refreshBooks(stories, words);
+               }
+
+               for (GuiReaderGroup group : booksByAuthor.keySet()) {
+                       List<MetaData> stories = lib.getListByAuthor(booksByAuthor
+                                       .get(group));
+                       group.refreshBooks(stories, words);
+               }
+
+               pane.repaint();
+               this.repaint();
+       }
+
+       /**
+        * Open a {@link GuiReaderBook} item.
+        * 
+        * @param book
+        *            the {@link GuiReaderBook} to open
+        */
+       public void openBook(final GuiReaderBook book) {
+               final Progress pg = new Progress();
+               outOfUi(pg, new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       helper.getReader()
+                                                       .read(book.getMeta().getLuid(), false, pg);
+                                       SwingUtilities.invokeLater(new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       book.setCached(true);
+                                               }
+                                       });
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(e);
+                                       error("Cannot open the selected book", "Error", e);
+                               }
+                       }
+               });
+       }
+
+       /**
+        * Process the given action out of the Swing UI thread and link the given
+        * {@link ProgressBar} to the action.
+        * <p>
+        * The code will make sure that the {@link ProgressBar} (if not NULL) is set
+        * to done when the action is done.
+        * 
+        * @param progress
+        *            the {@link ProgressBar} or NULL
+        * @param run
+        *            the action to run
+        */
+       public void outOfUi(Progress progress, final Runnable run) {
+               final Progress pg = new Progress();
+               final Progress reload = new Progress("Reload books");
+               if (progress == null) {
+                       progress = new Progress();
+               }
+
+               pg.addProgress(progress, 90);
+               pg.addProgress(reload, 10);
+
+               invalidate();
+               pgBar.setProgress(pg);
+               validate();
+               setEnabled(false);
+
+               new Thread(new Runnable() {
+                       @Override
+                       public void run() {
+                               try {
+                                       run.run();
+                                       refreshBooks();
+                               } finally {
+                                       reload.done();
+                                       if (!pg.isDone()) {
+                                               // will trigger pgBar ActionListener:
+                                               pg.done();
+                                       }
+                               }
+                       }
+               }, "outOfUi thread").start();
+       }
+
+       /**
+        * Import a {@link Story} into the main {@link LocalLibrary}.
+        * <p>
+        * Should be called inside the UI thread.
+        * 
+        * @param askUrl
+        *            TRUE for an {@link URL}, false for a {@link File}
+        */
+       public void imprt(boolean askUrl) {
+               JFileChooser fc = new JFileChooser();
+
+               Object url;
+               if (askUrl) {
+                       String clipboard = "";
+                       try {
+                               clipboard = ("" + Toolkit.getDefaultToolkit()
+                                               .getSystemClipboard().getData(DataFlavor.stringFlavor))
+                                               .trim();
+                       } catch (Exception e) {
+                               // No data will be handled
+                       }
+
+                       if (clipboard == null || !clipboard.startsWith("http")) {
+                               clipboard = "";
+                       }
+
+                       url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
+                                       "url of the story to import?", "Importing from URL",
+                                       JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
+               } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
+                       url = fc.getSelectedFile().getAbsolutePath();
+               } else {
+                       url = null;
+               }
+
+               if (url != null && !url.toString().isEmpty()) {
+                       imprt(url.toString(), null, null);
+               }
+       }
+
+       /**
+        * Actually import the {@link Story} into the main {@link LocalLibrary}.
+        * <p>
+        * Should be called inside the UI thread.
+        * 
+        * @param url
+        *            the {@link Story} to import by {@link URL}
+        * @param onSuccess
+        *            Action to execute on success
+        */
+       public void imprt(final String url, final StoryRunnable onSuccess,
+                       String onSuccessPgName) {
+               final Progress pg = new Progress();
+               final Progress pgImprt = new Progress();
+               final Progress pgOnSuccess = new Progress(onSuccessPgName);
+               pg.addProgress(pgImprt, 95);
+               pg.addProgress(pgOnSuccess, 5);
+
+               outOfUi(pg, new Runnable() {
+                       @Override
+                       public void run() {
+                               Exception ex = null;
+                               Story story = null;
+                               try {
+                                       story = helper.getReader().getLibrary()
+                                                       .imprt(BasicReader.getUrl(url), pgImprt);
+                               } catch (IOException e) {
+                                       ex = e;
+                               }
+
+                               final Exception e = ex;
+
+                               final boolean ok = (e == null);
+
+                               pgOnSuccess.setProgress(0);
+                               if (!ok) {
+                                       if (e instanceof UnknownHostException) {
+                                               error("URL not supported: " + url, "Cannot import URL",
+                                                               null);
+                                       } else {
+                                               error("Failed to import " + url + ": \n"
+                                                               + e.getMessage(), "Cannot import URL", e);
+                                       }
+                               } else {
+                                       if (onSuccess != null) {
+                                               onSuccess.run(story);
+                                       }
+                               }
+                               pgOnSuccess.done();
+                       }
+               });
+       }
+
+       /**
+        * Enables or disables this component, depending on the value of the
+        * parameter <code>b</code>. An enabled component can respond to user input
+        * and generate events. Components are enabled initially by default.
+        * <p>
+        * Enabling or disabling <b>this</b> component will also affect its
+        * children.
+        * 
+        * @param b
+        *            If <code>true</code>, this component is enabled; otherwise
+        *            this component is disabled
+        */
+       @Override
+       public void setEnabled(boolean b) {
+               if (bar != null) {
+                       bar.setEnabled(b);
+               }
+
+               for (GuiReaderGroup group : booksByType.keySet()) {
+                       group.setEnabled(b);
+               }
+               for (GuiReaderGroup group : booksByAuthor.keySet()) {
+                       group.setEnabled(b);
+               }
+               super.setEnabled(b);
+               repaint();
+       }
+
+       public void setWords(boolean words) {
+               this.words = words;
+       }
+
+       public GuiReaderBook getSelectedBook() {
+               return selectedBook;
+       }
+
+       public void unsetSelectedBook() {
+               selectedBook = null;
+       }
+
+       private void addListPane(String name, List<String> values,
+                       final boolean type) {
+               // Sources -> i18n
+               GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), name,
+                               color);
+
+               List<MetaData> metas = new ArrayList<MetaData>();
+               for (String source : values) {
+                       MetaData mSource = new MetaData();
+                       mSource.setLuid(null);
+                       mSource.setTitle(source);
+                       mSource.setSource(source);
+                       metas.add(mSource);
+               }
+
+               bookPane.refreshBooks(metas, false);
+
+               this.invalidate();
+               pane.invalidate();
+               pane.add(bookPane);
+               pane.validate();
+               this.validate();
+
+               bookPane.setActionListener(new BookActionListener() {
+                       @Override
+                       public void select(GuiReaderBook book) {
+                               selectedBook = book;
+                       }
+
+                       @Override
+                       public void popupRequested(GuiReaderBook book, MouseEvent e) {
+                               JPopupMenu popup = helper.createSourcePopup();
+                               popup.show(e.getComponent(), e.getX(), e.getY());
+                       }
+
+                       @Override
+                       public void action(final GuiReaderBook book) {
+                               removeBookPanes();
+                               addBookPane(book.getMeta().getSource(), type);
+                               refreshBooks();
+                       }
+               });
+       }
+
+       /**
+        * Display an error message and log the linked {@link Exception}.
+        * 
+        * @param message
+        *            the message
+        * @param title
+        *            the title of the error message
+        * @param e
+        *            the exception to log if any
+        */
+       private void error(final String message, final String title, Exception e) {
+               Instance.getTraceHandler().error(title + ": " + message);
+               if (e != null) {
+                       Instance.getTraceHandler().error(e);
+               }
+
+               SwingUtilities.invokeLater(new Runnable() {
+                       @Override
+                       public void run() {
+                               JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
+                                               title, JOptionPane.ERROR_MESSAGE);
+                       }
+               });
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderPropertiesFrame.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderPropertiesFrame.java
new file mode 100644 (file)
index 0000000..5a647de
--- /dev/null
@@ -0,0 +1,84 @@
+package be.nikiroo.fanfix.reader.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Font;
+import java.util.List;
+import java.util.Map.Entry;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextArea;
+
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.reader.BasicReader;
+import be.nikiroo.fanfix.reader.Reader;
+
+public class GuiReaderPropertiesFrame extends JFrame {
+       private static final long serialVersionUID = 1L;
+
+       public GuiReaderPropertiesFrame(Reader reader, MetaData meta) {
+               // Borders
+               int top = 20;
+               int space = 10;
+
+               // Image
+               ImageIcon img = GuiReaderCoverImager.generateCoverIcon(
+                               reader.getLibrary(), meta);
+
+               // frame
+               setTitle(meta.getLuid() + ": " + meta.getTitle());
+
+               setSize(800, img.getIconHeight() + 2 * top);
+               setLayout(new BorderLayout());
+
+               // Main panel
+               JPanel mainPanel = new JPanel(new BorderLayout());
+               JPanel mainPanelKeys = new JPanel();
+               mainPanelKeys.setLayout(new BoxLayout(mainPanelKeys, BoxLayout.Y_AXIS));
+               JPanel mainPanelValues = new JPanel();
+               mainPanelValues.setLayout(new BoxLayout(mainPanelValues,
+                               BoxLayout.Y_AXIS));
+
+               mainPanel.add(mainPanelKeys, BorderLayout.WEST);
+               mainPanel.add(mainPanelValues, BorderLayout.CENTER);
+
+               List<Entry<String, String>> infos = BasicReader.getMetaDesc(meta);
+
+               Color trans = new Color(0, 0, 0, 1);
+               for (Entry<String, String> info : infos) {
+                       JTextArea key = new JTextArea(info.getKey());
+                       key.setFont(new Font(key.getFont().getFontName(), Font.BOLD, key
+                                       .getFont().getSize()));
+                       key.setEditable(false);
+                       key.setLineWrap(false);
+                       key.setBackground(trans);
+                       mainPanelKeys.add(key);
+
+                       JTextArea value = new JTextArea(info.getValue());
+                       value.setEditable(false);
+                       value.setLineWrap(false);
+                       value.setBackground(trans);
+                       mainPanelValues.add(value);
+               }
+
+               // Image
+               JLabel imgLabel = new JLabel(img);
+               imgLabel.setVerticalAlignment(JLabel.TOP);
+
+               // Borders
+               mainPanelKeys.setBorder(BorderFactory.createEmptyBorder(top, space, 0,
+                               0));
+               mainPanelValues.setBorder(BorderFactory.createEmptyBorder(top, space,
+                               0, 0));
+               imgLabel.setBorder(BorderFactory.createEmptyBorder(0, space, 0, 0));
+
+               // Add all
+               add(imgLabel, BorderLayout.WEST);
+               add(mainPanel, BorderLayout.CENTER);
+       }
+}