package be.nikiroo.fanfix.reader.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.EventQueue; 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.lang.reflect.InvocationTargetException; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; 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 books; private GuiReaderGroup bookPane; // for more "All" 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 boolean currentType; // type/source or author mode (All and Listing) /** * 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. *

* Will invalidate the layout. * * @param libOk * the library can be queried */ public void createMenu(boolean libOk); /** * 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 or an author. * * @return the popup menu to display */ public JPopupMenu createSourceAuthorPopup(); } /** * 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 parent * the associated {@link FrameHelper} 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) { pgBar.invalidate(); pgBar.setProgress(null); setEnabled(true); validate(); } }); pgBar.addUpdateListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { pgBar.invalidate(); validate(); repaint(); } }); books = new TreeMap(); pane.setVisible(false); final Progress pg = new Progress(); final String typeF = type; outOfUi(pg, true, new Runnable() { @Override public void run() { final BasicLibrary lib = helper.getReader().getLibrary(); final Status status = lib.getStatus(); if (status == Status.READY) { lib.refresh(pg); } inUi(new Runnable() { @Override public void run() { if (status == Status.READY) { helper.createMenu(true); if (typeF == null) { addBookPane(true, false); } else { addBookPane(typeF, true); } pane.setVisible(true); } else { 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); } } }); } }); } public boolean getCurrentType() { return currentType; } /** * 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. *

* A display of all the sources/types or all the authors will show one icon * per source/type or author. *

* 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) { this.currentType = type; 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. *

* Will invalidate the layout. * * @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) { this.currentType = type; GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), value, color); books.put(value, bookPane); pane.invalidate(); pane.add(bookPane); 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. *

* Will invalidate the layout. */ public void removeBookPanes() { books.clear(); pane.invalidate(); pane.removeAll(); } /** * Refresh the list of {@link GuiReaderBook}s from disk. *

* Will validate the layout, as it is a "refresh" operation. */ public void refreshBooks() { BasicLibrary lib = helper.getReader().getLibrary(); for (String value : books.keySet()) { List infos = new ArrayList(); List metas; if (currentType) { metas = lib.getListBySource(value); } else { metas = lib.getListByAuthor(value); } for (MetaData meta : metas) { infos.add(GuiReaderBookInfo.fromMeta(meta)); } books.get(value).refreshBooks(infos, words); } if (bookPane != null) { bookPane.refreshBooks(words); } this.validate(); } /** * 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, false, new Runnable() { @Override public void run() { try { helper.getReader().read(book.getInfo().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. *

* 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 refreshBooks * TRUE to refresh the books after * @param run * the action to run */ public void outOfUi(Progress progress, final boolean refreshBooks, final Runnable run) { final Progress pg = new Progress(); final Progress reload = new Progress("Reload books"); if (progress == null) { progress = new Progress(); } if (refreshBooks) { pg.addProgress(progress, 100); } else { 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(); if (refreshBooks) { refreshBooks(); } } finally { reload.done(); if (!pg.isDone()) { // will trigger pgBar ActionListener: pg.done(); } } } }, "outOfUi thread").start(); } /** * Process the given action in the main Swing UI thread. *

* The code will make sure the current thread is the main UI thread and, if * not, will switch to it before executing the runnable. *

* Synchronous operation. * * @param run * the action to run */ public void inUi(final Runnable run) { if (EventQueue.isDispatchThread()) { run.run(); } else { try { EventQueue.invokeAndWait(run); } catch (InterruptedException e) { Instance.getTraceHandler().error(e); } catch (InvocationTargetException e) { Instance.getTraceHandler().error(e); } } } /** * Import a {@link Story} into the main {@link LocalLibrary}. *

* 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}. *

* Should be called inside the UI thread. * * @param url * the {@link Story} to import by {@link URL} * @param onSuccess * Action to execute on success * @param onSuccessPgName * the name to use for the onSuccess progress bar */ 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, true, 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 b. An enabled component can respond to user input * and generate events. Components are enabled initially by default. *

* Enabling or disabling this component will also affect its * children. * * @param b * If true, this component is enabled; otherwise * this component is disabled */ @Override public void setEnabled(boolean b) { if (bar != null) { bar.setEnabled(b); } for (GuiReaderGroup group : books.values()) { 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 values, final boolean type) { GuiReader reader = helper.getReader(); BasicLibrary lib = reader.getLibrary(); bookPane = new GuiReaderGroup(reader, name, color); List infos = new ArrayList(); for (String value : values) { if (type) { infos.add(GuiReaderBookInfo.fromSource(lib, value)); } else { infos.add(GuiReaderBookInfo.fromAuthor(lib, value)); } } bookPane.refreshBooks(infos, words); 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.createSourceAuthorPopup(); popup.show(e.getComponent(), e.getX(), e.getY()); } @Override public void action(final GuiReaderBook book) { removeBookPanes(); addBookPane(book.getInfo().getMainInfo(), 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); } }); } }