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
.net
.UnknownHostException
;
17 import java
.util
.ArrayList
;
18 import java
.util
.HashMap
;
19 import java
.util
.List
;
21 import java
.util
.Map
.Entry
;
23 import javax
.swing
.BoxLayout
;
24 import javax
.swing
.JFileChooser
;
25 import javax
.swing
.JFrame
;
26 import javax
.swing
.JLabel
;
27 import javax
.swing
.JMenu
;
28 import javax
.swing
.JMenuBar
;
29 import javax
.swing
.JMenuItem
;
30 import javax
.swing
.JOptionPane
;
31 import javax
.swing
.JPanel
;
32 import javax
.swing
.JPopupMenu
;
33 import javax
.swing
.JScrollPane
;
34 import javax
.swing
.SwingConstants
;
35 import javax
.swing
.SwingUtilities
;
36 import javax
.swing
.filechooser
.FileFilter
;
37 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
39 import be
.nikiroo
.fanfix
.Instance
;
40 import be
.nikiroo
.fanfix
.bundles
.Config
;
41 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
42 import be
.nikiroo
.fanfix
.data
.MetaData
;
43 import be
.nikiroo
.fanfix
.data
.Story
;
44 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
45 import be
.nikiroo
.fanfix
.library
.BasicLibrary
.Status
;
46 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
47 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
48 import be
.nikiroo
.fanfix
.reader
.GuiReaderBook
.BookActionListener
;
49 import be
.nikiroo
.utils
.Progress
;
50 import be
.nikiroo
.utils
.Version
;
51 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
52 import be
.nikiroo
.utils
.ui
.ProgressBar
;
55 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
56 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
57 * way to copy them to the {@link GuiReader} cache (
58 * {@link BasicReader#getLibrary()}), read them, delete them...
62 class GuiReaderFrame
extends JFrame
{
63 private static final long serialVersionUID
= 1L;
64 private GuiReader reader
;
65 private Map
<GuiReaderGroup
, String
> booksByType
;
66 private Map
<GuiReaderGroup
, String
> booksByAuthor
;
69 private ProgressBar pgBar
;
71 private GuiReaderBook selectedBook
;
72 private boolean words
; // words or authors (secondary info on books)
75 * A {@link Runnable} with a {@link Story} parameter.
79 private interface StoryRunnable
{
86 public void run(Story story
);
90 * Create a new {@link GuiReaderFrame}.
93 * the associated {@link GuiReader} to forward some commands and
94 * access its {@link LocalLibrary}
96 * the type of {@link Story} to load, or NULL for all types
98 public GuiReaderFrame(GuiReader reader
, String type
) {
99 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
101 this.reader
= reader
;
103 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
105 setLayout(new BorderLayout());
108 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
110 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
112 setBackground(color
);
113 pane
.setBackground(color
);
116 JScrollPane scroll
= new JScrollPane(pane
);
117 scroll
.getVerticalScrollBar().setUnitIncrement(16);
118 add(scroll
, BorderLayout
.CENTER
);
120 String message
= reader
.getLibrary().getLibraryName();
121 if (!message
.isEmpty()) {
122 JLabel name
= new JLabel(message
, SwingConstants
.CENTER
);
123 add(name
, BorderLayout
.NORTH
);
126 pgBar
= new ProgressBar();
127 add(pgBar
, BorderLayout
.SOUTH
);
129 pgBar
.addActionListener(new ActionListener() {
131 public void actionPerformed(ActionEvent e
) {
133 pgBar
.setProgress(null);
139 pgBar
.addUpdateListener(new ActionListener() {
141 public void actionPerformed(ActionEvent e
) {
148 booksByType
= new HashMap
<GuiReaderGroup
, String
>();
149 booksByAuthor
= new HashMap
<GuiReaderGroup
, String
>();
151 pane
.setVisible(false);
152 final Progress pg
= new Progress();
153 final String typeF
= type
;
154 outOfUi(pg
, new Runnable() {
157 BasicLibrary lib
= GuiReaderFrame
.this.reader
.getLibrary();
158 Status status
= lib
.getStatus();
160 if (status
== Status
.READY
) {
163 setJMenuBar(createMenu(true));
164 addBookPane(typeF
, true);
167 pane
.setVisible(true);
170 setJMenuBar(createMenu(false));
173 String err
= lib
.getLibraryName() + "\n";
176 err
+= "Library not valid";
180 err
+= "You are not allowed to access this library";
184 err
+= "Library currently unavilable";
188 err
+= "An error occured when contacting the library";
192 error(err
, "Library error", null);
200 private void addSourcePanes() {
202 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, "Sources", color
);
204 List
<MetaData
> sources
= new ArrayList
<MetaData
>();
205 for (String source
: reader
.getLibrary().getSources()) {
206 MetaData mSource
= new MetaData();
207 mSource
.setLuid(null);
208 mSource
.setTitle(source
);
209 mSource
.setSource(source
);
210 sources
.add(mSource
);
213 bookPane
.refreshBooks(sources
, false);
221 bookPane
.setActionListener(new BookActionListener() {
223 public void select(GuiReaderBook book
) {
228 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
229 JPopupMenu popup
= new JPopupMenu();
230 popup
.add(createMenuItemOpenBook());
231 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
235 public void action(final GuiReaderBook book
) {
237 addBookPane(book
.getMeta().getSource(), true);
244 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
245 * selected type or author.
248 * the author or the type, or NULL to get all the
251 * TRUE for type, FALSE for author
253 private void addBookPane(String value
, boolean type
) {
256 if (Instance
.getUiConfig().getBoolean(UiConfig
.SOURCE_PAGE
,
260 for (String tt
: reader
.getLibrary().getSources()) {
262 addBookPane(tt
, type
);
267 for (String tt
: reader
.getLibrary().getAuthors()) {
269 addBookPane(tt
, type
);
277 GuiReaderGroup bookPane
= new GuiReaderGroup(reader
, value
, color
);
279 booksByType
.put(bookPane
, value
);
281 booksByAuthor
.put(bookPane
, value
);
290 bookPane
.setActionListener(new BookActionListener() {
292 public void select(GuiReaderBook book
) {
297 public void popupRequested(GuiReaderBook book
, MouseEvent e
) {
298 JPopupMenu popup
= new JPopupMenu();
299 popup
.add(createMenuItemOpenBook());
300 popup
.addSeparator();
301 popup
.add(createMenuItemExport());
302 popup
.add(createMenuItemMove(true));
303 popup
.add(createMenuItemSetCover());
304 popup
.add(createMenuItemClearCache());
305 popup
.add(createMenuItemRedownload());
306 popup
.addSeparator();
307 popup
.add(createMenuItemDelete());
308 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
312 public void action(final GuiReaderBook book
) {
318 private void removeBookPanes() {
320 booksByAuthor
.clear();
329 * Refresh the list of {@link GuiReaderBook}s from disk.
332 private void refreshBooks() {
333 for (GuiReaderGroup group
: booksByType
.keySet()) {
334 List
<MetaData
> stories
= reader
.getLibrary().getListBySource(
335 booksByType
.get(group
));
336 group
.refreshBooks(stories
, words
);
339 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
340 List
<MetaData
> stories
= reader
.getLibrary().getListByAuthor(
341 booksByAuthor
.get(group
));
342 group
.refreshBooks(stories
, words
);
350 * Create the main menu bar.
353 * the library can be queried
357 private JMenuBar
createMenu(boolean libOk
) {
358 bar
= new JMenuBar();
360 JMenu file
= new JMenu("File");
361 file
.setMnemonic(KeyEvent
.VK_F
);
363 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
364 imprt
.addActionListener(new ActionListener() {
366 public void actionPerformed(ActionEvent e
) {
370 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
371 imprtF
.addActionListener(new ActionListener() {
373 public void actionPerformed(ActionEvent e
) {
377 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
378 exit
.addActionListener(new ActionListener() {
380 public void actionPerformed(ActionEvent e
) {
381 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
382 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
386 file
.add(createMenuItemOpenBook());
387 file
.add(createMenuItemExport());
388 file
.add(createMenuItemMove(libOk
));
397 JMenu edit
= new JMenu("Edit");
398 edit
.setMnemonic(KeyEvent
.VK_E
);
400 edit
.add(createMenuItemClearCache());
401 edit
.add(createMenuItemRedownload());
403 edit
.add(createMenuItemDelete());
407 JMenu view
= new JMenu("View");
408 view
.setMnemonic(KeyEvent
.VK_V
);
409 JMenuItem vauthors
= new JMenuItem("Author");
410 vauthors
.setMnemonic(KeyEvent
.VK_A
);
411 vauthors
.addActionListener(new ActionListener() {
413 public void actionPerformed(ActionEvent e
) {
419 JMenuItem vwords
= new JMenuItem("Word count");
420 vwords
.setMnemonic(KeyEvent
.VK_W
);
421 vwords
.addActionListener(new ActionListener() {
423 public void actionPerformed(ActionEvent e
) {
431 JMenu sources
= new JMenu("Sources");
432 sources
.setMnemonic(KeyEvent
.VK_S
);
434 List
<String
> tt
= new ArrayList
<String
>();
436 tt
.addAll(reader
.getLibrary().getSources());
440 for (final String type
: tt
) {
441 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
442 item
.addActionListener(new ActionListener() {
444 public void actionPerformed(ActionEvent e
) {
446 addBookPane(type
, true);
453 sources
.addSeparator();
459 JMenu authors
= new JMenu("Authors");
460 authors
.setMnemonic(KeyEvent
.VK_A
);
462 List
<String
> aa
= new ArrayList
<String
>();
464 aa
.addAll(reader
.getLibrary().getAuthors());
467 for (final String author
: aa
) {
468 JMenuItem item
= new JMenuItem(author
== null ?
"All"
469 : author
.isEmpty() ?
"[unknown]" : author
);
470 item
.addActionListener(new ActionListener() {
472 public void actionPerformed(ActionEvent e
) {
474 addBookPane(author
, false);
480 if (author
== null || author
.isEmpty()) {
481 authors
.addSeparator();
487 JMenu options
= new JMenu("Options");
488 options
.setMnemonic(KeyEvent
.VK_O
);
489 options
.add(createMenuItemConfig());
490 options
.add(createMenuItemUiConfig());
497 * Create the Fanfix Configuration menu item.
501 private JMenuItem
createMenuItemConfig() {
502 final String title
= "Fanfix Configuration";
503 JMenuItem item
= new JMenuItem(title
);
504 item
.setMnemonic(KeyEvent
.VK_F
);
506 item
.addActionListener(new ActionListener() {
508 public void actionPerformed(ActionEvent e
) {
509 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
510 Config
.class, Instance
.getConfig(),
511 "This is where you configure the options of the program.");
512 JFrame frame
= new JFrame(title
);
514 frame
.setSize(800, 600);
515 frame
.setVisible(true);
523 * Create the UI Configuration menu item.
527 private JMenuItem
createMenuItemUiConfig() {
528 final String title
= "UI Configuration";
529 JMenuItem item
= new JMenuItem(title
);
530 item
.setMnemonic(KeyEvent
.VK_U
);
532 item
.addActionListener(new ActionListener() {
534 public void actionPerformed(ActionEvent e
) {
535 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
536 UiConfig
.class, Instance
.getUiConfig(),
537 "This is where you configure the graphical appearence of the program.");
538 JFrame frame
= new JFrame(title
);
540 frame
.setSize(800, 600);
541 frame
.setVisible(true);
549 * Create the export menu item.
553 private JMenuItem
createMenuItemExport() {
554 final JFileChooser fc
= new JFileChooser();
555 fc
.setAcceptAllFileFilterUsed(false);
557 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
558 for (OutputType type
: OutputType
.values()) {
559 String ext
= type
.getDefaultExtension(false);
560 String desc
= type
.getDesc(false);
562 if (ext
== null || ext
.isEmpty()) {
563 filters
.put(createAllFilter(desc
), type
);
565 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
569 // First the "ALL" filters, then, the extension filters
570 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
571 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
572 fc
.addChoosableFileFilter(entry
.getKey());
575 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
576 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
577 fc
.addChoosableFileFilter(entry
.getKey());
582 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
583 export
.addActionListener(new ActionListener() {
585 public void actionPerformed(ActionEvent e
) {
586 if (selectedBook
!= null) {
587 fc
.showDialog(GuiReaderFrame
.this, "Save");
588 if (fc
.getSelectedFile() != null) {
589 final OutputType type
= filters
.get(fc
.getFileFilter());
590 final String path
= fc
.getSelectedFile()
592 + type
.getDefaultExtension(false);
593 final Progress pg
= new Progress();
594 outOfUi(pg
, new Runnable() {
598 reader
.getLibrary().export(
599 selectedBook
.getMeta().getLuid(),
601 } catch (IOException e
) {
602 Instance
.getTraceHandler().error(e
);
615 * Create a {@link FileFilter} that accepts all files and return the given
623 private FileFilter
createAllFilter(final String desc
) {
624 return new FileFilter() {
626 public String
getDescription() {
631 public boolean accept(File f
) {
638 * Create the refresh (delete cache) menu item.
642 private JMenuItem
createMenuItemClearCache() {
643 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
644 refresh
.addActionListener(new ActionListener() {
646 public void actionPerformed(ActionEvent e
) {
647 if (selectedBook
!= null) {
648 outOfUi(null, new Runnable() {
651 reader
.clearLocalReaderCache(selectedBook
.getMeta()
653 selectedBook
.setCached(false);
654 GuiReaderBook
.clearIcon(selectedBook
.getMeta());
655 SwingUtilities
.invokeLater(new Runnable() {
658 selectedBook
.repaint();
671 * Create the delete menu item.
674 * the library can be queried
678 private JMenuItem
createMenuItemMove(boolean libOk
) {
679 JMenu moveTo
= new JMenu("Move to...");
680 moveTo
.setMnemonic(KeyEvent
.VK_M
);
682 List
<String
> types
= new ArrayList
<String
>();
685 types
.addAll(reader
.getLibrary().getSources());
688 for (String type
: types
) {
689 JMenuItem item
= new JMenuItem(type
== null ?
"New type..." : type
);
693 moveTo
.addSeparator();
696 final String ftype
= type
;
697 item
.addActionListener(new ActionListener() {
699 public void actionPerformed(ActionEvent e
) {
700 if (selectedBook
!= null) {
703 Object rep
= JOptionPane
.showInputDialog(
704 GuiReaderFrame
.this, "Move to:",
706 JOptionPane
.QUESTION_MESSAGE
, null, null,
707 selectedBook
.getMeta().getSource());
713 type
= rep
.toString();
716 final String ftype
= type
;
717 outOfUi(null, new Runnable() {
720 reader
.changeType(selectedBook
.getMeta()
725 SwingUtilities
.invokeLater(new Runnable() {
728 setJMenuBar(createMenu(true));
742 * Create the redownload (then delete original) menu item.
746 private JMenuItem
createMenuItemRedownload() {
747 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
748 refresh
.addActionListener(new ActionListener() {
750 public void actionPerformed(ActionEvent e
) {
751 if (selectedBook
!= null) {
752 final MetaData meta
= selectedBook
.getMeta();
753 imprt(meta
.getUrl(), new StoryRunnable() {
755 public void run(Story story
) {
756 reader
.delete(meta
.getLuid());
757 GuiReaderFrame
.this.selectedBook
= null;
758 MetaData newMeta
= story
.getMeta();
759 if (!newMeta
.getSource().equals(meta
.getSource())) {
760 reader
.changeType(newMeta
.getLuid(),
764 }, "Removing old copy");
773 * Create the delete menu item.
777 private JMenuItem
createMenuItemDelete() {
778 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
779 delete
.addActionListener(new ActionListener() {
781 public void actionPerformed(ActionEvent e
) {
782 if (selectedBook
!= null) {
783 outOfUi(null, new Runnable() {
786 reader
.delete(selectedBook
.getMeta().getLuid());
798 * Create the open menu item for a book or a source (no LUID).
802 private JMenuItem
createMenuItemOpenBook() {
803 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
804 open
.addActionListener(new ActionListener() {
806 public void actionPerformed(ActionEvent e
) {
807 if (selectedBook
!= null) {
808 if (selectedBook
.getMeta().getLuid() == null) {
810 addBookPane(selectedBook
.getMeta().getSource(), true);
813 openBook(selectedBook
);
823 * Create the SetCover menu item for a book to change the linked source
828 private JMenuItem
createMenuItemSetCover() {
829 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
830 open
.addActionListener(new ActionListener() {
832 public void actionPerformed(ActionEvent e
) {
833 if (selectedBook
!= null) {
834 reader
.getLibrary().setSourceCover(
835 selectedBook
.getMeta().getSource(),
836 selectedBook
.getMeta().getLuid());
837 MetaData source
= selectedBook
.getMeta().clone();
838 source
.setLuid(null);
839 GuiReaderBook
.clearIcon(source
);
848 * Open a {@link GuiReaderBook} item.
851 * the {@link GuiReaderBook} to open
853 private void openBook(final GuiReaderBook book
) {
854 final Progress pg
= new Progress();
855 outOfUi(pg
, new Runnable() {
859 reader
.read(book
.getMeta().getLuid(), pg
);
860 SwingUtilities
.invokeLater(new Runnable() {
863 book
.setCached(true);
866 } catch (IOException e
) {
867 // TODO: error message?
868 Instance
.getTraceHandler().error(e
);
875 * Process the given action out of the Swing UI thread and link the given
876 * {@link ProgressBar} to the action.
878 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
879 * to done when the action is done.
882 * the {@link ProgressBar} or NULL
886 private void outOfUi(Progress progress
, final Runnable run
) {
887 final Progress pg
= new Progress();
888 final Progress reload
= new Progress("Reload books");
889 if (progress
== null) {
890 progress
= new Progress();
893 pg
.addProgress(progress
, 90);
894 pg
.addProgress(reload
, 10);
897 pgBar
.setProgress(pg
);
901 new Thread(new Runnable() {
910 // will trigger pgBar ActionListener:
915 }, "outOfUi thread").start();
919 * Import a {@link Story} into the main {@link LocalLibrary}.
921 * Should be called inside the UI thread.
924 * TRUE for an {@link URL}, false for a {@link File}
926 private void imprt(boolean askUrl
) {
927 JFileChooser fc
= new JFileChooser();
931 String clipboard
= "";
933 clipboard
= ("" + Toolkit
.getDefaultToolkit()
934 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
936 } catch (Exception e
) {
937 // No data will be handled
940 if (clipboard
== null || !clipboard
.startsWith("http")) {
944 url
= JOptionPane
.showInputDialog(GuiReaderFrame
.this,
945 "url of the story to import?", "Importing from URL",
946 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
947 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
948 url
= fc
.getSelectedFile().getAbsolutePath();
953 if (url
!= null && !url
.toString().isEmpty()) {
954 imprt(url
.toString(), null, null);
959 * Actually import the {@link Story} into the main {@link LocalLibrary}.
961 * Should be called inside the UI thread.
964 * the {@link Story} to import by {@link URL}
966 * Action to execute on success
968 private void imprt(final String url
, final StoryRunnable onSuccess
,
969 String onSuccessPgName
) {
970 final Progress pg
= new Progress();
971 final Progress pgImprt
= new Progress();
972 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
973 pg
.addProgress(pgImprt
, 95);
974 pg
.addProgress(pgOnSuccess
, 5);
976 outOfUi(pg
, new Runnable() {
982 story
= reader
.getLibrary().imprt(BasicReader
.getUrl(url
),
984 } catch (IOException e
) {
988 final Exception e
= ex
;
990 final boolean ok
= (e
== null);
992 pgOnSuccess
.setProgress(0);
994 if (e
instanceof UnknownHostException
) {
995 error("URL not supported: " + url
, "Cannot import URL",
998 error("Failed to import " + url
+ ": \n"
999 + e
.getMessage(), "Cannot import URL", e
);
1002 if (onSuccess
!= null) {
1003 onSuccess
.run(story
);
1012 * Enables or disables this component, depending on the value of the
1013 * parameter <code>b</code>. An enabled component can respond to user input
1014 * and generate events. Components are enabled initially by default.
1016 * Disabling this component will also affect its children.
1019 * If <code>true</code>, this component is enabled; otherwise
1020 * this component is disabled
1023 public void setEnabled(boolean b
) {
1028 for (GuiReaderGroup group
: booksByType
.keySet()) {
1029 group
.setEnabled(b
);
1031 for (GuiReaderGroup group
: booksByAuthor
.keySet()) {
1032 group
.setEnabled(b
);
1034 super.setEnabled(b
);
1039 * Display an error message and log the linked {@link Exception}.
1044 * the title of the error message
1046 * the exception to log if any
1048 private void error(final String message
, final String title
, Exception e
) {
1049 Instance
.getTraceHandler().error(title
+ ": " + message
);
1051 Instance
.getTraceHandler().error(e
);
1054 SwingUtilities
.invokeLater(new Runnable() {
1057 JOptionPane
.showMessageDialog(GuiReaderFrame
.this, message
,
1058 title
, JOptionPane
.ERROR_MESSAGE
);