gui: do not always refresh the books after an action
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderFrame.java
index b8db8f747e420de4e89884aeb47be5a71105d10c..97541026188404fd27420d5184f38c41a02e2f54 100644 (file)
@@ -1,37 +1,25 @@
 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.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.BoxLayout;
 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.SwingConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.filechooser.FileFilter;
 import javax.swing.filechooser.FileNameExtensionFilter;
@@ -42,15 +30,14 @@ 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
@@ -60,31 +47,23 @@ 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 mainPanel;
 
        /**
-        * A {@link Runnable} with a {@link Story} parameter.
+        * The different modification actions you can use on {@link Story} items.
         * 
         * @author niki
         */
-       private interface StoryRunnable {
-               /**
-                * Run the action.
-                * 
-                * @param story
-                *            the story
-                */
-               public void run(Story story);
+       private enum ChangeAction {
+               /** Change the source/type, that is, move it to another source. */
+               SOURCE,
+               /** Change its name. */
+               TITLE,
+               /** Change its author. */
+               AUTHOR
        }
 
        /**
@@ -101,263 +80,46 @@ class GuiReaderFrame extends JFrame {
 
                this.reader = reader;
 
-               setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+               mainPanel = new GuiReaderMainPanel(this, type);
+
                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>();
-
-               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));
-                                       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);
-                               }
-                       }
-               });
-
-               setVisible(true);
+               add(mainPanel, BorderLayout.CENTER);
        }
 
-       private void addSourcePanes() {
-               // Sources -> i18n
-               GuiReaderGroup bookPane = new GuiReaderGroup(reader, "Sources", color);
-
-               List<MetaData> sources = new ArrayList<MetaData>();
-               for (String source : reader.getLibrary().getSources()) {
-                       MetaData mSource = new MetaData();
-                       mSource.setLuid(null);
-                       mSource.setTitle(source);
-                       mSource.setSource(source);
-                       sources.add(mSource);
-               }
-
-               bookPane.refreshBooks(sources, 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(), true);
-                               refreshBooks();
-                       }
-               });
-       }
-
-       /**
-        * 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, FALSE for author
-        */
-       private void addBookPane(String value, boolean type) {
-               if (value == null) {
-                       if (type) {
-                               if (Instance.getUiConfig().getBoolean(UiConfig.SOURCE_PAGE,
-                                               false)) {
-                                       addSourcePanes();
-                               } else {
-                                       for (String tt : reader.getLibrary().getSources()) {
-                                               if (tt != null) {
-                                                       addBookPane(tt, type);
-                                               }
-                                       }
-                               }
-                       } else {
-                               for (String tt : reader.getLibrary().getAuthors()) {
-                                       if (tt != null) {
-                                               addBookPane(tt, type);
-                                       }
-                               }
-                       }
-
-                       return;
-               }
-
-               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(createMenuItemMove(true));
-                               popup.add(createMenuItemSetCover());
-                               popup.add(createMenuItemClearCache());
-                               popup.add(createMenuItemRedownload());
-                               popup.addSeparator();
-                               popup.add(createMenuItemDelete());
-                               popup.show(e.getComponent(), e.getX(), e.getY());
-                       }
-
-                       @Override
-                       public void action(final GuiReaderBook book) {
-                               openBook(book);
-                       }
-               });
+       @Override
+       public JPopupMenu createBookPopup() {
+               JPopupMenu popup = new JPopupMenu();
+               popup.add(createMenuItemOpenBook());
+               popup.addSeparator();
+               popup.add(createMenuItemExport());
+               popup.add(createMenuItemMoveTo(true));
+               popup.add(createMenuItemSetCoverForSource());
+               popup.add(createMenuItemSetCoverForAuthor());
+               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;
        }
 
