1 package be
.nikiroo
.fanfix
.reader
.ui
;
3 import java
.awt
.BorderLayout
;
5 import java
.awt
.event
.ActionEvent
;
6 import java
.awt
.event
.ActionListener
;
7 import java
.awt
.event
.KeyEvent
;
8 import java
.awt
.event
.WindowEvent
;
10 import java
.io
.IOException
;
11 import java
.util
.HashMap
;
12 import java
.util
.List
;
14 import java
.util
.Map
.Entry
;
16 import javax
.swing
.JFileChooser
;
17 import javax
.swing
.JFrame
;
18 import javax
.swing
.JMenu
;
19 import javax
.swing
.JMenuBar
;
20 import javax
.swing
.JMenuItem
;
21 import javax
.swing
.JOptionPane
;
22 import javax
.swing
.JPopupMenu
;
23 import javax
.swing
.SwingUtilities
;
24 import javax
.swing
.filechooser
.FileFilter
;
25 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
27 import be
.nikiroo
.fanfix
.Instance
;
28 import be
.nikiroo
.fanfix
.bundles
.Config
;
29 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
30 import be
.nikiroo
.fanfix
.data
.MetaData
;
31 import be
.nikiroo
.fanfix
.data
.Story
;
32 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
33 import be
.nikiroo
.fanfix
.library
.LocalLibrary
;
34 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
35 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
36 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderMainPanel
.FrameHelper
;
37 import be
.nikiroo
.fanfix
.reader
.ui
.GuiReaderMainPanel
.StoryRunnable
;
38 import be
.nikiroo
.utils
.Progress
;
39 import be
.nikiroo
.utils
.Version
;
40 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
43 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
44 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
45 * way to copy them to the {@link GuiReader} cache (
46 * {@link BasicReader#getLibrary()}), read them, delete them...
50 class GuiReaderFrame
extends JFrame
implements FrameHelper
{
51 private static final long serialVersionUID
= 1L;
52 private GuiReader reader
;
53 private GuiReaderMainPanel mainPanel
;
56 * The different modification actions you can use on {@link Story} items.
60 private enum ChangeAction
{
61 /** Change the source/type, that is, move it to another source. */
63 /** Change its name. */
65 /** Change its author. */
70 * Create a new {@link GuiReaderFrame}.
73 * the associated {@link GuiReader} to forward some commands and
74 * access its {@link LocalLibrary}
76 * the type of {@link Story} to load, or NULL for all types
78 public GuiReaderFrame(GuiReader reader
, String type
) {
79 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
83 mainPanel
= new GuiReaderMainPanel(this, type
);
86 setLayout(new BorderLayout());
91 public JPopupMenu
createBookPopup() {
92 JPopupMenu popup
= new JPopupMenu();
93 popup
.add(createMenuItemOpenBook());
95 popup
.add(createMenuItemExport());
96 popup
.add(createMenuItemMoveTo(true));
97 popup
.add(createMenuItemSetCoverForSource());
98 popup
.add(createMenuItemSetCoverForAuthor());
99 popup
.add(createMenuItemClearCache());
100 popup
.add(createMenuItemRedownload());
101 popup
.addSeparator();
102 popup
.add(createMenuItemRename(true));
103 popup
.add(createMenuItemSetAuthor(true));
104 popup
.addSeparator();
105 popup
.add(createMenuItemDelete());
106 popup
.addSeparator();
107 popup
.add(createMenuItemProperties());
112 public JPopupMenu
createSourceAuthorPopup() {
113 JPopupMenu popup
= new JPopupMenu();
114 popup
.add(createMenuItemOpenBook());
119 public void createMenu(boolean libOk
) {
122 JMenuBar bar
= new JMenuBar();
124 JMenu file
= new JMenu("File");
125 file
.setMnemonic(KeyEvent
.VK_F
);
127 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
128 imprt
.addActionListener(new ActionListener() {
130 public void actionPerformed(ActionEvent e
) {
131 mainPanel
.imprt(true);
134 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
135 imprtF
.addActionListener(new ActionListener() {
137 public void actionPerformed(ActionEvent e
) {
138 mainPanel
.imprt(false);
141 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
142 exit
.addActionListener(new ActionListener() {
144 public void actionPerformed(ActionEvent e
) {
145 GuiReaderFrame
.this.dispatchEvent(new WindowEvent(
146 GuiReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
150 file
.add(createMenuItemOpenBook());
151 file
.add(createMenuItemExport());
152 file
.add(createMenuItemMoveTo(libOk
));
157 file
.add(createMenuItemRename(libOk
));
158 file
.add(createMenuItemSetAuthor(libOk
));
164 JMenu edit
= new JMenu("Edit");
165 edit
.setMnemonic(KeyEvent
.VK_E
);
167 edit
.add(createMenuItemClearCache());
168 edit
.add(createMenuItemRedownload());
170 edit
.add(createMenuItemDelete());
174 JMenu view
= new JMenu("View");
175 view
.setMnemonic(KeyEvent
.VK_V
);
176 JMenuItem vauthors
= new JMenuItem("Author");
177 vauthors
.setMnemonic(KeyEvent
.VK_A
);
178 vauthors
.addActionListener(new ActionListener() {
180 public void actionPerformed(ActionEvent e
) {
181 mainPanel
.setWords(false);
182 mainPanel
.refreshBooks();
186 JMenuItem vwords
= new JMenuItem("Word count");
187 vwords
.setMnemonic(KeyEvent
.VK_W
);
188 vwords
.addActionListener(new ActionListener() {
190 public void actionPerformed(ActionEvent e
) {
191 mainPanel
.setWords(true);
192 mainPanel
.refreshBooks();
198 Map
<String
, List
<String
>> groupedSources
= new HashMap
<String
, List
<String
>>();
200 groupedSources
= reader
.getLibrary().getSourcesGrouped();
202 JMenu sources
= new JMenu("Sources");
203 sources
.setMnemonic(KeyEvent
.VK_S
);
204 populateMenuSA(sources
, groupedSources
, true);
207 Map
<String
, List
<String
>> goupedAuthors
= new HashMap
<String
, List
<String
>>();
209 goupedAuthors
= reader
.getLibrary().getAuthorsGrouped();
211 JMenu authors
= new JMenu("Authors");
212 authors
.setMnemonic(KeyEvent
.VK_A
);
213 populateMenuSA(authors
, goupedAuthors
, false);
216 JMenu options
= new JMenu("Options");
217 options
.setMnemonic(KeyEvent
.VK_O
);
218 options
.add(createMenuItemConfig());
219 options
.add(createMenuItemUiConfig());
226 private void populateMenuSA(JMenu menu
,
227 Map
<String
, List
<String
>> groupedValues
, boolean type
) {
229 // "All" and "Listing" special items
230 JMenuItem item
= new JMenuItem("All");
231 item
.addActionListener(getActionOpenList(type
, false));
233 item
= new JMenuItem("Listing");
234 item
.addActionListener(getActionOpenList(type
, true));
238 for (final String value
: groupedValues
.keySet()) {
239 List
<String
> list
= groupedValues
.get(value
);
240 if (type
&& list
.size() == 1 && list
.get(0).isEmpty()) {
241 // leaf item source/type
242 item
= new JMenuItem(value
.isEmpty() ?
"[unknown]" : value
);
243 item
.addActionListener(getActionOpen(value
, type
));
247 if (!type
&& groupedValues
.size() == 1) {
248 // only one group of authors
251 dir
= new JMenu(value
.isEmpty() ?
"[unknown]" : value
);
254 for (String sub
: list
) {
255 // " " instead of "" for the visual height
256 String itemName
= sub
;
257 if (itemName
.isEmpty()) {
258 itemName
= type ?
" " : "[unknown]";
261 String actualValue
= value
;
263 if (!sub
.isEmpty()) {
264 actualValue
+= "/" + sub
;
270 item
= new JMenuItem(itemName
);
271 item
.addActionListener(getActionOpen(actualValue
, type
));
283 * Return an {@link ActionListener} that will set the given source (type) as
284 * the selected/displayed one.
287 * the type (source) to select, cannot be NULL
289 * @return the {@link ActionListener}
291 private ActionListener
getActionOpen(final String source
, final boolean type
) {
292 return new ActionListener() {
294 public void actionPerformed(ActionEvent e
) {
295 mainPanel
.removeBookPanes();
296 mainPanel
.addBookPane(source
, type
);
297 mainPanel
.refreshBooks();
302 private ActionListener
getActionOpenList(final boolean type
,
303 final boolean listMode
) {
304 return new ActionListener() {
306 public void actionPerformed(ActionEvent e
) {
307 mainPanel
.removeBookPanes();
308 mainPanel
.addBookPane(type
, listMode
);
309 mainPanel
.refreshBooks();
315 * Create the Fanfix Configuration menu item.
319 private JMenuItem
createMenuItemConfig() {
320 final String title
= "Fanfix Configuration";
321 JMenuItem item
= new JMenuItem(title
);
322 item
.setMnemonic(KeyEvent
.VK_F
);
324 item
.addActionListener(new ActionListener() {
326 public void actionPerformed(ActionEvent e
) {
327 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
328 Config
.class, Instance
.getConfig(),
329 "This is where you configure the options of the program.");
330 JFrame frame
= new JFrame(title
);
332 frame
.setSize(800, 600);
333 frame
.setVisible(true);
341 * Create the UI Configuration menu item.
345 private JMenuItem
createMenuItemUiConfig() {
346 final String title
= "UI Configuration";
347 JMenuItem item
= new JMenuItem(title
);
348 item
.setMnemonic(KeyEvent
.VK_U
);
350 item
.addActionListener(new ActionListener() {
352 public void actionPerformed(ActionEvent e
) {
353 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
354 UiConfig
.class, Instance
.getUiConfig(),
355 "This is where you configure the graphical appearence of the program.");
356 JFrame frame
= new JFrame(title
);
358 frame
.setSize(800, 600);
359 frame
.setVisible(true);
367 * Create the export menu item.
371 private JMenuItem
createMenuItemExport() {
372 final JFileChooser fc
= new JFileChooser();
373 fc
.setAcceptAllFileFilterUsed(false);
375 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
376 for (OutputType type
: OutputType
.values()) {
377 String ext
= type
.getDefaultExtension(false);
378 String desc
= type
.getDesc(false);
380 if (ext
== null || ext
.isEmpty()) {
381 filters
.put(createAllFilter(desc
), type
);
383 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
387 // First the "ALL" filters, then, the extension filters
388 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
389 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
390 fc
.addChoosableFileFilter(entry
.getKey());
393 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
394 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
395 fc
.addChoosableFileFilter(entry
.getKey());
400 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
401 export
.addActionListener(new ActionListener() {
403 public void actionPerformed(ActionEvent e
) {
404 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
405 if (selectedBook
!= null) {
406 fc
.showDialog(GuiReaderFrame
.this, "Save");
407 if (fc
.getSelectedFile() != null) {
408 final OutputType type
= filters
.get(fc
.getFileFilter());
409 final String path
= fc
.getSelectedFile()
411 + type
.getDefaultExtension(false);
412 final Progress pg
= new Progress();
413 mainPanel
.outOfUi(pg
, new Runnable() {
417 reader
.getLibrary().export(
418 selectedBook
.getInfo().getMeta()
419 .getLuid(), type
, path
, pg
);
420 } catch (IOException e
) {
421 Instance
.getTraceHandler().error(e
);
434 * Create a {@link FileFilter} that accepts all files and return the given
442 private FileFilter
createAllFilter(final String desc
) {
443 return new FileFilter() {
445 public String
getDescription() {
450 public boolean accept(File f
) {
457 * Create the refresh (delete cache) menu item.
461 private JMenuItem
createMenuItemClearCache() {
462 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
463 refresh
.addActionListener(new ActionListener() {
465 public void actionPerformed(ActionEvent e
) {
466 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
467 if (selectedBook
!= null) {
468 mainPanel
.outOfUi(null, new Runnable() {
471 reader
.clearLocalReaderCache(selectedBook
.getInfo()
472 .getMeta().getLuid());
473 selectedBook
.setCached(false);
474 GuiReaderCoverImager
.clearIcon(selectedBook
476 SwingUtilities
.invokeLater(new Runnable() {
479 selectedBook
.repaint();
492 * Create the "move to" menu item.
495 * the library can be queried
499 private JMenuItem
createMenuItemMoveTo(boolean libOk
) {
500 JMenu changeTo
= new JMenu("Move to");
501 changeTo
.setMnemonic(KeyEvent
.VK_M
);
503 Map
<String
, List
<String
>> groupedSources
= new HashMap
<String
, List
<String
>>();
505 groupedSources
= reader
.getLibrary().getSourcesGrouped();
508 JMenuItem item
= new JMenuItem("New type...");
509 item
.addActionListener(createMoveAction(ChangeAction
.SOURCE
, null));
511 changeTo
.addSeparator();
513 for (final String type
: groupedSources
.keySet()) {
514 List
<String
> list
= groupedSources
.get(type
);
515 if (list
.size() == 1 && list
.get(0).isEmpty()) {
516 item
= new JMenuItem(type
);
517 item
.addActionListener(createMoveAction(ChangeAction
.SOURCE
,
521 JMenu dir
= new JMenu(type
);
522 for (String sub
: list
) {
523 // " " instead of "" for the visual height
524 String itemName
= sub
.isEmpty() ?
" " : sub
;
525 String actualType
= type
;
526 if (!sub
.isEmpty()) {
527 actualType
+= "/" + sub
;
530 item
= new JMenuItem(itemName
);
531 item
.addActionListener(createMoveAction(
532 ChangeAction
.SOURCE
, actualType
));
543 * Create the "set author" menu item.
546 * the library can be queried
550 private JMenuItem
createMenuItemSetAuthor(boolean libOk
) {
551 JMenu changeTo
= new JMenu("Set author");
552 changeTo
.setMnemonic(KeyEvent
.VK_A
);
555 JMenuItem newItem
= new JMenuItem("New author...");
556 changeTo
.add(newItem
);
557 changeTo
.addSeparator();
558 newItem
.addActionListener(createMoveAction(ChangeAction
.AUTHOR
, null));
562 Map
<String
, List
<String
>> groupedAuthors
= reader
.getLibrary()
563 .getAuthorsGrouped();
565 if (groupedAuthors
.size() > 1) {
566 for (String key
: groupedAuthors
.keySet()) {
567 JMenu group
= new JMenu(key
);
568 for (String value
: groupedAuthors
.get(key
)) {
569 JMenuItem item
= new JMenuItem(
570 value
.isEmpty() ?
"[unknown]" : value
);
571 item
.addActionListener(createMoveAction(
572 ChangeAction
.AUTHOR
, value
));
577 } else if (groupedAuthors
.size() == 1) {
578 for (String value
: groupedAuthors
.values().iterator().next()) {
579 JMenuItem item
= new JMenuItem(
580 value
.isEmpty() ?
"[unknown]" : value
);
581 item
.addActionListener(createMoveAction(
582 ChangeAction
.AUTHOR
, value
));
592 * Create the "rename" menu item.
595 * the library can be queried
599 private JMenuItem
createMenuItemRename(
600 @SuppressWarnings("unused") boolean libOk
) {
601 JMenuItem changeTo
= new JMenuItem("Rename...");
602 changeTo
.setMnemonic(KeyEvent
.VK_R
);
603 changeTo
.addActionListener(createMoveAction(ChangeAction
.TITLE
, null));
607 private ActionListener
createMoveAction(final ChangeAction what
,
609 return new ActionListener() {
611 public void actionPerformed(ActionEvent e
) {
612 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
613 if (selectedBook
!= null) {
614 String changeTo
= type
;
616 MetaData meta
= selectedBook
.getInfo().getMeta();
618 if (what
== ChangeAction
.SOURCE
) {
619 init
= meta
.getSource();
620 } else if (what
== ChangeAction
.TITLE
) {
621 init
= meta
.getTitle();
622 } else if (what
== ChangeAction
.AUTHOR
) {
623 init
= meta
.getAuthor();
626 Object rep
= JOptionPane
.showInputDialog(
627 GuiReaderFrame
.this, "Move to:",
628 "Moving story", JOptionPane
.QUESTION_MESSAGE
,
635 changeTo
= rep
.toString();
638 final String fChangeTo
= changeTo
;
639 mainPanel
.outOfUi(null, new Runnable() {
642 String luid
= selectedBook
.getInfo().getMeta()
644 if (what
== ChangeAction
.SOURCE
) {
645 reader
.changeSource(luid
, fChangeTo
);
646 } else if (what
== ChangeAction
.TITLE
) {
647 reader
.changeTitle(luid
, fChangeTo
);
648 } else if (what
== ChangeAction
.AUTHOR
) {
649 reader
.changeAuthor(luid
, fChangeTo
);
652 mainPanel
.unsetSelectedBook();
654 SwingUtilities
.invokeLater(new Runnable() {
668 * Create the re-download (then delete original) menu item.
672 private JMenuItem
createMenuItemRedownload() {
673 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
674 refresh
.addActionListener(new ActionListener() {
676 public void actionPerformed(ActionEvent e
) {
677 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
678 if (selectedBook
!= null) {
679 final MetaData meta
= selectedBook
.getInfo().getMeta();
680 mainPanel
.imprt(meta
.getUrl(), new StoryRunnable() {
682 public void run(Story story
) {
683 reader
.delete(meta
.getLuid());
684 mainPanel
.unsetSelectedBook();
685 MetaData newMeta
= story
.getMeta();
686 if (!newMeta
.getSource().equals(meta
.getSource())) {
687 reader
.changeSource(newMeta
.getLuid(),
691 }, "Removing old copy");
700 * Create the delete menu item.
704 private JMenuItem
createMenuItemDelete() {
705 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
706 delete
.addActionListener(new ActionListener() {
708 public void actionPerformed(ActionEvent e
) {
709 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
710 if (selectedBook
!= null) {
711 mainPanel
.outOfUi(null, new Runnable() {
714 reader
.delete(selectedBook
.getInfo().getMeta()
716 mainPanel
.unsetSelectedBook();
727 * Create the properties menu item.
731 private JMenuItem
createMenuItemProperties() {
732 JMenuItem delete
= new JMenuItem("Properties", KeyEvent
.VK_P
);
733 delete
.addActionListener(new ActionListener() {
735 public void actionPerformed(ActionEvent e
) {
736 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
737 if (selectedBook
!= null) {
738 mainPanel
.outOfUi(null, new Runnable() {
741 new GuiReaderPropertiesFrame(reader
, selectedBook
742 .getInfo()).setVisible(true);
753 * Create the open menu item for a book, a source/type or an author.
757 public JMenuItem
createMenuItemOpenBook() {
758 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
759 open
.addActionListener(new ActionListener() {
761 public void actionPerformed(ActionEvent e
) {
762 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
763 if (selectedBook
!= null) {
764 if (selectedBook
.getInfo().getMeta() == null) {
765 mainPanel
.removeBookPanes();
766 mainPanel
.addBookPane(selectedBook
.getInfo().getMainInfo(),
767 mainPanel
.getCurrentType());
768 mainPanel
.refreshBooks();
770 mainPanel
.openBook(selectedBook
);
780 * Create the SetCover menu item for a book to change the linked source
785 private JMenuItem
createMenuItemSetCoverForSource() {
786 JMenuItem open
= new JMenuItem("Set as cover for source", KeyEvent
.VK_C
);
787 open
.addActionListener(new ActionListener() {
789 public void actionPerformed(ActionEvent e
) {
790 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
791 if (selectedBook
!= null) {
792 BasicLibrary lib
= reader
.getLibrary();
793 String luid
= selectedBook
.getInfo().getMeta().getLuid();
794 String source
= selectedBook
.getInfo().getMeta()
797 lib
.setSourceCover(source
, luid
);
799 GuiReaderBookInfo sourceInfo
= GuiReaderBookInfo
800 .fromSource(lib
, source
);
801 GuiReaderCoverImager
.clearIcon(sourceInfo
);
810 * Create the SetCover menu item for a book to change the linked source
815 private JMenuItem
createMenuItemSetCoverForAuthor() {
816 JMenuItem open
= new JMenuItem("Set as cover for author", KeyEvent
.VK_A
);
817 open
.addActionListener(new ActionListener() {
819 public void actionPerformed(ActionEvent e
) {
820 final GuiReaderBook selectedBook
= mainPanel
.getSelectedBook();
821 if (selectedBook
!= null) {
822 BasicLibrary lib
= reader
.getLibrary();
823 String luid
= selectedBook
.getInfo().getMeta().getLuid();
824 String author
= selectedBook
.getInfo().getMeta()
827 lib
.setAuthorCover(author
, luid
);
829 GuiReaderBookInfo authorInfo
= GuiReaderBookInfo
830 .fromAuthor(lib
, author
);
831 GuiReaderCoverImager
.clearIcon(authorInfo
);
840 * Display an error message and log the linked {@link Exception}.
845 * the title of the error message
847 * the exception to log if any
849 public void error(final String message
, final String title
, Exception e
) {
850 Instance
.getTraceHandler().error(title
+ ": " + message
);
852 Instance
.getTraceHandler().error(e
);
855 SwingUtilities
.invokeLater(new Runnable() {
858 JOptionPane
.showMessageDialog(GuiReaderFrame
.this, message
,
859 title
, JOptionPane
.ERROR_MESSAGE
);
865 public GuiReader
getReader() {