1 package be
.nikiroo
.fanfix
.reader
;
3 import java
.awt
.BorderLayout
;
6 import java
.awt
.Toolkit
;
7 import java
.awt
.datatransfer
.DataFlavor
;
8 import java
.awt
.event
.ActionEvent
;
9 import java
.awt
.event
.ActionListener
;
10 import java
.awt
.event
.KeyEvent
;
11 import java
.awt
.event
.MouseEvent
;
12 import java
.awt
.event
.WindowEvent
;
14 import java
.io
.IOException
;
16 import java
.util
.ArrayList
;
17 import java
.util
.HashMap
;
18 import java
.util
.List
;
20 import java
.util
.Map
.Entry
;
22 import javax
.swing
.BoxLayout
;
23 import javax
.swing
.JFileChooser
;
24 import javax
.swing
.JFrame
;
25 import javax
.swing
.JLabel
;
26 import javax
.swing
.JMenu
;
27 import javax
.swing
.JMenuBar
;
28 import javax
.swing
.JMenuItem
;
29 import javax
.swing
.JOptionPane
;
30 import javax
.swing
.JPanel
;
31 import javax
.swing
.JPopupMenu
;
32 import javax
.swing
.JScrollPane
;
33 import javax
.swing
.SwingConstants
;
34 import javax
.swing
.SwingUtilities
;
35 import javax
.swing
.filechooser
.FileFilter
;
36 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
38 import be
.nikiroo
.fanfix
.Instance
;
39 import be
.nikiroo
.fanfix
.bundles
.Config
;
40 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
41 import be
.nikiroo
.fanfix
.data
.MetaData
;
42 import be
.nikiroo
.fanfix
.data
.Story
;
43 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
44 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
45 import be
.nikiroo
.fanfix
.reader
.GuiReaderBook
.BookActionListener
;
46 import be
.nikiroo
.utils
.Progress
;
47 import be
.nikiroo
.utils
.Version
;
48 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
49 import be
.nikiroo
.utils
.ui
.ProgressBar
;
52 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
53 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
54 * way to copy them to the {@link GuiReader} cache (
55 * {@link BasicReader#getLibrary()}), read them, delete them...
59 class GuiReaderFrame
extends JFrame
{
60 private static final long serialVersionUID
= 1L;
61 private GuiReader reader
;
62 private Map
<GuiReaderGroup
, String
> booksByType
;
63 private Map
<GuiReaderGroup
, String
> booksByAuthor
;
66 private ProgressBar pgBar
;
68 private GuiReaderBook selectedBook
;
69 private boolean words
; // words or authors (secondary info on books)
72 * A {@link Runnable} with a {@link Story} parameter.
76 private interface StoryRunnable
{
83 public void run(Story story
);
87 * Create a new {@link GuiReaderFrame}.
90 * the associated {@link GuiReader} to forward some commands and
91 * access its {@link LocalLibrary}
93 * the type of {@link Story} to load, or NULL for all types
95 public GuiReaderFrame(GuiReader reader
, String type
) {
96 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
100 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
102 setLayout(new BorderLayout());
105 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
107 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
109 setBackground(color
);
110 pane
.setBackground(color
);
113 JScrollPane scroll
= new JScrollPane(pane
);
114 scroll
.getVerticalScrollBar().setUnitIncrement(16);
115 add(scroll
, BorderLayout
.CENTER
);
117 String message
= reader
.getLibrary().getLibraryName();
118 if (!message
.isEmpty()) {
119 JLabel name
= new JLabel(message
, SwingConstants
.CENTER
);
120 add(name
, BorderLayout
.NORTH
);
123 pgBar
= new ProgressBar();
124 add(pgBar
, BorderLayout
.SOUTH
);
126 pgBar
.addActionListener(new ActionListener() {
128 public void actionPerformed(ActionEvent e
) {
130 pgBar
.setProgress(null);
136 pgBar
.addUpdateListener(new ActionListener() {
138 public void actionPerformed(ActionEvent e
) {
145 booksByType
= new HashMap
<GuiReaderGroup
, String
>();
146 booksByAuthor
= new HashMap
<GuiReaderGroup
, String
>();
148 pane
.setVisible(false);
149 final Progress pg
= new Progress();
150 final String typeF
= type
;
151 outOfUi(pg
, new Runnable() {
154 GuiReaderFrame
.this.reader
.getLibrary().refresh(false, pg
);
156 setJMenuBar(createMenu());
157 addBookPane(typeF
, true);
160 pane
.setVisible(true);
167 private void addSourcePanes() {
169 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, "Sources", color
);
171 List
<MetaData
> sources
= new ArrayList
<MetaData
>();
172 for (String source
: reader
.getLibrary().getSources()) {
173 MetaData mSource
= new MetaData();
174 mSource
.setLuid(null);
175 mSource
.setTitle(source
);
176 mSource
.setSource(source
);
177 sources
.add(mSource
);
180 bookPane
.refreshBooks(sources
, false);
188 bookPane
.setActionListener(new BookActionListener() {
190 public void select(GuiReaderBook book
) {
195 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
196 JPopupMenu popup
= new JPopupMenu();
197 popup
.add(createMenuItemOpenBook());
198 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
202 public void action(final GuiReaderBook book
) {
204 addBookPane(book
.getMeta().getSource(), true);
211 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
212 * selected type or author.
215 * the author or the type, or NULL to get all the
218 * TRUE for type, FALSE for author
220 private void addBookPane(String value
, boolean type
) {
223 if (Instance
.getUiConfig().getBoolean(UiConfig
.SOURCE_PAGE
,
227 for (String tt
: reader
.getLibrary().getSources()) {
229 addBookPane(tt
, type
);
234 for (String tt
: reader
.getLibrary().getAuthors()) {
236 addBookPane(tt
, type
);
244 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, value
, color
);
246 booksByType
.put(bookPane
, value
);
248 booksByAuthor
.put(bookPane
, value
);
257 bookPane
.setActionListener(new BookActionListener() {
259 public void select(GuiReaderBook book
) {
264 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
265 JPopupMenu popup
= new JPopupMenu();
266 popup
.add(createMenuItemOpenBook());
267 popup
.addSeparator();
268 popup
.add(createMenuItemExport());
269 popup
.add(createMenuItemMove());
270 popup
.add(createMenuItemSetCover());
271 popup
.add(createMenuItemClearCache());
272 popup
.add(createMenuItemRedownload());
273 popup
.addSeparator();
274 popup
.add(createMenuItemDelete());
275 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
279 public void action(final GuiReaderBook book
) {
285 private void removeBookPanes() {
287 booksByAuthor
.clear();
296 * Refresh the list of {@link GuiReaderBook}s from disk.
299 private void refreshBooks() {
300 for (GuiReaderGroup group
: booksByType
.keySet()) {
301 List
<MetaData
> stories
= reader
.getLibrary().getListBySource(
302 booksByType
.get(group
));
303 group
.refreshBooks(stories
, words
);
306 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
307 List
<MetaData
> stories
= reader
.getLibrary().getListByAuthor(
308 booksByAuthor
.get(group
));
309 group
.refreshBooks(stories
, words
);
317 * Create the main menu bar.
321 private JMenuBar
createMenu() {
322 bar
= new JMenuBar();
324 JMenu file
= new JMenu("File");
325 file
.setMnemonic(KeyEvent
.VK_F
);
327 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
328 imprt
.addActionListener(new ActionListener() {
330 public void actionPerformed(ActionEvent e
) {
334 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
335 imprtF
.addActionListener(new ActionListener() {
337 public void actionPerformed(ActionEvent e
) {
341 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
342 exit
.addActionListener(new ActionListener() {
344 public void actionPerformed(ActionEvent e
) {
345 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
346 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
350 file
.add(createMenuItemOpenBook());
351 file
.add(createMenuItemExport());
352 file
.add(createMenuItemMove());
361 JMenu edit
= new JMenu("Edit");
362 edit
.setMnemonic(KeyEvent
.VK_E
);
364 edit
.add(createMenuItemClearCache());
365 edit
.add(createMenuItemRedownload());
367 edit
.add(createMenuItemDelete());
371 JMenu view
= new JMenu("View");
372 view
.setMnemonic(KeyEvent
.VK_V
);
373 JMenuItem vauthors
= new JMenuItem("Author");
374 vauthors
.setMnemonic(KeyEvent
.VK_A
);
375 vauthors
.addActionListener(new ActionListener() {
377 public void actionPerformed(ActionEvent e
) {
383 JMenuItem vwords
= new JMenuItem("Word count");
384 vwords
.setMnemonic(KeyEvent
.VK_W
);
385 vwords
.addActionListener(new ActionListener() {
387 public void actionPerformed(ActionEvent e
) {
395 JMenu sources
= new JMenu("Sources");
396 sources
.setMnemonic(KeyEvent
.VK_S
);
398 List
<String
> tt
= reader
.getLibrary().getSources();
400 for (final String type
: tt
) {
401 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
402 item
.addActionListener(new ActionListener() {
404 public void actionPerformed(ActionEvent e
) {
406 addBookPane(type
, true);
413 sources
.addSeparator();
419 JMenu authors
= new JMenu("Authors");
420 authors
.setMnemonic(KeyEvent
.VK_A
);
422 List
<String
> aa
= reader
.getLibrary().getAuthors();
424 for (final String author
: aa
) {
425 JMenuItem item
= new JMenuItem(author
== null ?
"All"
426 : author
.isEmpty() ?
"[unknown]" : author
);
427 item
.addActionListener(new ActionListener() {
429 public void actionPerformed(ActionEvent e
) {
431 addBookPane(author
, false);
437 if (author
== null || author
.isEmpty()) {
438 authors
.addSeparator();
444 JMenu options
= new JMenu("Options");
445 options
.setMnemonic(KeyEvent
.VK_O
);
446 options
.add(createMenuItemConfig());
447 options
.add(createMenuItemUiConfig());
454 * Create the Fanfix Configuration menu item.
458 private JMenuItem
createMenuItemConfig() {
459 final String title
= "Fanfix Configuration";
460 JMenuItem item
= new JMenuItem(title
);
461 item
.setMnemonic(KeyEvent
.VK_F
);
463 item
.addActionListener(new ActionListener() {
465 public void actionPerformed(ActionEvent e
) {
466 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
467 Config
.class, Instance
.getConfig(),
468 "This is where you configure the options of the program.");
469 JFrame frame
= new JFrame(title
);
471 frame
.setSize(800, 600);
472 frame
.setVisible(true);
480 * Create the UI Configuration menu item.
484 private JMenuItem
createMenuItemUiConfig() {
485 final String title
= "UI Configuration";
486 JMenuItem item
= new JMenuItem(title
);
487 item
.setMnemonic(KeyEvent
.VK_U
);
489 item
.addActionListener(new ActionListener() {
491 public void actionPerformed(ActionEvent e
) {
492 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
493 UiConfig
.class, Instance
.getUiConfig(),
494 "This is where you configure the graphical appearence of the program.");
495 JFrame frame
= new JFrame(title
);
497 frame
.setSize(800, 600);
498 frame
.setVisible(true);
506 * Create the export menu item.
510 private JMenuItem
createMenuItemExport() {
511 final JFileChooser fc
= new JFileChooser();
512 fc
.setAcceptAllFileFilterUsed(false);
514 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
515 for (OutputType type
: OutputType
.values()) {
516 String ext
= type
.getDefaultExtension(false);
517 String desc
= type
.getDesc(false);
519 if (ext
== null || ext
.isEmpty()) {
520 filters
.put(createAllFilter(desc
), type
);
522 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
526 // First the "ALL" filters, then, the extension filters
527 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
528 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
529 fc
.addChoosableFileFilter(entry
.getKey());
532 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
533 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
534 fc
.addChoosableFileFilter(entry
.getKey());
539 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
540 export
.addActionListener(new ActionListener() {
542 public void actionPerformed(ActionEvent e
) {
543 if (selectedBook
!= null) {
544 fc
.showDialog(GuiReaderFrame
.this, "Save");
545 if (fc
.getSelectedFile() != null) {
546 final OutputType type
= filters
.get(fc
.getFileFilter());
547 final String path
= fc
.getSelectedFile()
549 + type
.getDefaultExtension(false);
550 final Progress pg
= new Progress();
551 outOfUi(pg
, new Runnable() {
555 reader
.getLibrary().export(
556 selectedBook
.getMeta().getLuid(),
558 } catch (IOException e
) {
572 * Create a {@link FileFilter} that accepts all files and return the given
580 private FileFilter
createAllFilter(final String desc
) {
581 return new FileFilter() {
583 public String
getDescription() {
588 public boolean accept(File f
) {
595 * Create the refresh (delete cache) menu item.
599 private JMenuItem
createMenuItemClearCache() {
600 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
601 refresh
.addActionListener(new ActionListener() {
603 public void actionPerformed(ActionEvent e
) {
604 if (selectedBook
!= null) {
605 outOfUi(null, new Runnable() {
608 reader
.clearLocalReaderCache(selectedBook
.getMeta()
610 selectedBook
.setCached(false);
611 GuiReaderBook
.clearIcon(selectedBook
.getMeta());
612 SwingUtilities
.invokeLater(new Runnable() {
615 selectedBook
.repaint();
628 * Create the delete menu item.
632 private JMenuItem
createMenuItemMove() {
633 JMenu moveTo
= new JMenu("Move to...");
634 moveTo
.setMnemonic(KeyEvent
.VK_M
);
636 List
<String
> types
= new ArrayList
<String
>();
638 types
.addAll(reader
.getLibrary().getSources());
640 for (String type
: types
) {
641 JMenuItem item
= new JMenuItem(type
== null ?
"New type..." : type
);
645 moveTo
.addSeparator();
648 final String ftype
= type
;
649 item
.addActionListener(new ActionListener() {
651 public void actionPerformed(ActionEvent e
) {
652 if (selectedBook
!= null) {
655 Object rep
= JOptionPane
.showInputDialog(
656 GuiReaderFrame
.this, "Move to:",
658 JOptionPane
.QUESTION_MESSAGE
, null, null,
659 selectedBook
.getMeta().getSource());
665 type
= rep
.toString();
668 final String ftype
= type
;
669 outOfUi(null, new Runnable() {
672 reader
.changeType(selectedBook
.getMeta()
677 SwingUtilities
.invokeLater(new Runnable() {
680 setJMenuBar(createMenu());
694 * Create the redownload (then delete original) menu item.
698 private JMenuItem
createMenuItemRedownload() {
699 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
700 refresh
.addActionListener(new ActionListener() {
702 public void actionPerformed(ActionEvent e
) {
703 if (selectedBook
!= null) {
704 final MetaData meta
= selectedBook
.getMeta();
705 imprt(meta
.getUrl(), new StoryRunnable() {
707 public void run(Story story
) {
708 reader
.delete(meta
.getLuid());
709 GuiReaderFrame
.this.selectedBook
= null;
710 MetaData newMeta
= story
.getMeta();
711 if (!newMeta
.getSource().equals(meta
.getSource())) {
712 reader
.changeType(newMeta
.getLuid(),
716 }, "Removing old copy");
725 * Create the delete menu item.
729 private JMenuItem
createMenuItemDelete() {
730 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
731 delete
.addActionListener(new ActionListener() {
733 public void actionPerformed(ActionEvent e
) {
734 if (selectedBook
!= null) {
735 outOfUi(null, new Runnable() {
738 reader
.delete(selectedBook
.getMeta().getLuid());
750 * Create the open menu item for a book or a source (no LUID).
754 private JMenuItem
createMenuItemOpenBook() {
755 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
756 open
.addActionListener(new ActionListener() {
758 public void actionPerformed(ActionEvent e
) {
759 if (selectedBook
!= null) {
760 if (selectedBook
.getMeta().getLuid() == null) {
762 addBookPane(selectedBook
.getMeta().getSource(), true);
765 openBook(selectedBook
);
775 * Create the SetCover menu item for a book to change the linked source
780 private JMenuItem
createMenuItemSetCover() {
781 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
782 open
.addActionListener(new ActionListener() {
784 public void actionPerformed(ActionEvent e
) {
785 if (selectedBook
!= null) {
786 reader
.getLibrary().setSourceCover(
787 selectedBook
.getMeta().getSource(),
788 selectedBook
.getMeta().getLuid());
789 MetaData source
= selectedBook
.getMeta().clone();
790 source
.setLuid(null);
791 GuiReaderBook
.clearIcon(source
);
800 * Open a {@link GuiReaderBook} item.
803 * the {@link GuiReaderBook} to open
805 private void openBook(final GuiReaderBook book
) {
806 final Progress pg
= new Progress();
807 outOfUi(pg
, new Runnable() {
811 reader
.read(book
.getMeta().getLuid(), pg
);
812 SwingUtilities
.invokeLater(new Runnable() {
815 book
.setCached(true);
818 } catch (IOException e
) {
819 // TODO: error message?
827 * Process the given action out of the Swing UI thread and link the given
828 * {@link ProgressBar} to the action.
830 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
831 * to done when the action is done.
834 * the {@link ProgressBar} or NULL
838 private void outOfUi(Progress progress
, final Runnable run
) {
839 final Progress pg
= new Progress();
840 final Progress reload
= new Progress("Reload books");
841 if (progress
== null) {
842 progress
= new Progress();
845 pg
.addProgress(progress
, 90);
846 pg
.addProgress(reload
, 10);
849 pgBar
.setProgress(pg
);
853 new Thread(new Runnable() {
860 // will trigger pgBar ActionListener:
864 }, "outOfUi thread").start();
868 * Import a {@link Story} into the main {@link LocalLibrary}.
870 * Should be called inside the UI thread.
873 * TRUE for an {@link URL}, false for a {@link File}
875 private void imprt(boolean askUrl
) {
876 JFileChooser fc
= new JFileChooser();
880 String clipboard
= "";
882 clipboard
= ("" + Toolkit
.getDefaultToolkit()
883 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
885 } catch (Exception e
) {
886 // No data will be handled
889 if (clipboard
== null || !clipboard
.startsWith("http")) {
893 url
= JOptionPane
.showInputDialog(GuiReaderFrame
.this,
894 "url of the story to import?", "Importing from URL",
895 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
896 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
897 url
= fc
.getSelectedFile().getAbsolutePath();
902 if (url
!= null && !url
.toString().isEmpty()) {
903 imprt(url
.toString(), null, null);
908 * Actually import the {@link Story} into the main {@link LocalLibrary}.
910 * Should be called inside the UI thread.
913 * the {@link Story} to import by {@link URL}
915 * Action to execute on success
917 private void imprt(final String url
, final StoryRunnable onSuccess
,
918 String onSuccessPgName
) {
919 final Progress pg
= new Progress();
920 final Progress pgImprt
= new Progress();
921 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
922 pg
.addProgress(pgImprt
, 95);
923 pg
.addProgress(pgOnSuccess
, 5);
925 outOfUi(pg
, new Runnable() {
931 story
= reader
.getLibrary().imprt(BasicReader
.getUrl(url
),
933 } catch (IOException e
) {
937 final Exception e
= ex
;
939 final boolean ok
= (e
== null);
941 pgOnSuccess
.setProgress(0);
944 SwingUtilities
.invokeLater(new Runnable() {
947 JOptionPane
.showMessageDialog(GuiReaderFrame
.this,
948 "Cannot import: " + url
, e
.getMessage(),
949 JOptionPane
.ERROR_MESSAGE
);
953 if (onSuccess
!= null) {
954 onSuccess
.run(story
);
963 * Enables or disables this component, depending on the value of the
964 * parameter <code>b</code>. An enabled component can respond to user input
965 * and generate events. Components are enabled initially by default.
967 * Disabling this component will also affect its children.
970 * If <code>true</code>, this component is enabled; otherwise
971 * this component is disabled
974 public void setEnabled(boolean b
) {
979 for (GuiReaderGroup group
: booksByType
.keySet()) {
982 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {