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
.HashMap
;
17 import java
.util
.List
;
19 import java
.util
.Map
.Entry
;
21 import javax
.swing
.BoxLayout
;
22 import javax
.swing
.JFileChooser
;
23 import javax
.swing
.JFrame
;
24 import javax
.swing
.JMenu
;
25 import javax
.swing
.JMenuBar
;
26 import javax
.swing
.JMenuItem
;
27 import javax
.swing
.JOptionPane
;
28 import javax
.swing
.JPanel
;
29 import javax
.swing
.JPopupMenu
;
30 import javax
.swing
.JScrollPane
;
31 import javax
.swing
.SwingUtilities
;
32 import javax
.swing
.filechooser
.FileFilter
;
33 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
35 import be
.nikiroo
.fanfix
.Instance
;
36 import be
.nikiroo
.fanfix
.Library
;
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
.output
.BasicOutput
.OutputType
;
42 import be
.nikiroo
.fanfix
.reader
.LocalReaderBook
.BookActionListener
;
43 import be
.nikiroo
.utils
.Progress
;
44 import be
.nikiroo
.utils
.Version
;
45 import be
.nikiroo
.utils
.ui
.ConfigEditor
;
46 import be
.nikiroo
.utils
.ui
.ProgressBar
;
49 * A {@link Frame} that will show a {@link LocalReaderBook} item for each
50 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
51 * way to copy them to the {@link LocalReader} cache ({@link LocalReader#lib}),
52 * read them, delete them...
56 class LocalReaderFrame
extends JFrame
{
57 private static final long serialVersionUID
= 1L;
58 private LocalReader reader
;
59 private Map
<LocalReaderGroup
, String
> booksByType
;
60 private Map
<LocalReaderGroup
, String
> booksByAuthor
;
63 private ProgressBar pgBar
;
65 private LocalReaderBook selectedBook
;
66 private boolean words
; // words or authors (secondary info on books)
69 * Create a new {@link LocalReaderFrame}.
72 * the associated {@link LocalReader} to forward some commands
73 * and access its {@link Library}
75 * the type of {@link Story} to load, or NULL for all types
77 public LocalReaderFrame(LocalReader reader
, String type
) {
78 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
82 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
84 setLayout(new BorderLayout());
87 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
89 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
92 pane
.setBackground(color
);
95 JScrollPane scroll
= new JScrollPane(pane
);
96 scroll
.getVerticalScrollBar().setUnitIncrement(16);
97 add(scroll
, BorderLayout
.CENTER
);
99 pgBar
= new ProgressBar();
100 add(pgBar
, BorderLayout
.SOUTH
);
102 pgBar
.addActionListener(new ActionListener() {
103 public void actionPerformed(ActionEvent e
) {
105 pgBar
.setProgress(null);
111 pgBar
.addUpdateListener(new ActionListener() {
112 public void actionPerformed(ActionEvent e
) {
119 booksByType
= new HashMap
<LocalReaderGroup
, String
>();
120 booksByAuthor
= new HashMap
<LocalReaderGroup
, String
>();
122 pane
.setVisible(false);
123 final Progress pg
= new Progress();
124 final String typeF
= type
;
125 outOfUi(pg
, new Runnable() {
127 Instance
.getLibrary().refresh(pg
);
129 setJMenuBar(createMenu());
130 addBookPane(typeF
, true);
133 pane
.setVisible(true);
141 * Add a new {@link LocalReaderGroup} on the frame to display the books of
142 * the selected type or author.
145 * the author or the type
147 * TRUE for type, FALSE for author
149 private void addBookPane(String value
, boolean type
) {
152 for (String tt
: Instance
.getLibrary().getTypes()) {
154 addBookPane(tt
, type
);
158 for (String tt
: Instance
.getLibrary().getAuthors()) {
160 addBookPane(tt
, type
);
168 LocalReaderGroup bookPane
= new LocalReaderGroup(reader
, value
, color
);
170 booksByType
.put(bookPane
, value
);
172 booksByAuthor
.put(bookPane
, value
);
181 bookPane
.setActionListener(new BookActionListener() {
182 public void select(LocalReaderBook book
) {
186 public void popupRequested(LocalReaderBook book
, MouseEvent e
) {
187 JPopupMenu popup
= new JPopupMenu();
188 popup
.add(createMenuItemOpenBook());
189 popup
.addSeparator();
190 popup
.add(createMenuItemExport());
191 popup
.add(createMenuItemClearCache());
192 popup
.add(createMenuItemRedownload());
193 popup
.addSeparator();
194 popup
.add(createMenuItemDelete());
195 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
198 public void action(final LocalReaderBook book
) {
204 private void removeBookPanes() {
206 booksByAuthor
.clear();
215 * Refresh the list of {@link LocalReaderBook}s from disk.
218 private void refreshBooks() {
219 for (LocalReaderGroup group
: booksByType
.keySet()) {
220 List
<MetaData
> stories
= Instance
.getLibrary().getListByType(
221 booksByType
.get(group
));
222 group
.refreshBooks(stories
, words
);
225 for (LocalReaderGroup group
: booksByAuthor
.keySet()) {
226 List
<MetaData
> stories
= Instance
.getLibrary().getListByAuthor(
227 booksByAuthor
.get(group
));
228 group
.refreshBooks(stories
, words
);
236 * Create the main menu bar.
240 private JMenuBar
createMenu() {
241 bar
= new JMenuBar();
243 JMenu file
= new JMenu("File");
244 file
.setMnemonic(KeyEvent
.VK_F
);
246 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
247 imprt
.addActionListener(new ActionListener() {
248 public void actionPerformed(ActionEvent e
) {
252 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
253 imprtF
.addActionListener(new ActionListener() {
254 public void actionPerformed(ActionEvent e
) {
258 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
259 exit
.addActionListener(new ActionListener() {
260 public void actionPerformed(ActionEvent e
) {
261 LocalReaderFrame
.this.dispatchEvent(new WindowEvent(
262 LocalReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
266 file
.add(createMenuItemOpenBook());
267 file
.add(createMenuItemExport());
276 JMenu edit
= new JMenu("Edit");
277 edit
.setMnemonic(KeyEvent
.VK_E
);
279 edit
.add(createMenuItemClearCache());
280 edit
.add(createMenuItemRedownload());
282 edit
.add(createMenuItemDelete());
286 JMenu view
= new JMenu("View");
287 view
.setMnemonic(KeyEvent
.VK_V
);
288 JMenuItem vauthors
= new JMenuItem("Author");
289 vauthors
.setMnemonic(KeyEvent
.VK_A
);
290 vauthors
.addActionListener(new ActionListener() {
291 public void actionPerformed(ActionEvent e
) {
297 JMenuItem vwords
= new JMenuItem("Word count");
298 vwords
.setMnemonic(KeyEvent
.VK_W
);
299 vwords
.addActionListener(new ActionListener() {
300 public void actionPerformed(ActionEvent e
) {
308 JMenu sources
= new JMenu("Sources");
309 sources
.setMnemonic(KeyEvent
.VK_S
);
311 List
<String
> tt
= Instance
.getLibrary().getTypes();
313 for (final String type
: tt
) {
314 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
315 item
.addActionListener(new ActionListener() {
316 public void actionPerformed(ActionEvent e
) {
318 addBookPane(type
, true);
325 sources
.addSeparator();
331 JMenu authors
= new JMenu("Authors");
332 authors
.setMnemonic(KeyEvent
.VK_A
);
334 List
<String
> aa
= Instance
.getLibrary().getAuthors();
336 for (final String author
: aa
) {
337 JMenuItem item
= new JMenuItem(author
== null ?
"All"
338 : author
.isEmpty() ?
"[unknown]" : author
);
339 item
.addActionListener(new ActionListener() {
340 public void actionPerformed(ActionEvent e
) {
342 addBookPane(author
, false);
348 if (author
== null || author
.isEmpty()) {
349 authors
.addSeparator();
355 JMenu options
= new JMenu("Options");
356 options
.setMnemonic(KeyEvent
.VK_O
);
357 options
.add(createMenuItemConfig());
358 options
.add(createMenuItemUiConfig());
365 * Create the Fanfix Configuration menu item.
369 private JMenuItem
createMenuItemConfig() {
370 final String title
= "Fanfix Configuration";
371 JMenuItem item
= new JMenuItem(title
);
372 item
.setMnemonic(KeyEvent
.VK_F
);
374 item
.addActionListener(new ActionListener() {
375 public void actionPerformed(ActionEvent e
) {
376 ConfigEditor
<Config
> ed
= new ConfigEditor
<Config
>(
377 Config
.class, Instance
.getConfig(),
378 "This is where you configure the options of the program.");
379 JFrame frame
= new JFrame(title
);
381 frame
.setSize(800, 600);
382 frame
.setVisible(true);
390 * Create the UI Configuration menu item.
394 private JMenuItem
createMenuItemUiConfig() {
395 final String title
= "UI Configuration";
396 JMenuItem item
= new JMenuItem(title
);
397 item
.setMnemonic(KeyEvent
.VK_U
);
399 item
.addActionListener(new ActionListener() {
400 public void actionPerformed(ActionEvent e
) {
401 ConfigEditor
<UiConfig
> ed
= new ConfigEditor
<UiConfig
>(
402 UiConfig
.class, Instance
.getUiConfig(),
403 "This is where you configure the graphical appearence of the program.");
404 JFrame frame
= new JFrame(title
);
406 frame
.setSize(800, 600);
407 frame
.setVisible(true);
415 * Create the export menu item.
419 private JMenuItem
createMenuItemExport() {
420 final JFileChooser fc
= new JFileChooser();
421 fc
.setAcceptAllFileFilterUsed(false);
423 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
424 for (OutputType type
: OutputType
.values()) {
425 String ext
= type
.getDefaultExtension(false);
426 String desc
= type
.getDesc(false);
428 if (ext
== null || ext
.isEmpty()) {
429 filters
.put(createAllFilter(desc
), type
);
431 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
435 // First the "ALL" filters, then, the extension filters
436 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
437 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
438 fc
.addChoosableFileFilter(entry
.getKey());
441 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
442 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
443 fc
.addChoosableFileFilter(entry
.getKey());
448 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
449 export
.addActionListener(new ActionListener() {
450 public void actionPerformed(ActionEvent e
) {
451 if (selectedBook
!= null) {
452 fc
.showDialog(LocalReaderFrame
.this, "Save");
453 if (fc
.getSelectedFile() != null) {
454 final OutputType type
= filters
.get(fc
.getFileFilter());
455 final String path
= fc
.getSelectedFile()
457 + type
.getDefaultExtension(false);
458 final Progress pg
= new Progress();
459 outOfUi(pg
, new Runnable() {
462 Instance
.getLibrary().export(
463 selectedBook
.getMeta().getLuid(),
465 } catch (IOException e
) {
479 * Create a {@link FileFilter} that accepts all files and return the given
487 private FileFilter
createAllFilter(final String desc
) {
488 return new FileFilter() {
490 public String
getDescription() {
495 public boolean accept(File f
) {
502 * Create the refresh (delete cache) menu item.
506 private JMenuItem
createMenuItemClearCache() {
507 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
508 refresh
.addActionListener(new ActionListener() {
509 public void actionPerformed(ActionEvent e
) {
510 if (selectedBook
!= null) {
511 outOfUi(null, new Runnable() {
513 reader
.clearLocalReaderCache(selectedBook
.getMeta()
515 selectedBook
.setCached(false);
516 SwingUtilities
.invokeLater(new Runnable() {
518 selectedBook
.repaint();
531 * Create the redownload (then delete original) menu item.
535 private JMenuItem
createMenuItemRedownload() {
536 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
537 refresh
.addActionListener(new ActionListener() {
538 public void actionPerformed(ActionEvent e
) {
539 if (selectedBook
!= null) {
540 final MetaData meta
= selectedBook
.getMeta();
541 imprt(meta
.getUrl(), new Runnable() {
543 reader
.delete(meta
.getLuid());
544 LocalReaderFrame
.this.selectedBook
= null;
546 }, "Removing old copy");
555 * Create the delete menu item.
559 private JMenuItem
createMenuItemDelete() {
560 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
561 delete
.addActionListener(new ActionListener() {
562 public void actionPerformed(ActionEvent e
) {
563 if (selectedBook
!= null) {
564 outOfUi(null, new Runnable() {
566 reader
.delete(selectedBook
.getMeta().getLuid());
578 * Create the open menu item.
582 private JMenuItem
createMenuItemOpenBook() {
583 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
584 open
.addActionListener(new ActionListener() {
585 public void actionPerformed(ActionEvent e
) {
586 if (selectedBook
!= null) {
587 openBook(selectedBook
);
596 * Open a {@link LocalReaderBook} item.
599 * the {@link LocalReaderBook} to open
601 private void openBook(final LocalReaderBook book
) {
602 final Progress pg
= new Progress();
603 outOfUi(pg
, new Runnable() {
606 reader
.open(book
.getMeta().getLuid(), pg
);
607 SwingUtilities
.invokeLater(new Runnable() {
609 book
.setCached(true);
612 } catch (IOException e
) {
613 // TODO: error message?
621 * Process the given action out of the Swing UI thread and link the given
622 * {@link ProgressBar} to the action.
624 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
625 * to done when the action is done.
628 * the {@link ProgressBar} or NULL
632 private void outOfUi(Progress progress
, final Runnable run
) {
633 final Progress pg
= new Progress();
634 final Progress reload
= new Progress("Reload books");
635 if (progress
== null) {
636 progress
= new Progress();
639 pg
.addProgress(progress
, 90);
640 pg
.addProgress(reload
, 10);
643 pgBar
.setProgress(pg
);
647 new Thread(new Runnable() {
653 // will trigger pgBar ActionListener:
657 }, "outOfUi thread").start();
661 * Import a {@link Story} into the main {@link Library}.
663 * Should be called inside the UI thread.
666 * TRUE for an {@link URL}, false for a {@link File}
668 private void imprt(boolean askUrl
) {
669 JFileChooser fc
= new JFileChooser();
673 String clipboard
= "";
675 clipboard
= ("" + Toolkit
.getDefaultToolkit()
676 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
678 } catch (Exception e
) {
679 // No data will be handled
682 if (clipboard
== null || !clipboard
.startsWith("http")) {
686 url
= JOptionPane
.showInputDialog(LocalReaderFrame
.this,
687 "url of the story to import?", "Importing from URL",
688 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
689 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
690 url
= fc
.getSelectedFile().getAbsolutePath();
695 if (url
!= null && !url
.toString().isEmpty()) {
696 imprt(url
.toString(), null, null);
701 * Actually import the {@link Story} into the main {@link Library}.
703 * Should be called inside the UI thread.
706 * the {@link Story} to import by {@link URL}
708 * Action to execute on success
710 private void imprt(final String url
, final Runnable onSuccess
,
711 String onSuccessPgName
) {
712 final Progress pg
= new Progress();
713 final Progress pgImprt
= new Progress();
714 final Progress pgOnSuccess
= new Progress(onSuccessPgName
);
715 pg
.addProgress(pgImprt
, 95);
716 pg
.addProgress(pgOnSuccess
, 5);
718 outOfUi(pg
, new Runnable() {
722 Instance
.getLibrary().imprt(BasicReader
.getUrl(url
),
724 } catch (IOException e
) {
728 final Exception e
= ex
;
730 final boolean ok
= (e
== null);
732 pgOnSuccess
.setProgress(0);
735 SwingUtilities
.invokeLater(new Runnable() {
737 JOptionPane
.showMessageDialog(
738 LocalReaderFrame
.this, "Cannot import: "
739 + url
, e
.getMessage(),
740 JOptionPane
.ERROR_MESSAGE
);
744 if (onSuccess
!= null) {
754 * Enables or disables this component, depending on the value of the
755 * parameter <code>b</code>. An enabled component can respond to user input
756 * and generate events. Components are enabled initially by default.
758 * Disabling this component will also affect its children.
761 * If <code>true</code>, this component is enabled; otherwise
762 * this component is disabled
765 public void setEnabled(boolean b
) {
770 for (LocalReaderGroup group
: booksByType
.keySet()) {
773 for (LocalReaderGroup group
: booksByAuthor
.keySet()) {