-       private void removeBookPanes() {
-               booksByType.clear();
-               booksByAuthor.clear();
-               pane.invalidate();
-               this.invalidate();
-               pane.removeAll();
-               pane.validate();
-               this.validate();
+       @Override
+       public JPopupMenu createSourceAuthorPopup() {
+               JPopupMenu popup = new JPopupMenu();
+               popup.add(createMenuItemOpenBook());
+               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 void createMenu(boolean libOk) {
+               invalidate();
 
-       /**
-        * Create the main menu bar.
-        * 
-        * @param libOk
-        *            the library can be queried
-        * 
-        * @return the bar
-        */
-       private JMenuBar createMenu(boolean libOk) {
-               bar = new JMenuBar();
+               JMenuBar bar = new JMenuBar();
 
                JMenu file = new JMenu("File");
                file.setMnemonic(KeyEvent.VK_F);
@@ -366,14 +128,14 @@ class GuiReaderFrame extends JFrame {
                imprt.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               imprt(true);
+                               mainPanel.imprt(true);
                        }
                });
                JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
                imprtF.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               imprt(false);
+                               mainPanel.imprt(false);
                        }
                });
                JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
@@ -387,11 +149,14 @@ class GuiReaderFrame extends JFrame {
 
                file.add(createMenuItemOpenBook());
                file.add(createMenuItemExport());
-               file.add(createMenuItemMove(libOk));
+               file.add(createMenuItemMoveTo(libOk));
                file.addSeparator();
                file.add(imprt);
                file.add(imprtF);
                file.addSeparator();
+               file.add(createMenuItemRename(libOk));
+               file.add(createMenuItemSetAuthor(libOk));
+               file.addSeparator();
                file.add(exit);
 
                bar.add(file);
@@ -413,8 +178,8 @@ class GuiReaderFrame extends JFrame {
                vauthors.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               words = false;
-                               refreshBooks();
+                               mainPanel.setWords(false);
+                               mainPanel.refreshBooks();
                        }
                });
                view.add(vauthors);
@@ -423,67 +188,29 @@ class GuiReaderFrame extends JFrame {
                vwords.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               words = true;
-                               refreshBooks();
+                               mainPanel.setWords(true);
+                               mainPanel.refreshBooks();
                        }
                });
                view.add(vwords);
                bar.add(view);
 
+               Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
+               if (libOk) {
+                       groupedSources = reader.getLibrary().getSourcesGrouped();
+               }
                JMenu sources = new JMenu("Sources");
                sources.setMnemonic(KeyEvent.VK_S);
+               populateMenuSA(sources, groupedSources, true);
+               bar.add(sources);
 
-               List<String> tt = new ArrayList<String>();
+               Map<String, List<String>> goupedAuthors = new HashMap<String, List<String>>();
                if (libOk) {
-                       tt.addAll(reader.getLibrary().getSources());
+                       goupedAuthors = reader.getLibrary().getAuthorsGrouped();
                }
-               tt.add(0, null);
-
-               for (final String type : tt) {
-                       JMenuItem item = new JMenuItem(type == null ? "All" : type);
-                       item.addActionListener(new ActionListener() {
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       removeBookPanes();
-                                       addBookPane(type, true);
-                                       refreshBooks();
-                               }
-                       });
-                       sources.add(item);
-
-                       if (type == null) {
-                               sources.addSeparator();
-                       }
-               }
-
-               bar.add(sources);
-
                JMenu authors = new JMenu("Authors");
                authors.setMnemonic(KeyEvent.VK_A);
-
-               List<String> aa = new ArrayList<String>();
-               if (libOk) {
-                       aa.addAll(reader.getLibrary().getAuthors());
-               }
-               aa.add(0, null);
-               for (final String author : aa) {
-                       JMenuItem item = new JMenuItem(author == null ? "All"
-                                       : author.isEmpty() ? "[unknown]" : author);
-                       item.addActionListener(new ActionListener() {
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       removeBookPanes();
-                                       addBookPane(author, false);
-                                       refreshBooks();
-                               }
-                       });
-                       authors.add(item);
-
-                       if (author == null || author.isEmpty()) {
-                               authors.addSeparator();
-                       }
-               }
-
+               populateMenuSA(authors, goupedAuthors, false);
                bar.add(authors);
 
                JMenu options = new JMenu("Options");
