package be.nikiroo.fanfix.reader.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; 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.FocusAdapter; import java.awt.event.FocusEvent; 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.LinkedList; 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.StringIdGui; 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.fanfix.reader.ui.GuiReaderBookInfo.Type; 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 status * the library status, must not be NULL */ public void createMenu(Status status); /** * 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 MetaData} parameter. * * @author niki */ public interface MetaDataRunnable { /** * Run the action. * * @param meta * the meta of the story */ public void run(MetaData meta); } /** * 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)); JScrollPane scroll = new JScrollPane(pane); Integer icolor = Instance.getInstance().getUiConfig().getColor(UiConfig.BACKGROUND_COLOR); if (icolor != null) { color = new Color(icolor); setBackground(color); pane.setBackground(color); scroll.setBackground(color); } 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(); addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { focus(); } }); 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.READ_WRITE) { lib.refresh(pg); } inUi(new Runnable() { @Override public void run() { if (status.isReady()) { helper.createMenu(status); pane.setVisible(true); if (typeF == null) { try { addBookPane(true, false); } catch (IOException e) { error(e.getLocalizedMessage(), "IOException", e); } } else { addBookPane(typeF, true); } } else { helper.createMenu(status); validate(); String desc = Instance.getInstance().getTransGui().getStringX(StringIdGui.ERROR_LIB_STATUS, status.toString()); if (desc == null) { desc = GuiReader .trans(StringIdGui.ERROR_LIB_STATUS); } String err = lib.getLibraryName() + "\n" + desc; error(err, GuiReader .trans(StringIdGui.TITLE_ERROR_LIBRARY), 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 * * @throws IOException * in case of I/O error */ public void addBookPane(boolean type, boolean listMode) throws IOException { this.currentType = type; BasicLibrary lib = helper.getReader().getLibrary(); if (type) { if (!listMode) { addListPane(GuiReader.trans(StringIdGui.MENU_SOURCES), lib.getSources(), type); } else { for (String tt : lib.getSources()) { if (tt != null) { addBookPane(tt, type); } } } } else { if (!listMode) { addListPane(GuiReader.trans(StringIdGui.MENU_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, Component target, int x, int y) { JPopupMenu popup = helper.createBookPopup(); popup.show(target, x, y); } @Override public void action(final GuiReaderBook book) { openBook(book); } }); focus(); } /** * 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; try { if (currentType) { metas = lib.getList().filter(value, null, null); } else { metas = lib.getList().filter(null, value, null); } } catch (IOException e) { error(e.getLocalizedMessage(), "IOException", e); metas = new ArrayList(); } 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.getInstance().getTraceHandler().error(e); error(GuiReader.trans(StringIdGui.ERROR_CANNOT_OPEN), GuiReader.trans(StringIdGui.TITLE_ERROR), e); } } }); } /** * Prefetch a {@link GuiReaderBook} item (which can be a group, in which * case we prefetch all its members). * * @param book * the {@link GuiReaderBook} to open */ public void prefetchBook(final GuiReaderBook book) { final List luids = new LinkedList(); try { switch (book.getInfo().getType()) { case STORY: luids.add(book.getInfo().getMeta().getLuid()); break; case SOURCE: for (MetaData meta : helper.getReader().getLibrary() .getList().filter(book.getInfo().getMainInfo(), null, null)) { luids.add(meta.getLuid()); } break; case AUTHOR: for (MetaData meta : helper.getReader().getLibrary() .getList().filter(null, book.getInfo().getMainInfo(), null)) { luids.add(meta.getLuid()); } break; } } catch (IOException e) { Instance.getInstance().getTraceHandler().error(e); } final Progress pg = new Progress(); pg.setMax(luids.size()); outOfUi(pg, false, new Runnable() { @Override public void run() { try { for (String luid : luids) { Progress pgStep = new Progress(); pg.addProgress(pgStep, 1); helper.getReader().prefetch(luid, pgStep); } // TODO: also set the green button on sources/authors? // requires to do the same when all stories inside are green if (book.getInfo().getType() == Type.STORY) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { book.setCached(true); } }); } } catch (IOException e) { Instance.getInstance().getTraceHandler().error(e); error(GuiReader.trans(StringIdGui.ERROR_CANNOT_OPEN), GuiReader.trans(StringIdGui.TITLE_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( GuiReader.trans(StringIdGui.PROGRESS_OUT_OF_UI_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.getInstance().getTraceHandler().error(e); } catch (InvocationTargetException e) { Instance.getInstance().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.startsWith("https://"))) { clipboard = ""; } url = JOptionPane.showInputDialog(GuiReaderMainPanel.this, GuiReader.trans(StringIdGui.SUBTITLE_IMPORT_URL), GuiReader.trans(StringIdGui.TITLE_IMPORT_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 MetaDataRunnable 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; MetaData meta = null; try { meta = 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(GuiReader.trans( StringIdGui.ERROR_URL_NOT_SUPPORTED, url), GuiReader.trans(StringIdGui.TITLE_ERROR), null); } else { error(GuiReader.trans( StringIdGui.ERROR_URL_IMPORT_FAILED, url, e.getMessage()), GuiReader .trans(StringIdGui.TITLE_ERROR), e); } } else { if (onSuccess != null) { onSuccess.run(meta); } } 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, Component target, int x, int y) { JPopupMenu popup = helper.createSourceAuthorPopup(); popup.show(target, x, y); } @Override public void action(final GuiReaderBook book) { removeBookPanes(); addBookPane(book.getInfo().getMainInfo(), type); refreshBooks(); } }); focus(); } /** * Focus the first {@link GuiReaderGroup} we find. */ private void focus() { GuiReaderGroup group = null; Map books = this.books; if (books.size() > 0) { group = books.values().iterator().next(); } if (group == null) { group = bookPane; } if (group != null) { group.requestFocusInWindow(); } } /** * 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.getInstance().getTraceHandler().error(title + ": " + message); if (e != null) { Instance.getInstance().getTraceHandler().error(e); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message, title, JOptionPane.ERROR_MESSAGE); } }); } }