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
.JMenu
;
26 import javax
.swing
.JMenuBar
;
27 import javax
.swing
.JMenuItem
;
28 import javax
.swing
.JOptionPane
;
29 import javax
.swing
.JPanel
;
30 import javax
.swing
.JPopupMenu
;
31 import javax
.swing
.JScrollPane
;
32 import javax
.swing
.SwingUtilities
;
33 import javax
.swing
.filechooser
.FileFilter
;
34 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
36 import be
.nikiroo
.fanfix
.Instance
;
37 import be
.nikiroo
.fanfix
.bundles
.Config
;
38 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
39 import be
.nikiroo
.fanfix
.data
.MetaData
;
40 import be
.nikiroo
.fanfix
.data
.Story
;
41 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
42 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
43 import be
.nikiroo
.fanfix
.reader
.GuiReaderBook
.BookActionListener
;
44 import be
.nikiroo
.utils
.Progress
;
45 import be
.nikiroo
.utils
.Version
;
46 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
47 import be
.nikiroo
.utils
.ui
.ProgressBar
;
50 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
51 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
52 * way to copy them to the {@link GuiReader} cache (
53 * {@link BasicReader#getLibrary()}), read them, delete them...
57 class GuiReaderFrame
extends JFrame
{
58 private static final long serialVersionUID
= 1L;
59 private GuiReader reader
;
60 private Map
<GuiReaderGroup
, String
> booksByType
;
61 private Map
<GuiReaderGroup
, String
> booksByAuthor
;
64 private ProgressBar pgBar
;
66 private GuiReaderBook selectedBook
;
67 private boolean words
; // words or authors (secondary info on books)
70 * A {@link Runnable} with a {@link Story} parameter.
74 private interface StoryRunnable
{
81 public void run(Story story
);
85 * Create a new {@link GuiReaderFrame}.
88 * the associated {@link GuiReader} to forward some commands and
89 * access its {@link LocalLibrary}
91 * the type of {@link Story} to load, or NULL for all types
93 public GuiReaderFrame(GuiReader reader
, String type
) {
94 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
98 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
100 setLayout(new BorderLayout());
103 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
105 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
107 setBackground(color
);
108 pane
.setBackground(color
);
111 JScrollPane scroll
= new JScrollPane(pane
);
112 scroll
.getVerticalScrollBar().setUnitIncrement(16);
113 add(scroll
, BorderLayout
.CENTER
);
115 pgBar
= new ProgressBar();
116 add(pgBar
, BorderLayout
.SOUTH
);
118 pgBar
.addActionListener(new ActionListener() {
120 public void actionPerformed(ActionEvent e
) {
122 pgBar
.setProgress(null);
128 pgBar
.addUpdateListener(new ActionListener() {
130 public void actionPerformed(ActionEvent e
) {
137 booksByType
= new HashMap
<GuiReaderGroup
, String
>();
138 booksByAuthor
= new HashMap
<GuiReaderGroup
, String
>();
140 pane
.setVisible(false);
141 final Progress pg
= new Progress();
142 final String typeF
= type
;
143 outOfUi(pg
, new Runnable() {
146 GuiReaderFrame
.this.reader
.getLibrary().refresh(false, pg
);
148 setJMenuBar(createMenu());
149 addBookPane(typeF
, true);
152 pane
.setVisible(true);
159 private void addSourcePanes() {
161 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, "Sources", color
);
163 List
<MetaData
> sources
= new ArrayList
<MetaData
>();
164 for (String source
: reader
.getLibrary().getSources()) {
165 MetaData mSource
= new MetaData();
166 mSource
.setLuid(null);
167 mSource
.setTitle(source
);
168 mSource
.setSource(source
);
169 sources
.add(mSource
);
172 bookPane
.refreshBooks(sources
, false);
180 bookPane
.setActionListener(new BookActionListener() {
182 public void select(GuiReaderBook book
) {
187 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
188 JPopupMenu popup
= new JPopupMenu();
189 popup
.add(createMenuItemOpenBook());
190 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
194 public void action(final GuiReaderBook book
) {
196 addBookPane(book
.getMeta().getSource(), true);
203 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
204 * selected type or author.
207 * the author or the type, or NULL to get all the
210 * TRUE for type, FALSE for author
212 private void addBookPane(String value
, boolean type
) {
215 if (Instance
.getUiConfig().getBoolean(UiConfig
.SOURCE_PAGE
,
219 for (String tt
: reader
.getLibrary().getSources()) {
221 addBookPane(tt
, type
);
226 for (String tt
: reader
.getLibrary().getAuthors()) {
228 addBookPane(tt
, type
);
236 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, value
, color
);
238 booksByType
.put(bookPane
, value
);
240 booksByAuthor
.put(bookPane
, value
);
249 bookPane
.setActionListener(new BookActionListener() {
251 public void select(GuiReaderBook book
) {
256 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
257 JPopupMenu popup
= new JPopupMenu();
258 popup
.add(createMenuItemOpenBook());
259 popup
.addSeparator();
260 popup
.add(createMenuItemExport());
261 popup
.add(createMenuItemMove());
262 popup
.add(createMenuItemSetCover());
263 popup
.add(createMenuItemClearCache());
264 popup
.add(createMenuItemRedownload());
265 popup
.addSeparator();
266 popup
.add(createMenuItemDelete());
267 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
271 public void action(final GuiReaderBook book
) {
277 private void removeBookPanes() {
279 booksByAuthor
.clear();
288 * Refresh the list of {@link GuiReaderBook}s from disk.
291 private void refreshBooks() {
292 for (GuiReaderGroup group
: booksByType
.keySet()) {
293 List
<MetaData
> stories
= reader
.getLibrary().getListBySource(
294 booksByType
.get(group
));
295 group
.refreshBooks(stories
, words
);
298 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
299 List
<MetaData
> stories
= reader
.getLibrary().getListByAuthor(
300 booksByAuthor
.get(group
));
301 group
.refreshBooks(stories
, words
);
309 * Create the main menu bar.
313 private JMenuBar
createMenu() {
314 bar
= new JMenuBar();
316 JMenu file
= new JMenu("File");
317 file
.setMnemonic(KeyEvent
.VK_F
);
319 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
320 imprt
.addActionListener(new ActionListener() {
322 public void actionPerformed(ActionEvent e
) {
326 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
327 imprtF
.addActionListener(new ActionListener() {
329 public void actionPerformed(ActionEvent e
) {
333 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
334 exit
.addActionListener(new ActionListener() {
336 public void actionPerformed(ActionEvent e
) {
337 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
338 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
342 file
.add(createMenuItemOpenBook());
343 file
.add(createMenuItemExport());
344 file
.add(createMenuItemMove());
353 JMenu edit
= new JMenu("Edit");
354 edit
.setMnemonic(KeyEvent
.VK_E
);
356 edit
.add(createMenuItemClearCache());
357 edit
.add(createMenuItemRedownload());
359 edit
.add(createMenuItemDelete());
363 JMenu view
= new JMenu("View");
364 view
.setMnemonic(KeyEvent
.VK_V
);
365 JMenuItem vauthors
= new JMenuItem("Author");
366 vauthors
.setMnemonic(KeyEvent
.VK_A
);
367 vauthors
.addActionListener(new ActionListener() {
369 public void actionPerformed(ActionEvent e
) {
375 JMenuItem vwords
= new JMenuItem("Word count");
376 vwords
.setMnemonic(KeyEvent
.VK_W
);
377 vwords
.addActionListener(new ActionListener() {
379 public void actionPerformed(ActionEvent e
) {
387 JMenu sources
= new JMenu("Sources");
388 sources
.setMnemonic(KeyEvent
.VK_S
);
390 List
<String
> tt
= reader
.getLibrary().getSources();
392 for (final String type
: tt
) {
393 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
394 item
.addActionListener(new ActionListener() {
396 public void actionPerformed(ActionEvent e
) {
398 addBookPane(type
, true);
405 sources
.addSeparator();
411 JMenu authors
= new JMenu("Authors");
412 authors
.setMnemonic(KeyEvent
.VK_A
);
414 List
<String
> aa
= reader
.getLibrary().getAuthors();
416 for (final String author
: aa
) {
417 JMenuItem item
= new JMenuItem(author
== null ?
"All"
418 : author
.isEmpty() ?
"[unknown]" : author
);
419 item
.addActionListener(new ActionListener() {
421 public void actionPerformed(ActionEvent e
) {
423 addBookPane(author
, false);
429 if (author
== null || author
.isEmpty()) {
430 authors
.addSeparator();
436 JMenu options
= new JMenu("Options");
437 options
.setMnemonic(KeyEvent
.VK_O
);
438 options
.add(createMenuItemConfig());
439 options
.add(createMenuItemUiConfig());
446 * Create the Fanfix Configuration menu item.
450 private JMenuItem
createMenuItemConfig() {
451 final String title
= "Fanfix Configuration";
452 JMenuItem item
= new JMenuItem(title
);
453 item
.setMnemonic(KeyEvent
.VK_F
);
455 item
.addActionListener(new ActionListener() {
457 public void actionPerformed(ActionEvent e
) {
458 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
459 Config
.class, Instance
.getConfig(),
460 "This is where you configure the options of the program.");
461 JFrame frame
= new JFrame(title
);
463 frame
.setSize(800, 600);
464 frame
.setVisible(true);
472 * Create the UI Configuration menu item.
476 private JMenuItem
createMenuItemUiConfig() {
477 final String title
= "UI Configuration";
478 JMenuItem item
= new JMenuItem(title
);
479 item
.setMnemonic(KeyEvent
.VK_U
);
481 item
.addActionListener(new ActionListener() {
483 public void actionPerformed(ActionEvent e
) {
484 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
485 UiConfig
.class, Instance
.getUiConfig(),
486 "This is where you configure the graphical appearence of the program.");
487 JFrame frame
= new JFrame(title
);
489 frame
.setSize(800, 600);
490 frame
.setVisible(true);
498 * Create the export menu item.
502 private JMenuItem
createMenuItemExport() {
503 final JFileChooser fc
= new JFileChooser();
504 fc
.setAcceptAllFileFilterUsed(false);
506 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
507 for (OutputType type
: OutputType
.values()) {
508 String ext
= type
.getDefaultExtension(false);
509 String desc
= type
.getDesc(false);
511 if (ext
== null || ext
.isEmpty()) {
512 filters
.put(createAllFilter(desc
), type
);
514 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
518 // First the "ALL" filters, then, the extension filters
519 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
520 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
521 fc
.addChoosableFileFilter(entry
.getKey());
524 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
525 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
526 fc
.addChoosableFileFilter(entry
.getKey());
531 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
532 export
.addActionListener(new ActionListener() {
534 public void actionPerformed(ActionEvent e
) {
535 if (selectedBook
!= null) {
536 fc
.showDialog(GuiReaderFrame
.this, "Save");
537 if (fc
.getSelectedFile() != null) {
538 final OutputType type
= filters
.get(fc
.getFileFilter());
539 final String path
= fc
.getSelectedFile()
541 + type
.getDefaultExtension(false);
542 final Progress pg
= new Progress();
543 outOfUi(pg
, new Runnable() {
547 reader
.getLibrary().export(
548 selectedBook
.getMeta().getLuid(),
550 } catch (IOException e
) {
564 * Create a {@link FileFilter} that accepts all files and return the given
572 private FileFilter
createAllFilter(final String desc
) {
573 return new FileFilter() {
575 public String
getDescription() {
580 public boolean accept(File f
) {
587 * Create the refresh (delete cache) menu item.
591 private JMenuItem
createMenuItemClearCache() {
592 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
593 refresh
.addActionListener(new ActionListener() {
595 public void actionPerformed(ActionEvent e
) {
596 if (selectedBook
!= null) {
597 outOfUi(null, new Runnable() {
600 reader
.clearLocalReaderCache(selectedBook
.getMeta()
602 selectedBook
.setCached(false);
603 SwingUtilities
.invokeLater(new Runnable() {
606 selectedBook
.repaint();
619 * Create the delete menu item.
623 private JMenuItem
createMenuItemMove() {
624 JMenu moveTo
= new JMenu("Move to...");
625 moveTo
.setMnemonic(KeyEvent
.VK_M
);
627 List
<String
> types
= new ArrayList
<String
>();
629 types
.addAll(reader
.getLibrary().getSources());
631 for (String type
: types
) {
632 JMenuItem item
= new JMenuItem(type
== null ?
"New type..." : type
);
636 moveTo
.addSeparator();
639 final String ftype
= type
;
640 item
.addActionListener(new ActionListener() {
642 public void actionPerformed(ActionEvent e
) {
643 if (selectedBook
!= null) {
646 Object rep
= JOptionPane
.showInputDialog(
647 GuiReaderFrame
.this, "Move to:",
649 JOptionPane
.QUESTION_MESSAGE
, null, null,
650 selectedBook
.getMeta().getSource());
656 type
= rep
.toString();
659 final String ftype
= type
;
660 outOfUi(null, new Runnable() {
663 reader
.changeType(selectedBook
.getMeta()
668 SwingUtilities
.invokeLater(new Runnable() {
671 setJMenuBar(createMenu());
685 * Create the redownload (then delete original) menu item.
689 private JMenuItem
createMenuItemRedownload() {
690 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
691 refresh
.addActionListener(new ActionListener() {
693 public void actionPerformed(ActionEvent e
) {
694 if (selectedBook
!= null) {
695 final MetaData meta
= selectedBook
.getMeta();
696 imprt(meta
.getUrl(), new StoryRunnable() {
698 public void run(Story story
) {
699 reader
.delete(meta
.getLuid());
700 GuiReaderFrame
.this.selectedBook
= null;
701 MetaData newMeta
= story
.getMeta();
702 if (!newMeta
.getSource().equals(meta
.getSource())) {
703 reader
.changeType(newMeta
.getLuid(),
707 }, "Removing old copy");
716 * Create the delete menu item.
720 private JMenuItem
createMenuItemDelete() {
721 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
722 delete
.addActionListener(new ActionListener() {
724 public void actionPerformed(ActionEvent e
) {
725 if (selectedBook
!= null) {
726 outOfUi(null, new Runnable() {
729 reader
.delete(selectedBook
.getMeta().getLuid());
741 * Create the open menu item for a book or a source (no LUID).
745 private JMenuItem
createMenuItemOpenBook() {
746 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
747 open
.addActionListener(new ActionListener() {
749 public void actionPerformed(ActionEvent e
) {
750 if (selectedBook
!= null) {
751 if (selectedBook
.getMeta().getLuid() == null) {
753 addBookPane(selectedBook
.getMeta().getSource(), true);
756 openBook(selectedBook
);
766 * Create the SetCover menu item for a book to change the linked source
771 private JMenuItem
createMenuItemSetCover() {
772 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
773 open
.addActionListener(new ActionListener() {
775 public void actionPerformed(ActionEvent e
) {
776 if (selectedBook
!= null) {
777 reader
.getLibrary().setSourceCover(
778 selectedBook
.getMeta().getSource(),
779 selectedBook
.getMeta().getLuid());
788 * Open a {@link GuiReaderBook} item.
791 * the {@link GuiReaderBook} to open
793 private void openBook(final GuiReaderBook book
) {
794 final Progress pg
= new Progress();
795 outOfUi(pg
, new Runnable() {
799 reader
.read(book
.getMeta().getLuid(), pg
);
800 SwingUtilities
.invokeLater(new Runnable() {
803 book
.setCached(true);
806 } catch (IOException e
) {
807 // TODO: error message?
815 * Process the given action out of the Swing UI thread and link the given
816 * {@link ProgressBar} to the action.
818 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
819 * to done when the action is done.
822 * the {@link ProgressBar} or NULL
826 private void outOfUi(Progress progress
, final Runnable run
) {
827 final Progress pg
= new Progress();
828 final Progress reload
= new Progress("Reload books");
829 if (progress
== null) {
830 progress
= new Progress();
833 pg
.addProgress(progress
, 90);
834 pg
.addProgress(reload
, 10);
837 pgBar
.setProgress(pg
);
841 new Thread(new Runnable() {
848 // will trigger pgBar ActionListener:
852 }, "outOfUi thread").start();
856 * Import a {@link Story} into the main {@link LocalLibrary}.
858 * Should be called inside the UI thread.
861 * TRUE for an {@link URL}, false for a {@link File}
863 private void imprt(boolean askUrl
) {
864 JFileChooser fc
= new JFileChooser();
868 String clipboard
= "";
870 clipboard
= ("" + Toolkit
.getDefaultToolkit()
871 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
873 } catch (Exception e
) {
874 // No data will be handled
877 if (clipboard
== null || !clipboard
.startsWith("http")) {
881 url
= JOptionPane
.showInputDialog(GuiReaderFrame
.this,
882 "url of the story to import?", "Importing from URL",
883 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
884 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
885 url
= fc
.getSelectedFile().getAbsolutePath();
890 if (url
!= null && !url
.toString().isEmpty()) {
891 imprt(url
.toString(), null, null);
896 * Actually import the {@link Story} into the main {@link LocalLibrary}.
898 * Should be called inside the UI thread.
901 * the {@link Story} to import by {@link URL}
903 * Action to execute on success
905 private void imprt(final String url
, final StoryRunnable onSuccess
,
906 String onSuccessPgName
) {
907 final Progress pg
= new Progress();
908 final Progress pgImprt
= new Progress();
909 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
910 pg
.addProgress(pgImprt
, 95);
911 pg
.addProgress(pgOnSuccess
, 5);
913 outOfUi(pg
, new Runnable() {
919 story
= reader
.getLibrary().imprt(BasicReader
.getUrl(url
),
921 } catch (IOException e
) {
925 final Exception e
= ex
;
927 final boolean ok
= (e
== null);
929 pgOnSuccess
.setProgress(0);
932 SwingUtilities
.invokeLater(new Runnable() {
935 JOptionPane
.showMessageDialog(GuiReaderFrame
.this,
936 "Cannot import: " + url
, e
.getMessage(),
937 JOptionPane
.ERROR_MESSAGE
);
941 if (onSuccess
!= null) {
942 onSuccess
.run(story
);
951 * Enables or disables this component, depending on the value of the
952 * parameter <code>b</code>. An enabled component can respond to user input
953 * and generate events. Components are enabled initially by default.
955 * Disabling this component will also affect its children.
958 * If <code>true</code>, this component is enabled; otherwise
959 * this component is disabled
962 public void setEnabled(boolean b
) {
967 for (GuiReaderGroup group
: booksByType
.keySet()) {
970 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {