Version 1.4.2
[fanfix.git] / src / be / nikiroo / fanfix / reader / LocalReaderFrame.java
1 package be.nikiroo.fanfix.reader;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Frame;
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;
13 import java.io.File;
14 import java.io.IOException;
15 import java.net.URL;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Map.Entry;
20
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;
34
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;
47
48 /**
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...
53 *
54 * @author niki
55 */
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;
61 private JPanel pane;
62 private Color color;
63 private ProgressBar pgBar;
64 private JMenuBar bar;
65 private LocalReaderBook selectedBook;
66 private boolean words; // words or authors (secondary info on books)
67
68 /**
69 * Create a new {@link LocalReaderFrame}.
70 *
71 * @param reader
72 * the associated {@link LocalReader} to forward some commands
73 * and access its {@link Library}
74 * @param type
75 * the type of {@link Story} to load, or NULL for all types
76 */
77 public LocalReaderFrame(LocalReader reader, String type) {
78 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
79
80 this.reader = reader;
81
82 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
83 setSize(800, 600);
84 setLayout(new BorderLayout());
85
86 pane = new JPanel();
87 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
88
89 color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR);
90 if (color != null) {
91 setBackground(color);
92 pane.setBackground(color);
93 }
94
95 JScrollPane scroll = new JScrollPane(pane);
96 scroll.getVerticalScrollBar().setUnitIncrement(16);
97 add(scroll, BorderLayout.CENTER);
98
99 pgBar = new ProgressBar();
100 add(pgBar, BorderLayout.SOUTH);
101
102 pgBar.addActionListener(new ActionListener() {
103 public void actionPerformed(ActionEvent e) {
104 invalidate();
105 pgBar.setProgress(null);
106 validate();
107 setEnabled(true);
108 }
109 });
110
111 pgBar.addUpdateListener(new ActionListener() {
112 public void actionPerformed(ActionEvent e) {
113 invalidate();
114 validate();
115 repaint();
116 }
117 });
118
119 booksByType = new HashMap<LocalReaderGroup, String>();
120 booksByAuthor = new HashMap<LocalReaderGroup, String>();
121
122 pane.setVisible(false);
123 final Progress pg = new Progress();
124 final String typeF = type;
125 outOfUi(pg, new Runnable() {
126 public void run() {
127 Instance.getLibrary().refresh(pg);
128 invalidate();
129 setJMenuBar(createMenu());
130 addBookPane(typeF, true);
131 refreshBooks();
132 validate();
133 pane.setVisible(true);
134 }
135 });
136
137 setVisible(true);
138 }
139
140 /**
141 * Add a new {@link LocalReaderGroup} on the frame to display the books of
142 * the selected type or author.
143 *
144 * @param value
145 * the author or the type
146 * @param type
147 * TRUE for type, FALSE for author
148 */
149 private void addBookPane(String value, boolean type) {
150 if (value == null) {
151 if (type) {
152 for (String tt : Instance.getLibrary().getTypes()) {
153 if (tt != null) {
154 addBookPane(tt, type);
155 }
156 }
157 } else {
158 for (String tt : Instance.getLibrary().getAuthors()) {
159 if (tt != null) {
160 addBookPane(tt, type);
161 }
162 }
163 }
164
165 return;
166 }
167
168 LocalReaderGroup bookPane = new LocalReaderGroup(reader, value, color);
169 if (type) {
170 booksByType.put(bookPane, value);
171 } else {
172 booksByAuthor.put(bookPane, value);
173 }
174
175 this.invalidate();
176 pane.invalidate();
177 pane.add(bookPane);
178 pane.validate();
179 this.validate();
180
181 bookPane.setActionListener(new BookActionListener() {
182 public void select(LocalReaderBook book) {
183 selectedBook = book;
184 }
185
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());
196 }
197
198 public void action(final LocalReaderBook book) {
199 openBook(book);
200 }
201 });
202 }
203
204 private void removeBookPanes() {
205 booksByType.clear();
206 booksByAuthor.clear();
207 pane.invalidate();
208 this.invalidate();
209 pane.removeAll();
210 pane.validate();
211 this.validate();
212 }
213
214 /**
215 * Refresh the list of {@link LocalReaderBook}s from disk.
216 *
217 */
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);
223 }
224
225 for (LocalReaderGroup group : booksByAuthor.keySet()) {
226 List<MetaData> stories = Instance.getLibrary().getListByAuthor(
227 booksByAuthor.get(group));
228 group.refreshBooks(stories, words);
229 }
230
231 pane.repaint();
232 this.repaint();
233 }
234
235 /**
236 * Create the main menu bar.
237 *
238 * @return the bar
239 */
240 private JMenuBar createMenu() {
241 bar = new JMenuBar();
242
243 JMenu file = new JMenu("File");
244 file.setMnemonic(KeyEvent.VK_F);
245
246 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
247 imprt.addActionListener(new ActionListener() {
248 public void actionPerformed(ActionEvent e) {
249 imprt(true);
250 }
251 });
252 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
253 imprtF.addActionListener(new ActionListener() {
254 public void actionPerformed(ActionEvent e) {
255 imprt(false);
256 }
257 });
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));
263 }
264 });
265
266 file.add(createMenuItemOpenBook());
267 file.add(createMenuItemExport());
268 file.addSeparator();
269 file.add(imprt);
270 file.add(imprtF);
271 file.addSeparator();
272 file.add(exit);
273
274 bar.add(file);
275
276 JMenu edit = new JMenu("Edit");
277 edit.setMnemonic(KeyEvent.VK_E);
278
279 edit.add(createMenuItemClearCache());
280 edit.add(createMenuItemRedownload());
281 edit.addSeparator();
282 edit.add(createMenuItemDelete());
283
284 bar.add(edit);
285
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) {
292 words = false;
293 refreshBooks();
294 }
295 });
296 view.add(vauthors);
297 JMenuItem vwords = new JMenuItem("Word count");
298 vwords.setMnemonic(KeyEvent.VK_W);
299 vwords.addActionListener(new ActionListener() {
300 public void actionPerformed(ActionEvent e) {
301 words = true;
302 refreshBooks();
303 }
304 });
305 view.add(vwords);
306 bar.add(view);
307
308 JMenu sources = new JMenu("Sources");
309 sources.setMnemonic(KeyEvent.VK_S);
310
311 List<String> tt = Instance.getLibrary().getTypes();
312 tt.add(0, null);
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) {
317 removeBookPanes();
318 addBookPane(type, true);
319 refreshBooks();
320 }
321 });
322 sources.add(item);
323
324 if (type == null) {
325 sources.addSeparator();
326 }
327 }
328
329 bar.add(sources);
330
331 JMenu authors = new JMenu("Authors");
332 authors.setMnemonic(KeyEvent.VK_A);
333
334 List<String> aa = Instance.getLibrary().getAuthors();
335 aa.add(0, null);
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) {
341 removeBookPanes();
342 addBookPane(author, false);
343 refreshBooks();
344 }
345 });
346 authors.add(item);
347
348 if (author == null || author.isEmpty()) {
349 authors.addSeparator();
350 }
351 }
352
353 bar.add(authors);
354
355 JMenu options = new JMenu("Options");
356 options.setMnemonic(KeyEvent.VK_O);
357 options.add(createMenuItemConfig());
358 options.add(createMenuItemUiConfig());
359 bar.add(options);
360
361 return bar;
362 }
363
364 /**
365 * Create the Fanfix Configuration menu item.
366 *
367 * @return the item
368 */
369 private JMenuItem createMenuItemConfig() {
370 final String title = "Fanfix Configuration";
371 JMenuItem item = new JMenuItem(title);
372 item.setMnemonic(KeyEvent.VK_F);
373
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);
380 frame.add(ed);
381 frame.setSize(800, 600);
382 frame.setVisible(true);
383 }
384 });
385
386 return item;
387 }
388
389 /**
390 * Create the UI Configuration menu item.
391 *
392 * @return the item
393 */
394 private JMenuItem createMenuItemUiConfig() {
395 final String title = "UI Configuration";
396 JMenuItem item = new JMenuItem(title);
397 item.setMnemonic(KeyEvent.VK_U);
398
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);
405 frame.add(ed);
406 frame.setSize(800, 600);
407 frame.setVisible(true);
408 }
409 });
410
411 return item;
412 }
413
414 /**
415 * Create the export menu item.
416 *
417 * @return the item
418 */
419 private JMenuItem createMenuItemExport() {
420 final JFileChooser fc = new JFileChooser();
421 fc.setAcceptAllFileFilterUsed(false);
422
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);
427
428 if (ext == null || ext.isEmpty()) {
429 filters.put(createAllFilter(desc), type);
430 } else {
431 filters.put(new FileNameExtensionFilter(desc, ext), type);
432 }
433 }
434
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());
439 }
440 }
441 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
442 if (entry.getKey() instanceof FileNameExtensionFilter) {
443 fc.addChoosableFileFilter(entry.getKey());
444 }
445 }
446 //
447
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()
456 .getAbsolutePath()
457 + type.getDefaultExtension(false);
458 final Progress pg = new Progress();
459 outOfUi(pg, new Runnable() {
460 public void run() {
461 try {
462 Instance.getLibrary().export(
463 selectedBook.getMeta().getLuid(),
464 type, path, pg);
465 } catch (IOException e) {
466 Instance.syserr(e);
467 }
468 }
469 });
470 }
471 }
472 }
473 });
474
475 return export;
476 }
477
478 /**
479 * Create a {@link FileFilter} that accepts all files and return the given
480 * description.
481 *
482 * @param desc
483 * the description
484 *
485 * @return the filter
486 */
487 private FileFilter createAllFilter(final String desc) {
488 return new FileFilter() {
489 @Override
490 public String getDescription() {
491 return desc;
492 }
493
494 @Override
495 public boolean accept(File f) {
496 return true;
497 }
498 };
499 }
500
501 /**
502 * Create the refresh (delete cache) menu item.
503 *
504 * @return the item
505 */
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() {
512 public void run() {
513 reader.clearLocalReaderCache(selectedBook.getMeta()
514 .getLuid());
515 selectedBook.setCached(false);
516 SwingUtilities.invokeLater(new Runnable() {
517 public void run() {
518 selectedBook.repaint();
519 }
520 });
521 }
522 });
523 }
524 }
525 });
526
527 return refresh;
528 }
529
530 /**
531 * Create the redownload (then delete original) menu item.
532 *
533 * @return the item
534 */
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() {
542 public void run() {
543 reader.delete(meta.getLuid());
544 LocalReaderFrame.this.selectedBook = null;
545 }
546 }, "Removing old copy");
547 }
548 }
549 });
550
551 return refresh;
552 }
553
554 /**
555 * Create the delete menu item.
556 *
557 * @return the item
558 */
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() {
565 public void run() {
566 reader.delete(selectedBook.getMeta().getLuid());
567 selectedBook = null;
568 }
569 });
570 }
571 }
572 });
573
574 return delete;
575 }
576
577 /**
578 * Create the open menu item.
579 *
580 * @return the item
581 */
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);
588 }
589 }
590 });
591
592 return open;
593 }
594
595 /**
596 * Open a {@link LocalReaderBook} item.
597 *
598 * @param book
599 * the {@link LocalReaderBook} to open
600 */
601 private void openBook(final LocalReaderBook book) {
602 final Progress pg = new Progress();
603 outOfUi(pg, new Runnable() {
604 public void run() {
605 try {
606 reader.open(book.getMeta().getLuid(), pg);
607 SwingUtilities.invokeLater(new Runnable() {
608 public void run() {
609 book.setCached(true);
610 }
611 });
612 } catch (IOException e) {
613 // TODO: error message?
614 Instance.syserr(e);
615 }
616 }
617 });
618 }
619
620 /**
621 * Process the given action out of the Swing UI thread and link the given
622 * {@link ProgressBar} to the action.
623 * <p>
624 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
625 * to done when the action is done.
626 *
627 * @param pg
628 * the {@link ProgressBar} or NULL
629 * @param run
630 * the action to run
631 */
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();
637 }
638
639 pg.addProgress(progress, 90);
640 pg.addProgress(reload, 10);
641
642 invalidate();
643 pgBar.setProgress(pg);
644 validate();
645 setEnabled(false);
646
647 new Thread(new Runnable() {
648 public void run() {
649 run.run();
650 refreshBooks();
651 reload.done();
652 if (!pg.isDone()) {
653 // will trigger pgBar ActionListener:
654 pg.done();
655 }
656 }
657 }, "outOfUi thread").start();
658 }
659
660 /**
661 * Import a {@link Story} into the main {@link Library}.
662 * <p>
663 * Should be called inside the UI thread.
664 *
665 * @param askUrl
666 * TRUE for an {@link URL}, false for a {@link File}
667 */
668 private void imprt(boolean askUrl) {
669 JFileChooser fc = new JFileChooser();
670
671 Object url;
672 if (askUrl) {
673 String clipboard = "";
674 try {
675 clipboard = ("" + Toolkit.getDefaultToolkit()
676 .getSystemClipboard().getData(DataFlavor.stringFlavor))
677 .trim();
678 } catch (Exception e) {
679 // No data will be handled
680 }
681
682 if (clipboard == null || !clipboard.startsWith("http")) {
683 clipboard = "";
684 }
685
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();
691 } else {
692 url = null;
693 }
694
695 if (url != null && !url.toString().isEmpty()) {
696 imprt(url.toString(), null, null);
697 }
698 }
699
700 /**
701 * Actually import the {@link Story} into the main {@link Library}.
702 * <p>
703 * Should be called inside the UI thread.
704 *
705 * @param url
706 * the {@link Story} to import by {@link URL}
707 * @param onSuccess
708 * Action to execute on success
709 */
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);
717
718 outOfUi(pg, new Runnable() {
719 public void run() {
720 Exception ex = null;
721 try {
722 Instance.getLibrary().imprt(BasicReader.getUrl(url),
723 pgImprt);
724 } catch (IOException e) {
725 ex = e;
726 }
727
728 final Exception e = ex;
729
730 final boolean ok = (e == null);
731
732 pgOnSuccess.setProgress(0);
733 if (!ok) {
734 Instance.syserr(e);
735 SwingUtilities.invokeLater(new Runnable() {
736 public void run() {
737 JOptionPane.showMessageDialog(
738 LocalReaderFrame.this, "Cannot import: "
739 + url, e.getMessage(),
740 JOptionPane.ERROR_MESSAGE);
741 }
742 });
743 } else {
744 if (onSuccess != null) {
745 onSuccess.run();
746 }
747 }
748 pgOnSuccess.done();
749 }
750 });
751 }
752
753 /**
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.
757 * <p>
758 * Disabling this component will also affect its children.
759 *
760 * @param b
761 * If <code>true</code>, this component is enabled; otherwise
762 * this component is disabled
763 */
764 @Override
765 public void setEnabled(boolean b) {
766 if (bar != null) {
767 bar.setEnabled(b);
768 }
769
770 for (LocalReaderGroup group : booksByType.keySet()) {
771 group.setEnabled(b);
772 }
773 for (LocalReaderGroup group : booksByAuthor.keySet()) {
774 group.setEnabled(b);
775 }
776 super.setEnabled(b);
777 repaint();
778 }
779 }