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
.BasicLibrary
;
44 import be
.nikiroo
.fanfix
.library
.BasicLibrary
.Status
;
45 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
46 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
47 import be
.nikiroo
.fanfix
.reader
.GuiReaderBook
.BookActionListener
;
48 import be
.nikiroo
.utils
.Progress
;
49 import be
.nikiroo
.utils
.Version
;
50 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
51 import be
.nikiroo
.utils
.ui
.ProgressBar
;
54 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
55 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
56 * way to copy them to the {@link GuiReader} cache (
57 * {@link BasicReader#getLibrary()}), read them, delete them...
61 class GuiReaderFrame
extends JFrame
{
62 private static final long serialVersionUID
= 1L;
63 private GuiReader reader
;
64 private Map
<GuiReaderGroup
, String
> booksByType
;
65 private Map
<GuiReaderGroup
, String
> booksByAuthor
;
68 private ProgressBar pgBar
;
70 private GuiReaderBook selectedBook
;
71 private boolean words
; // words or authors (secondary info on books)
74 * A {@link Runnable} with a {@link Story} parameter.
78 private interface StoryRunnable
{
85 public void run(Story story
);
89 * Create a new {@link GuiReaderFrame}.
92 * the associated {@link GuiReader} to forward some commands and
93 * access its {@link LocalLibrary}
95 * the type of {@link Story} to load, or NULL for all types
97 public GuiReaderFrame(GuiReader reader
, String type
) {
98 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
100 this.reader
= reader
;
102 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
104 setLayout(new BorderLayout());
107 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
109 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
111 setBackground(color
);
112 pane
.setBackground(color
);
115 JScrollPane scroll
= new JScrollPane(pane
);
116 scroll
.getVerticalScrollBar().setUnitIncrement(16);
117 add(scroll
, BorderLayout
.CENTER
);
119 String message
= reader
.getLibrary().getLibraryName();
120 if (!message
.isEmpty()) {
121 JLabel name
= new JLabel(message
, SwingConstants
.CENTER
);
122 add(name
, BorderLayout
.NORTH
);
125 pgBar
= new ProgressBar();
126 add(pgBar
, BorderLayout
.SOUTH
);
128 pgBar
.addActionListener(new ActionListener() {
130 public void actionPerformed(ActionEvent e
) {
132 pgBar
.setProgress(null);
138 pgBar
.addUpdateListener(new ActionListener() {
140 public void actionPerformed(ActionEvent e
) {
147 booksByType
= new HashMap
<GuiReaderGroup
, String
>();
148 booksByAuthor
= new HashMap
<GuiReaderGroup
, String
>();
150 pane
.setVisible(false);
151 final Progress pg
= new Progress();
152 final String typeF
= type
;
153 outOfUi(pg
, new Runnable() {
156 BasicLibrary lib
= GuiReaderFrame
.this.reader
.getLibrary();
157 Status status
= lib
.getStatus();
159 if (status
== Status
.READY
) {
162 setJMenuBar(createMenu(true));
163 addBookPane(typeF
, true);
166 pane
.setVisible(true);
169 setJMenuBar(createMenu(false));
172 String err
= lib
.getLibraryName() + "\n";
175 err
+= "Library not valid";
179 err
+= "You are not allowed to access this library";
183 err
+= "Library currently unavilable";
187 err
+= "An error occured when contacting the library";
191 error(err
, "Library error", null);
199 private void addSourcePanes() {
201 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, "Sources", color
);
203 List
<MetaData
> sources
= new ArrayList
<MetaData
>();
204 for (String source
: reader
.getLibrary().getSources()) {
205 MetaData mSource
= new MetaData();
206 mSource
.setLuid(null);
207 mSource
.setTitle(source
);
208 mSource
.setSource(source
);
209 sources
.add(mSource
);
212 bookPane
.refreshBooks(sources
, false);
220 bookPane
.setActionListener(new BookActionListener() {
222 public void select(GuiReaderBook book
) {
227 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
228 JPopupMenu popup
= new JPopupMenu();
229 popup
.add(createMenuItemOpenBook());
230 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
234 public void action(final GuiReaderBook book
) {
236 addBookPane(book
.getMeta().getSource(), true);
243 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
244 * selected type or author.
247 * the author or the type, or NULL to get all the
250 * TRUE for type, FALSE for author
252 private void addBookPane(String value
, boolean type
) {
255 if (Instance
.getUiConfig().getBoolean(UiConfig
.SOURCE_PAGE
,
259 for (String tt
: reader
.getLibrary().getSources()) {
261 addBookPane(tt
, type
);
266 for (String tt
: reader
.getLibrary().getAuthors()) {
268 addBookPane(tt
, type
);
276 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, value
, color
);
278 booksByType
.put(bookPane
, value
);
280 booksByAuthor
.put(bookPane
, value
);
289 bookPane
.setActionListener(new BookActionListener() {
291 public void select(GuiReaderBook book
) {
296 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
297 JPopupMenu popup
= new JPopupMenu();
298 popup
.add(createMenuItemOpenBook());
299 popup
.addSeparator();
300 popup
.add(createMenuItemExport());
301 popup
.add(createMenuItemMove(true));
302 popup
.add(createMenuItemSetCover());
303 popup
.add(createMenuItemClearCache());
304 popup
.add(createMenuItemRedownload());
305 popup
.addSeparator();
306 popup
.add(createMenuItemDelete());
307 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
311 public void action(final GuiReaderBook book
) {
317 private void removeBookPanes() {
319 booksByAuthor
.clear();
328 * Refresh the list of {@link GuiReaderBook}s from disk.
331 private void refreshBooks() {
332 for (GuiReaderGroup group
: booksByType
.keySet()) {
333 List
<MetaData
> stories
= reader
.getLibrary().getListBySource(
334 booksByType
.get(group
));
335 group
.refreshBooks(stories
, words
);
338 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
339 List
<MetaData
> stories
= reader
.getLibrary().getListByAuthor(
340 booksByAuthor
.get(group
));
341 group
.refreshBooks(stories
, words
);
349 * Create the main menu bar.
352 * the library can be queried
356 private JMenuBar
createMenu(boolean libOk
) {
357 bar
= new JMenuBar();
359 JMenu file
= new JMenu("File");
360 file
.setMnemonic(KeyEvent
.VK_F
);
362 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
363 imprt
.addActionListener(new ActionListener() {
365 public void actionPerformed(ActionEvent e
) {
369 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
370 imprtF
.addActionListener(new ActionListener() {
372 public void actionPerformed(ActionEvent e
) {
376 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
377 exit
.addActionListener(new ActionListener() {
379 public void actionPerformed(ActionEvent e
) {
380 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
381 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
385 file
.add(createMenuItemOpenBook());
386 file
.add(createMenuItemExport());
387 file
.add(createMenuItemMove(libOk
));
396 JMenu edit
= new JMenu("Edit");
397 edit
.setMnemonic(KeyEvent
.VK_E
);
399 edit
.add(createMenuItemClearCache());
400 edit
.add(createMenuItemRedownload());
402 edit
.add(createMenuItemDelete());
406 JMenu view
= new JMenu("View");
407 view
.setMnemonic(KeyEvent
.VK_V
);
408 JMenuItem vauthors
= new JMenuItem("Author");
409 vauthors
.setMnemonic(KeyEvent
.VK_A
);
410 vauthors
.addActionListener(new ActionListener() {
412 public void actionPerformed(ActionEvent e
) {
418 JMenuItem vwords
= new JMenuItem("Word count");
419 vwords
.setMnemonic(KeyEvent
.VK_W
);
420 vwords
.addActionListener(new ActionListener() {
422 public void actionPerformed(ActionEvent e
) {
430 JMenu sources
= new JMenu("Sources");
431 sources
.setMnemonic(KeyEvent
.VK_S
);
433 List
<String
> tt
= new ArrayList
<String
>();
435 tt
.addAll(reader
.getLibrary().getSources());
439 for (final String type
: tt
) {
440 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
441 item
.addActionListener(new ActionListener() {
443 public void actionPerformed(ActionEvent e
) {
445 addBookPane(type
, true);
452 sources
.addSeparator();
458 JMenu authors
= new JMenu("Authors");
459 authors
.setMnemonic(KeyEvent
.VK_A
);
461 List
<String
> aa
= new ArrayList
<String
>();
463 aa
.addAll(reader
.getLibrary().getAuthors());
466 for (final String author
: aa
) {
467 JMenuItem item
= new JMenuItem(author
== null ?
"All"
468 : author
.isEmpty() ?
"[unknown]" : author
);
469 item
.addActionListener(new ActionListener() {
471 public void actionPerformed(ActionEvent e
) {
473 addBookPane(author
, false);
479 if (author
== null || author
.isEmpty()) {
480 authors
.addSeparator();
486 JMenu options
= new JMenu("Options");
487 options
.setMnemonic(KeyEvent
.VK_O
);
488 options
.add(createMenuItemConfig());
489 options
.add(createMenuItemUiConfig());
496 * Create the Fanfix Configuration menu item.
500 private JMenuItem
createMenuItemConfig() {
501 final String title
= "Fanfix Configuration";
502 JMenuItem item
= new JMenuItem(title
);
503 item
.setMnemonic(KeyEvent
.VK_F
);
505 item
.addActionListener(new ActionListener() {
507 public void actionPerformed(ActionEvent e
) {
508 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
509 Config
.class, Instance
.getConfig(),
510 "This is where you configure the options of the program.");
511 JFrame frame
= new JFrame(title
);
513 frame
.setSize(800, 600);
514 frame
.setVisible(true);
522 * Create the UI Configuration menu item.
526 private JMenuItem
createMenuItemUiConfig() {
527 final String title
= "UI Configuration";
528 JMenuItem item
= new JMenuItem(title
);
529 item
.setMnemonic(KeyEvent
.VK_U
);
531 item
.addActionListener(new ActionListener() {
533 public void actionPerformed(ActionEvent e
) {
534 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
535 UiConfig
.class, Instance
.getUiConfig(),
536 "This is where you configure the graphical appearence of the program.");
537 JFrame frame
= new JFrame(title
);
539 frame
.setSize(800, 600);
540 frame
.setVisible(true);
548 * Create the export menu item.
552 private JMenuItem
createMenuItemExport() {
553 final JFileChooser fc
= new JFileChooser();
554 fc
.setAcceptAllFileFilterUsed(false);
556 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
557 for (OutputType type
: OutputType
.values()) {
558 String ext
= type
.getDefaultExtension(false);
559 String desc
= type
.getDesc(false);
561 if (ext
== null || ext
.isEmpty()) {
562 filters
.put(createAllFilter(desc
), type
);
564 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
568 // First the "ALL" filters, then, the extension filters
569 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
570 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
571 fc
.addChoosableFileFilter(entry
.getKey());
574 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
575 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
576 fc
.addChoosableFileFilter(entry
.getKey());
581 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
582 export
.addActionListener(new ActionListener() {
584 public void actionPerformed(ActionEvent e
) {
585 if (selectedBook
!= null) {
586 fc
.showDialog(GuiReaderFrame
.this, "Save");
587 if (fc
.getSelectedFile() != null) {
588 final OutputType type
= filters
.get(fc
.getFileFilter());
589 final String path
= fc
.getSelectedFile()
591 + type
.getDefaultExtension(false);
592 final Progress pg
= new Progress();
593 outOfUi(pg
, new Runnable() {
597 reader
.getLibrary().export(
598 selectedBook
.getMeta().getLuid(),
600 } catch (IOException e
) {
601 Instance
.getTraceHandler().error(e
);
614 * Create a {@link FileFilter} that accepts all files and return the given
622 private FileFilter
createAllFilter(final String desc
) {
623 return new FileFilter() {
625 public String
getDescription() {
630 public boolean accept(File f
) {
637 * Create the refresh (delete cache) menu item.
641 private JMenuItem
createMenuItemClearCache() {
642 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
643 refresh
.addActionListener(new ActionListener() {
645 public void actionPerformed(ActionEvent e
) {
646 if (selectedBook
!= null) {
647 outOfUi(null, new Runnable() {
650 reader
.clearLocalReaderCache(selectedBook
.getMeta()
652 selectedBook
.setCached(false);
653 GuiReaderBook
.clearIcon(selectedBook
.getMeta());
654 SwingUtilities
.invokeLater(new Runnable() {
657 selectedBook
.repaint();
670 * Create the delete menu item.
673 * the library can be queried
677 private JMenuItem
createMenuItemMove(boolean libOk
) {
678 JMenu moveTo
= new JMenu("Move to...");
679 moveTo
.setMnemonic(KeyEvent
.VK_M
);
681 List
<String
> types
= new ArrayList
<String
>();
684 types
.addAll(reader
.getLibrary().getSources());
687 for (String type
: types
) {
688 JMenuItem item
= new JMenuItem(type
== null ?
"New type..." : type
);
692 moveTo
.addSeparator();
695 final String ftype
= type
;
696 item
.addActionListener(new ActionListener() {
698 public void actionPerformed(ActionEvent e
) {
699 if (selectedBook
!= null) {
702 Object rep
= JOptionPane
.showInputDialog(
703 GuiReaderFrame
.this, "Move to:",
705 JOptionPane
.QUESTION_MESSAGE
, null, null,
706 selectedBook
.getMeta().getSource());
712 type
= rep
.toString();
715 final String ftype
= type
;
716 outOfUi(null, new Runnable() {
719 reader
.changeType(selectedBook
.getMeta()
724 SwingUtilities
.invokeLater(new Runnable() {
727 setJMenuBar(createMenu(true));
741 * Create the redownload (then delete original) menu item.
745 private JMenuItem
createMenuItemRedownload() {
746 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
747 refresh
.addActionListener(new ActionListener() {
749 public void actionPerformed(ActionEvent e
) {
750 if (selectedBook
!= null) {
751 final MetaData meta
= selectedBook
.getMeta();
752 imprt(meta
.getUrl(), new StoryRunnable() {
754 public void run(Story story
) {
755 reader
.delete(meta
.getLuid());
756 GuiReaderFrame
.this.selectedBook
= null;
757 MetaData newMeta
= story
.getMeta();
758 if (!newMeta
.getSource().equals(meta
.getSource())) {
759 reader
.changeType(newMeta
.getLuid(),
763 }, "Removing old copy");
772 * Create the delete menu item.
776 private JMenuItem
createMenuItemDelete() {
777 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
778 delete
.addActionListener(new ActionListener() {
780 public void actionPerformed(ActionEvent e
) {
781 if (selectedBook
!= null) {
782 outOfUi(null, new Runnable() {
785 reader
.delete(selectedBook
.getMeta().getLuid());
797 * Create the open menu item for a book or a source (no LUID).
801 private JMenuItem
createMenuItemOpenBook() {
802 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
803 open
.addActionListener(new ActionListener() {
805 public void actionPerformed(ActionEvent e
) {
806 if (selectedBook
!= null) {
807 if (selectedBook
.getMeta().getLuid() == null) {
809 addBookPane(selectedBook
.getMeta().getSource(), true);
812 openBook(selectedBook
);
822 * Create the SetCover menu item for a book to change the linked source
827 private JMenuItem
createMenuItemSetCover() {
828 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
829 open
.addActionListener(new ActionListener() {
831 public void actionPerformed(ActionEvent e
) {
832 if (selectedBook
!= null) {
833 reader
.getLibrary().setSourceCover(
834 selectedBook
.getMeta().getSource(),
835 selectedBook
.getMeta().getLuid());
836 MetaData source
= selectedBook
.getMeta().clone();
837 source
.setLuid(null);
838 GuiReaderBook
.clearIcon(source
);
847 * Open a {@link GuiReaderBook} item.
850 * the {@link GuiReaderBook} to open
852 private void openBook(final GuiReaderBook book
) {
853 final Progress pg
= new Progress();
854 outOfUi(pg
, new Runnable() {
858 reader
.read(book
.getMeta().getLuid(), pg
);
859 SwingUtilities
.invokeLater(new Runnable() {
862 book
.setCached(true);
865 } catch (IOException e
) {
866 // TODO: error message?
867 Instance
.getTraceHandler().error(e
);
874 * Process the given action out of the Swing UI thread and link the given
875 * {@link ProgressBar} to the action.
877 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
878 * to done when the action is done.
881 * the {@link ProgressBar} or NULL
885 private void outOfUi(Progress progress
, final Runnable run
) {
886 final Progress pg
= new Progress();
887 final Progress reload
= new Progress("Reload books");
888 if (progress
== null) {
889 progress
= new Progress();
892 pg
.addProgress(progress
, 90);
893 pg
.addProgress(reload
, 10);
896 pgBar
.setProgress(pg
);
900 new Thread(new Runnable() {
907 // will trigger pgBar ActionListener:
911 }, "outOfUi thread").start();
915 * Import a {@link Story} into the main {@link LocalLibrary}.
917 * Should be called inside the UI thread.
920 * TRUE for an {@link URL}, false for a {@link File}
922 private void imprt(boolean askUrl
) {
923 JFileChooser fc
= new JFileChooser();
927 String clipboard
= "";
929 clipboard
= ("" + Toolkit
.getDefaultToolkit()
930 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
932 } catch (Exception e
) {
933 // No data will be handled
936 if (clipboard
== null || !clipboard
.startsWith("http")) {
940 url
= JOptionPane
.showInputDialog(GuiReaderFrame
.this,
941 "url of the story to import?", "Importing from URL",
942 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
943 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
944 url
= fc
.getSelectedFile().getAbsolutePath();
949 if (url
!= null && !url
.toString().isEmpty()) {
950 imprt(url
.toString(), null, null);
955 * Actually import the {@link Story} into the main {@link LocalLibrary}.
957 * Should be called inside the UI thread.
960 * the {@link Story} to import by {@link URL}
962 * Action to execute on success
964 private void imprt(final String url
, final StoryRunnable onSuccess
,
965 String onSuccessPgName
) {
966 final Progress pg
= new Progress();
967 final Progress pgImprt
= new Progress();
968 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
969 pg
.addProgress(pgImprt
, 95);
970 pg
.addProgress(pgOnSuccess
, 5);
972 outOfUi(pg
, new Runnable() {
978 story
= reader
.getLibrary().imprt(BasicReader
.getUrl(url
),
980 } catch (IOException e
) {
984 final Exception e
= ex
;
986 final boolean ok
= (e
== null);
988 pgOnSuccess
.setProgress(0);
990 error("Cannot import URL", "Failed to import " + url
991 + ": \n" + e
.getMessage(), e
);
993 if (onSuccess
!= null) {
994 onSuccess
.run(story
);
1003 * Enables or disables this component, depending on the value of the
1004 * parameter <code>b</code>. An enabled component can respond to user input
1005 * and generate events. Components are enabled initially by default.
1007 * Disabling this component will also affect its children.
1010 * If <code>true</code>, this component is enabled; otherwise
1011 * this component is disabled
1014 public void setEnabled(boolean b
) {
1019 for (GuiReaderGroup group
: booksByType
.keySet()) {
1020 group
.setEnabled(b
);
1022 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
1023 group
.setEnabled(b
);
1025 super.setEnabled(b
);
1030 * Display an error message and log the linked {@link Exception}.
1035 * the title of the error message
1037 * the exception to log if any
1039 private void error(final String message
, final String title
, Exception e
) {
1040 Instance
.getTraceHandler().error(title
+ ": " + message
);
1042 Instance
.getTraceHandler().error(e
);
1045 SwingUtilities
.invokeLater(new Runnable() {
1048 JOptionPane
.showMessageDialog(GuiReaderFrame
.this, message
,
1049 title
, JOptionPane
.ERROR_MESSAGE
);