@@ -492,7 +219,96 @@ class GuiReaderFrame extends JFrame {
                options.add(createMenuItemUiConfig());
                bar.add(options);
 
-               return bar;
+               setJMenuBar(bar);
+       }
+
+       // "" = [unknown]
+       private void populateMenuSA(JMenu menu,
+                       Map<String, List<String>> groupedValues, boolean type) {
+
+               // "All" and "Listing" special items
+               JMenuItem item = new JMenuItem("All");
+               item.addActionListener(getActionOpenList(type, false));
+               menu.add(item);
+               item = new JMenuItem("Listing");
+               item.addActionListener(getActionOpenList(type, true));
+               menu.add(item);
+               menu.addSeparator();
+
+               for (final String value : groupedValues.keySet()) {
+                       List<String> list = groupedValues.get(value);
+                       if (type && list.size() == 1 && list.get(0).isEmpty()) {
+                               // leaf item source/type
+                               item = new JMenuItem(value.isEmpty() ? "[unknown]" : value);
+                               item.addActionListener(getActionOpen(value, type));
+                               menu.add(item);
+                       } else {
+                               JMenu dir;
+                               if (!type && groupedValues.size() == 1) {
+                                       // only one group of authors
+                                       dir = menu;
+                               } else {
+                                       dir = new JMenu(value.isEmpty() ? "[unknown]" : value);
+                               }
+
+                               for (String sub : list) {
+                                       // " " instead of "" for the visual height
+                                       String itemName = sub;
+                                       if (itemName.isEmpty()) {
+                                               itemName = type ? " " : "[unknown]";
+                                       }
+
+                                       String actualValue = value;
+                                       if (type) {
+                                               if (!sub.isEmpty()) {
+                                                       actualValue += "/" + sub;
+                                               }
+                                       } else {
+                                               actualValue = sub;
+                                       }
+
+                                       item = new JMenuItem(itemName);
+                                       item.addActionListener(getActionOpen(actualValue, type));
+                                       dir.add(item);
+                               }
+
+                               if (menu != dir) {
+                                       menu.add(dir);
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Return an {@link ActionListener} that will set the given source (type) as
+        * the selected/displayed one.
+        * 
+        * @param type
+        *            the type (source) to select, cannot be NULL
+        * 
+        * @return the {@link ActionListener}
+        */
+       private ActionListener getActionOpen(final String source, final boolean type) {
+               return new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               mainPanel.removeBookPanes();
+                               mainPanel.addBookPane(source, type);
+                               mainPanel.refreshBooks();
+                       }
+               };
+       }
+
+       private ActionListener getActionOpenList(final boolean type,
+                       final boolean listMode) {
+               return new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               mainPanel.removeBookPanes();
+                               mainPanel.addBookPane(type, listMode);
+                               mainPanel.refreshBooks();
+                       }
+               };
        }
 
        /**
@@ -585,6 +401,7 @@ class GuiReaderFrame extends JFrame {
                export.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
                                        fc.showDialog(GuiReaderFrame.this, "Save");
                                        if (fc.getSelectedFile() != null) {
@@ -593,13 +410,13 @@ class GuiReaderFrame extends JFrame {
                                                                .getAbsolutePath()
                                                                + type.getDefaultExtension(false);
                                                final Progress pg = new Progress();
-                                               outOfUi(pg, new Runnable() {
+                                               mainPanel.outOfUi(pg, false, new Runnable() {
                                                        @Override
                                                        public void run() {
                                                                try {
                                                                        reader.getLibrary().export(
-                                                                                       selectedBook.getMeta().getLuid(),
-                                                                                       type, path, pg);
+                                                                                       selectedBook.getInfo().getMeta()
+                                                                                                       .getLuid(), type, path, pg);
                                                                } catch (IOException e) {
                                                                        Instance.getTraceHandler().error(e);
                                                                }
@@ -646,14 +463,16 @@ class GuiReaderFrame extends JFrame {
                refresh.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
-                                       outOfUi(null, new Runnable() {
+                                       mainPanel.outOfUi(null, false, new Runnable() {
                                                @Override
                                                public void run() {
-                                                       reader.clearLocalReaderCache(selectedBook.getMeta()
-                                                                       .getLuid());
+                                                       reader.clearLocalReaderCache(selectedBook.getInfo()
+                                                                       .getMeta().getLuid());
                                                        selectedBook.setCached(false);
-                                                       GuiReaderBook.clearIcon(selectedBook.getMeta());
+                                                       GuiReaderCoverImager.clearIcon(selectedBook
+                                                                       .getInfo());
                                                        SwingUtilities.invokeLater(new Runnable() {
                                                                @Override
                                                                public void run() {
@@ -670,78 +489,183 @@ class GuiReaderFrame extends JFrame {
        }
 
        /**
-        * Create the delete menu item.
+        * Create the "move to" menu item.
         * 
         * @param libOk
         *            the library can be queried
         * 
         * @return the item
         */
-       private JMenuItem createMenuItemMove(boolean libOk) {
-               JMenu moveTo = new JMenu("Move to...");
-               moveTo.setMnemonic(KeyEvent.VK_M);
+       private JMenuItem createMenuItemMoveTo(boolean libOk) {
+               JMenu changeTo = new JMenu("Move to");
+               changeTo.setMnemonic(KeyEvent.VK_M);
 
-               List<String> types = new ArrayList<String>();
-               types.add(null);
+               Map<String, List<String>> groupedSources = new HashMap<String, List<String>>();
                if (libOk) {
-                       types.addAll(reader.getLibrary().getSources());
+                       groupedSources = reader.getLibrary().getSourcesGrouped();
                }
 
-               for (String type : types) {
-                       JMenuItem item = new JMenuItem(type == null ? "New type..." : type);
+               JMenuItem item = new JMenuItem("New type...");
+               item.addActionListener(createMoveAction(ChangeAction.SOURCE, null));
+               changeTo.add(item);
+               changeTo.addSeparator();
+
+               for (final String type : groupedSources.keySet()) {
+                       List<String> list = groupedSources.get(type);
+                       if (list.size() == 1 && list.get(0).isEmpty()) {
+                               item = new JMenuItem(type);
+                               item.addActionListener(createMoveAction(ChangeAction.SOURCE,
+                                               type));
+                               changeTo.add(item);
+                       } else {
+                               JMenu dir = new JMenu(type);
+                               for (String sub : list) {
+                                       // " " instead of "" for the visual height
+                                       String itemName = sub.isEmpty() ? " " : sub;
+                                       String actualType = type;
+                                       if (!sub.isEmpty()) {
+                                               actualType += "/" + sub;
+                                       }
 
-                       moveTo.add(item);
-                       if (type == null) {
-                               moveTo.addSeparator();
+                                       item = new JMenuItem(itemName);
+                                       item.addActionListener(createMoveAction(
+                                                       ChangeAction.SOURCE, actualType));
+                                       dir.add(item);
+                               }
+                               changeTo.add(dir);
                        }
+               }
 
-                       final String ftype = type;
-                       item.addActionListener(new ActionListener() {
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       if (selectedBook != null) {
-                                               String type = ftype;
-                                               if (type == null) {
-                                                       Object rep = JOptionPane.showInputDialog(
-                                                                       GuiReaderFrame.this, "Move to:",
-                                                                       "Moving story",
-                                                                       JOptionPane.QUESTION_MESSAGE, null, null,
-                                                                       selectedBook.getMeta().getSource());
-
-                                                       if (rep == null) {
-                                                               return;
-                                                       }
-
-                                                       type = rep.toString();
-                                               }
+               return changeTo;
+       }
 
-                                               final String ftype = type;
-                                               outOfUi(null, new Runnable() {
-                                                       @Override
-                                                       public void run() {
-                                                               reader.changeSource(selectedBook.getMeta()
-                                                                               .getLuid(), ftype);
+       /**
+        * Create the "set author" menu item.
+        * 
+        * @param libOk
+        *            the library can be queried
+        * 
+        * @return the item
+        */
+       private JMenuItem createMenuItemSetAuthor(boolean libOk) {
+               JMenu changeTo = new JMenu("Set author");
+               changeTo.setMnemonic(KeyEvent.VK_A);
 
-                                                               selectedBook = null;
+               // New author
+               JMenuItem newItem = new JMenuItem("New author...");
+               changeTo.add(newItem);
+               changeTo.addSeparator();
+               newItem.addActionListener(createMoveAction(ChangeAction.AUTHOR, null));
 
-                                                               SwingUtilities.invokeLater(new Runnable() {
-                                                                       @Override
-                                                                       public void run() {
-                                                                               setJMenuBar(createMenu(true));
-                                                                       }
-                                                               });
-                                                       }
-                                               });
+               // Existing authors
+               if (libOk) {
+                       Map<String, List<String>> groupedAuthors = reader.getLibrary()
+                                       .getAuthorsGrouped();
+
+                       if (groupedAuthors.size() > 1) {
+                               for (String key : groupedAuthors.keySet()) {
+                                       JMenu group = new JMenu(key);
+                                       for (String value : groupedAuthors.get(key)) {
+                                               JMenuItem item = new JMenuItem(
+                                                               value.isEmpty() ? "[unknown]" : value);
+                                               item.addActionListener(createMoveAction(
+                                                               ChangeAction.AUTHOR, value));
+                                               group.add(item);
                                        }
+                                       changeTo.add(group);
                                }
-                       });
+                       } else if (groupedAuthors.size() == 1) {
+                               for (String value : groupedAuthors.values().iterator().next()) {
+                                       JMenuItem item = new JMenuItem(
+                                                       value.isEmpty() ? "[unknown]" : value);
+                                       item.addActionListener(createMoveAction(
+                                                       ChangeAction.AUTHOR, value));
+                                       changeTo.add(item);
+                               }
+                       }
                }
 
-               return moveTo;
+               return changeTo;
        }
 
        /**
-        * Create the redownload (then delete original) menu item.
+        * Create the "rename" menu item.
+        * 
+        * @param libOk
+        *            the library can be queried
+        * 
+        * @return the item
+        */
+       private JMenuItem createMenuItemRename(
+                       @SuppressWarnings("unused") boolean libOk) {
+               JMenuItem changeTo = new JMenuItem("Rename...");
+               changeTo.setMnemonic(KeyEvent.VK_R);
+               changeTo.addActionListener(createMoveAction(ChangeAction.TITLE, null));
+               return changeTo;
+       }
+
+       private ActionListener createMoveAction(final ChangeAction what,
+                       final String type) {
+               return new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
+                               if (selectedBook != null) {
+                                       String changeTo = type;
+                                       if (type == null) {
+                                               MetaData meta = selectedBook.getInfo().getMeta();
+                                               String init = "";
+                                               if (what == ChangeAction.SOURCE) {
+                                                       init = meta.getSource();
+                                               } else if (what == ChangeAction.TITLE) {
+                                                       init = meta.getTitle();
+                                               } else if (what == ChangeAction.AUTHOR) {
+                                                       init = meta.getAuthor();
+                                               }
+
+                                               Object rep = JOptionPane.showInputDialog(
+                                                               GuiReaderFrame.this, "Move to:",
+                                                               "Moving story", JOptionPane.QUESTION_MESSAGE,
+                                                               null, null, init);
+
+                                               if (rep == null) {
+                                                       return;
+                                               }
+
+                                               changeTo = rep.toString();
+                                       }
+
+                                       final String fChangeTo = changeTo;
+                                       mainPanel.outOfUi(null, true, new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       String luid = selectedBook.getInfo().getMeta()
+                                                                       .getLuid();
+                                                       if (what == ChangeAction.SOURCE) {
+                                                               reader.changeSource(luid, fChangeTo);
+                                                       } else if (what == ChangeAction.TITLE) {
+                                                               reader.changeTitle(luid, fChangeTo);
+                                                       } else if (what == ChangeAction.AUTHOR) {
+                                                               reader.changeAuthor(luid, fChangeTo);
+                                                       }
+
+                                                       mainPanel.unsetSelectedBook();
+
+                                                       SwingUtilities.invokeLater(new Runnable() {
+                                                               @Override
+                                                               public void run() {
+                                                                       createMenu(true);
+                                                               }
+                                                       });
+                                               }
+                                       });
+                               }
+                       }
+               };
+       }
+
+       /**
+        * Create the re-download (then delete original) menu item.
         * 
         * @return the item
         */
@@ -750,13 +674,14 @@ class GuiReaderFrame extends JFrame {
                refresh.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
-                                       final MetaData meta = selectedBook.getMeta();
-                                       imprt(meta.getUrl(), new StoryRunnable() {
+                                       final MetaData meta = selectedBook.getInfo().getMeta();
+                                       mainPanel.imprt(meta.getUrl(), new StoryRunnable() {
                                                @Override
                                                public void run(Story story) {
                                                        reader.delete(meta.getLuid());
-                                                       GuiReaderFrame.this.selectedBook = null;
+                                                       mainPanel.unsetSelectedBook();
                                                        MetaData newMeta = story.getMeta();
                                                        if (!newMeta.getSource().equals(meta.getSource())) {
                                                                reader.changeSource(newMeta.getLuid(),
@@ -781,12 +706,14 @@ class GuiReaderFrame extends JFrame {
                delete.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
-                                       outOfUi(null, new Runnable() {
+                                       mainPanel.outOfUi(null, true, new Runnable() {
                                                @Override
                                                public void run() {
-                                                       reader.delete(selectedBook.getMeta().getLuid());
-                                                       selectedBook = null;
+                                                       reader.delete(selectedBook.getInfo().getMeta()
+                                                                       .getLuid());
+                                                       mainPanel.unsetSelectedBook();
                                                }
                                        });
                                }
@@ -797,48 +724,52 @@ class GuiReaderFrame extends JFrame {
        }
 
        /**
-        * Create the open menu item for a book or a source (no LUID).
+        * Create the properties menu item.
         * 
         * @return the item
         */
-       private JMenuItem createMenuItemOpenBook() {
-               JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
-               open.addActionListener(new ActionListener() {
+       private JMenuItem createMenuItemProperties() {
+               JMenuItem delete = new JMenuItem("Properties", KeyEvent.VK_P);
+               delete.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
-                                       if (selectedBook.getMeta().getLuid() == null) {
-                                               removeBookPanes();
-                                               addBookPane(selectedBook.getMeta().getSource(), true);
-                                               refreshBooks();
-                                       } else {
-                                               openBook(selectedBook);
-                                       }
+                                       mainPanel.outOfUi(null, false, new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       new GuiReaderPropertiesFrame(reader.getLibrary(),
+                                                                       selectedBook.getInfo().getMeta())
+                                                                       .setVisible(true);
+                                               }
+                                       });
                                }
                        }
                });
 
-               return open;
+               return delete;
        }
 
        /**
-        * Create the SetCover menu item for a book to change the linked source
-        * cover.
+        * Create the open menu item for a book, a source/type or an author.
         * 
         * @return the item
         */
-       private JMenuItem createMenuItemSetCover() {
-               JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
+       public JMenuItem createMenuItemOpenBook() {
+               JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
                open.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
                                if (selectedBook != null) {
-                                       reader.getLibrary().setSourceCover(
-                                                       selectedBook.getMeta().getSource(),
-                                                       selectedBook.getMeta().getLuid());
-                                       MetaData source = selectedBook.getMeta().clone();
-                                       source.setLuid(null);
-                                       GuiReaderBook.clearIcon(source);
+                                       if (selectedBook.getInfo().getMeta() == null) {
+                                               mainPanel.removeBookPanes();
+                                               mainPanel.addBookPane(selectedBook.getInfo()
+                                                               .getMainInfo(), mainPanel.getCurrentType());
+                                               mainPanel.refreshBooks();
+                                       } else {
+                                               mainPanel.openBook(selectedBook);
+                                       }
                                }
                        }
                });
@@ -847,194 +778,63 @@ class GuiReaderFrame extends JFrame {
        }
 
        /**
-        * Open a {@link GuiReaderBook} item.
+        * Create the SetCover menu item for a book to change the linked source
+        * cover.
         * 
-        * @param book
-        *            the {@link GuiReaderBook} to open
+        * @return the item
         */
-       private void openBook(final GuiReaderBook book) {
-               final Progress pg = new Progress();
-               outOfUi(pg, new Runnable() {
+       private JMenuItem createMenuItemSetCoverForSource() {
+               JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
+               open.addActionListener(new ActionListener() {
                        @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);
+                       public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
+                               if (selectedBook != null) {
+                                       BasicLibrary lib = reader.getLibrary();
+                                       String luid = selectedBook.getInfo().getMeta().getLuid();
+                                       String source = selectedBook.getInfo().getMeta()
+                                                       .getSource();
 
-               invalidate();
-               pgBar.setProgress(pg);
-               validate();
-               setEnabled(false);
+                                       lib.setSourceCover(source, luid);
 
-               new Thread(new Runnable() {
-                       @Override
-                       public void run() {
-                               try {
-                                       run.run();
-                                       refreshBooks();
-                               } finally {
-                                       reload.done();
-                                       if (!pg.isDone()) {
-                                               // will trigger pgBar ActionListener:
-                                               pg.done();
-                                       }
+                                       GuiReaderBookInfo sourceInfo = GuiReaderBookInfo
+                                                       .fromSource(lib, source);
+                                       GuiReaderCoverImager.clearIcon(sourceInfo);
                                }
                        }
-               }, "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);
-               }
+               return open;
        }
 
        /**
-        * Actually import the {@link Story} into the main {@link LocalLibrary}.
-        * <p>
-        * Should be called inside the UI thread.
+        * Create the SetCover menu item for a book to change the linked source
+        * cover.
         * 
-        * @param url
-        *            the {@link Story} to import by {@link URL}
-        * @param onSuccess
-        *            Action to execute on success
+        * @return the item
         */
-       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() {
+       private JMenuItem createMenuItemSetCoverForAuthor() {
+               JMenuItem open = new JMenuItem("Set as cover for author", KeyEvent.VK_A);
+               open.addActionListener(new ActionListener() {
                        @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;
+                       public void actionPerformed(ActionEvent e) {
+                               final GuiReaderBook selectedBook = mainPanel.getSelectedBook();
+                               if (selectedBook != null) {
+                                       BasicLibrary lib = reader.getLibrary();
+                                       String luid = selectedBook.getInfo().getMeta().getLuid();
+                                       String author = selectedBook.getInfo().getMeta()
+                                                       .getAuthor();
 
-                               final boolean ok = (e == null);
+                                       lib.setAuthorCover(author, luid);
 
-                               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);
-                                       }
+                                       GuiReaderBookInfo authorInfo = GuiReaderBookInfo
+                                                       .fromAuthor(lib, author);
+                                       GuiReaderCoverImager.clearIcon(authorInfo);
                                }
-                               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();
+               return open;
        }
 
        /**
@@ -1047,7 +847,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);
@@ -1061,4 +861,9 @@ class GuiReaderFrame extends JFrame {
                        }
                });
        }
+
+       @Override
+       public GuiReader getReader() {
+               return reader;
+       }
 }