TODO update, gui: jdoc + small rename
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderFrame.java
1 package be.nikiroo.fanfix.reader.ui;
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.net.UnknownHostException;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22
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;
38
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.BasicReader;
49 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
50 import be.nikiroo.utils.Progress;
51 import be.nikiroo.utils.Version;
52 import be.nikiroo.utils.ui.ConfigEditor;
53 import be.nikiroo.utils.ui.ProgressBar;
54
55 /**
56 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
57 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
58 * way to copy them to the {@link GuiReader} cache (
59 * {@link BasicReader#getLibrary()}), read them, delete them...
60 *
61 * @author niki
62 */
63 class GuiReaderFrame extends JFrame {
64 private static final long serialVersionUID = 1L;
65 private GuiReader reader;
66 private Map<GuiReaderGroup, String> booksByType;
67 private Map<GuiReaderGroup, String> booksByAuthor;
68 private JPanel pane;
69 private Color color;
70 private ProgressBar pgBar;
71 private JMenuBar bar;
72 private GuiReaderBook selectedBook;
73 private boolean words; // words or authors (secondary info on books)
74
75 /**
76 * A {@link Runnable} with a {@link Story} parameter.
77 *
78 * @author niki
79 */
80 private interface StoryRunnable {
81 /**
82 * Run the action.
83 *
84 * @param story
85 * the story
86 */
87 public void run(Story story);
88 }
89
90 /**
91 * Create a new {@link GuiReaderFrame}.
92 *
93 * @param reader
94 * the associated {@link GuiReader} to forward some commands and
95 * access its {@link LocalLibrary}
96 * @param type
97 * the type of {@link Story} to load, or NULL for all types
98 */
99 public GuiReaderFrame(GuiReader reader, String type) {
100 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
101
102 this.reader = reader;
103
104 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
105 setSize(800, 600);
106 setLayout(new BorderLayout());
107
108 pane = new JPanel();
109 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
110
111 Integer icolor = Instance.getUiConfig().getColor(
112 UiConfig.BACKGROUND_COLOR);
113 if (icolor != null) {
114 color = new Color(icolor);
115 setBackground(color);
116 pane.setBackground(color);
117 }
118
119 JScrollPane scroll = new JScrollPane(pane);
120 scroll.getVerticalScrollBar().setUnitIncrement(16);
121 add(scroll, BorderLayout.CENTER);
122
123 String message = reader.getLibrary().getLibraryName();
124 if (!message.isEmpty()) {
125 JLabel name = new JLabel(message, SwingConstants.CENTER);
126 add(name, BorderLayout.NORTH);
127 }
128
129 pgBar = new ProgressBar();
130 add(pgBar, BorderLayout.SOUTH);
131
132 pgBar.addActionListener(new ActionListener() {
133 @Override
134 public void actionPerformed(ActionEvent e) {
135 invalidate();
136 pgBar.setProgress(null);
137 validate();
138 setEnabled(true);
139 }
140 });
141
142 pgBar.addUpdateListener(new ActionListener() {
143 @Override
144 public void actionPerformed(ActionEvent e) {
145 invalidate();
146 validate();
147 repaint();
148 }
149 });
150
151 booksByType = new HashMap<GuiReaderGroup, String>();
152 booksByAuthor = new HashMap<GuiReaderGroup, String>();
153
154 pane.setVisible(false);
155 final Progress pg = new Progress();
156 final String typeF = type;
157 outOfUi(pg, new Runnable() {
158 @Override
159 public void run() {
160 BasicLibrary lib = GuiReaderFrame.this.reader.getLibrary();
161 Status status = lib.getStatus();
162
163 if (status == Status.READY) {
164 lib.refresh(pg);
165 invalidate();
166 setJMenuBar(createMenu(true));
167 addBookPane(typeF, true);
168 refreshBooks();
169 validate();
170 pane.setVisible(true);
171 } else {
172 invalidate();
173 setJMenuBar(createMenu(false));
174 validate();
175
176 String err = lib.getLibraryName() + "\n";
177 switch (status) {
178 case INVALID:
179 err += "Library not valid";
180 break;
181
182 case UNAUTORIZED:
183 err += "You are not allowed to access this library";
184 break;
185
186 case UNAVAILABLE:
187 err += "Library currently unavailable";
188 break;
189
190 default:
191 err += "An error occured when contacting the library";
192 break;
193 }
194
195 error(err, "Library error", null);
196 }
197 }
198 });
199
200 setVisible(true);
201 }
202
203 private void addSourcePanes() {
204 // Sources -> i18n
205 GuiReaderGroup bookPane = new GuiReaderGroup(reader, "Sources", color);
206
207 List<MetaData> sources = new ArrayList<MetaData>();
208 for (String source : reader.getLibrary().getSources()) {
209 MetaData mSource = new MetaData();
210 mSource.setLuid(null);
211 mSource.setTitle(source);
212 mSource.setSource(source);
213 sources.add(mSource);
214 }
215
216 bookPane.refreshBooks(sources, false);
217
218 this.invalidate();
219 pane.invalidate();
220 pane.add(bookPane);
221 pane.validate();
222 this.validate();
223
224 bookPane.setActionListener(new BookActionListener() {
225 @Override
226 public void select(GuiReaderBook book) {
227 selectedBook = book;
228 }
229
230 @Override
231 public void popupRequested(GuiReaderBook book, MouseEvent e) {
232 JPopupMenu popup = new JPopupMenu();
233 popup.add(createMenuItemOpenBook());
234 popup.show(e.getComponent(), e.getX(), e.getY());
235 }
236
237 @Override
238 public void action(final GuiReaderBook book) {
239 removeBookPanes();
240 addBookPane(book.getMeta().getSource(), true);
241 refreshBooks();
242 }
243 });
244 }
245
246 /**
247 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
248 * selected type or author.
249 *
250 * @param value
251 * the author or the type, or NULL to get all the
252 * authors-or-types
253 * @param type
254 * TRUE for type, FALSE for author
255 */
256 private void addBookPane(String value, boolean type) {
257 if (value == null) {
258 if (type) {
259 if (Instance.getUiConfig().getBoolean(UiConfig.SOURCE_PAGE,
260 false)) {
261 addSourcePanes();
262 } else {
263 for (String tt : reader.getLibrary().getSources()) {
264 if (tt != null) {
265 addBookPane(tt, type);
266 }
267 }
268 }
269 } else {
270 for (String tt : reader.getLibrary().getAuthors()) {
271 if (tt != null) {
272 addBookPane(tt, type);
273 }
274 }
275 }
276
277 return;
278 }
279
280 GuiReaderGroup bookPane = new GuiReaderGroup(reader, value, color);
281 if (type) {
282 booksByType.put(bookPane, value);
283 } else {
284 booksByAuthor.put(bookPane, value);
285 }
286
287 this.invalidate();
288 pane.invalidate();
289 pane.add(bookPane);
290 pane.validate();
291 this.validate();
292
293 bookPane.setActionListener(new BookActionListener() {
294 @Override
295 public void select(GuiReaderBook book) {
296 selectedBook = book;
297 }
298
299 @Override
300 public void popupRequested(GuiReaderBook book, MouseEvent e) {
301 JPopupMenu popup = new JPopupMenu();
302 popup.add(createMenuItemOpenBook());
303 popup.addSeparator();
304 popup.add(createMenuItemExport());
305 popup.add(createMenuItemMove(true));
306 popup.add(createMenuItemSetCover());
307 popup.add(createMenuItemClearCache());
308 popup.add(createMenuItemRedownload());
309 popup.addSeparator();
310 popup.add(createMenuItemDelete());
311 popup.show(e.getComponent(), e.getX(), e.getY());
312 }
313
314 @Override
315 public void action(final GuiReaderBook book) {
316 openBook(book);
317 }
318 });
319 }
320
321 private void removeBookPanes() {
322 booksByType.clear();
323 booksByAuthor.clear();
324 pane.invalidate();
325 this.invalidate();
326 pane.removeAll();
327 pane.validate();
328 this.validate();
329 }
330
331 /**
332 * Refresh the list of {@link GuiReaderBook}s from disk.
333 */
334 private void refreshBooks() {
335 for (GuiReaderGroup group : booksByType.keySet()) {
336 List<MetaData> stories = reader.getLibrary().getListBySource(
337 booksByType.get(group));
338 group.refreshBooks(stories, words);
339 }
340
341 for (GuiReaderGroup group : booksByAuthor.keySet()) {
342 List<MetaData> stories = reader.getLibrary().getListByAuthor(
343 booksByAuthor.get(group));
344 group.refreshBooks(stories, words);
345 }
346
347 pane.repaint();
348 this.repaint();
349 }
350
351 /**
352 * Create the main menu bar.
353 *
354 * @param libOk
355 * the library can be queried
356 *
357 * @return the bar
358 */
359 private JMenuBar createMenu(boolean libOk) {
360 bar = new JMenuBar();
361
362 JMenu file = new JMenu("File");
363 file.setMnemonic(KeyEvent.VK_F);
364
365 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
366 imprt.addActionListener(new ActionListener() {
367 @Override
368 public void actionPerformed(ActionEvent e) {
369 imprt(true);
370 }
371 });
372 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
373 imprtF.addActionListener(new ActionListener() {
374 @Override
375 public void actionPerformed(ActionEvent e) {
376 imprt(false);
377 }
378 });
379 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
380 exit.addActionListener(new ActionListener() {
381 @Override
382 public void actionPerformed(ActionEvent e) {
383 GuiReaderFrame.this.dispatchEvent(new WindowEvent(
384 GuiReaderFrame.this, WindowEvent.WINDOW_CLOSING));
385 }
386 });
387
388 file.add(createMenuItemOpenBook());
389 file.add(createMenuItemExport());
390 file.add(createMenuItemMove(libOk));
391 file.addSeparator();
392 file.add(imprt);
393 file.add(imprtF);
394 file.addSeparator();
395 file.add(exit);
396
397 bar.add(file);
398
399 JMenu edit = new JMenu("Edit");
400 edit.setMnemonic(KeyEvent.VK_E);
401
402 edit.add(createMenuItemClearCache());
403 edit.add(createMenuItemRedownload());
404 edit.addSeparator();
405 edit.add(createMenuItemDelete());
406
407 bar.add(edit);
408
409 JMenu view = new JMenu("View");
410 view.setMnemonic(KeyEvent.VK_V);
411 JMenuItem vauthors = new JMenuItem("Author");
412 vauthors.setMnemonic(KeyEvent.VK_A);
413 vauthors.addActionListener(new ActionListener() {
414 @Override
415 public void actionPerformed(ActionEvent e) {
416 words = false;
417 refreshBooks();
418 }
419 });
420 view.add(vauthors);
421 JMenuItem vwords = new JMenuItem("Word count");
422 vwords.setMnemonic(KeyEvent.VK_W);
423 vwords.addActionListener(new ActionListener() {
424 @Override
425 public void actionPerformed(ActionEvent e) {
426 words = true;
427 refreshBooks();
428 }
429 });
430 view.add(vwords);
431 bar.add(view);
432
433 JMenu sources = new JMenu("Sources");
434 sources.setMnemonic(KeyEvent.VK_S);
435
436 List<String> tt = new ArrayList<String>();
437 if (libOk) {
438 tt.addAll(reader.getLibrary().getSources());
439 }
440 tt.add(0, null);
441
442 for (final String type : tt) {
443 JMenuItem item = new JMenuItem(type == null ? "All" : type);
444 item.addActionListener(new ActionListener() {
445 @Override
446 public void actionPerformed(ActionEvent e) {
447 removeBookPanes();
448 addBookPane(type, true);
449 refreshBooks();
450 }
451 });
452 sources.add(item);
453
454 if (type == null) {
455 sources.addSeparator();
456 }
457 }
458
459 bar.add(sources);
460
461 JMenu authors = new JMenu("Authors");
462 authors.setMnemonic(KeyEvent.VK_A);
463
464 List<String> aa = new ArrayList<String>();
465 if (libOk) {
466 aa.addAll(reader.getLibrary().getAuthors());
467 }
468 aa.add(0, null);
469 for (final String author : aa) {
470 JMenuItem item = new JMenuItem(author == null ? "All"
471 : author.isEmpty() ? "[unknown]" : author);
472 item.addActionListener(new ActionListener() {
473 @Override
474 public void actionPerformed(ActionEvent e) {
475 removeBookPanes();
476 addBookPane(author, false);
477 refreshBooks();
478 }
479 });
480 authors.add(item);
481
482 if (author == null || author.isEmpty()) {
483 authors.addSeparator();
484 }
485 }
486
487 bar.add(authors);
488
489 JMenu options = new JMenu("Options");
490 options.setMnemonic(KeyEvent.VK_O);
491 options.add(createMenuItemConfig());
492 options.add(createMenuItemUiConfig());
493 bar.add(options);
494
495 return bar;
496 }
497
498 /**
499 * Create the Fanfix Configuration menu item.
500 *
501 * @return the item
502 */
503 private JMenuItem createMenuItemConfig() {
504 final String title = "Fanfix Configuration";
505 JMenuItem item = new JMenuItem(title);
506 item.setMnemonic(KeyEvent.VK_F);
507
508 item.addActionListener(new ActionListener() {
509 @Override
510 public void actionPerformed(ActionEvent e) {
511 ConfigEditor<Config> ed = new ConfigEditor<Config>(
512 Config.class, Instance.getConfig(),
513 "This is where you configure the options of the program.");
514 JFrame frame = new JFrame(title);
515 frame.add(ed);
516 frame.setSize(800, 600);
517 frame.setVisible(true);
518 }
519 });
520
521 return item;
522 }
523
524 /**
525 * Create the UI Configuration menu item.
526 *
527 * @return the item
528 */
529 private JMenuItem createMenuItemUiConfig() {
530 final String title = "UI Configuration";
531 JMenuItem item = new JMenuItem(title);
532 item.setMnemonic(KeyEvent.VK_U);
533
534 item.addActionListener(new ActionListener() {
535 @Override
536 public void actionPerformed(ActionEvent e) {
537 ConfigEditor<UiConfig> ed = new ConfigEditor<UiConfig>(
538 UiConfig.class, Instance.getUiConfig(),
539 "This is where you configure the graphical appearence of the program.");
540 JFrame frame = new JFrame(title);
541 frame.add(ed);
542 frame.setSize(800, 600);
543 frame.setVisible(true);
544 }
545 });
546
547 return item;
548 }
549
550 /**
551 * Create the export menu item.
552 *
553 * @return the item
554 */
555 private JMenuItem createMenuItemExport() {
556 final JFileChooser fc = new JFileChooser();
557 fc.setAcceptAllFileFilterUsed(false);
558
559 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
560 for (OutputType type : OutputType.values()) {
561 String ext = type.getDefaultExtension(false);
562 String desc = type.getDesc(false);
563
564 if (ext == null || ext.isEmpty()) {
565 filters.put(createAllFilter(desc), type);
566 } else {
567 filters.put(new FileNameExtensionFilter(desc, ext), type);
568 }
569 }
570
571 // First the "ALL" filters, then, the extension filters
572 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
573 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
574 fc.addChoosableFileFilter(entry.getKey());
575 }
576 }
577 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
578 if (entry.getKey() instanceof FileNameExtensionFilter) {
579 fc.addChoosableFileFilter(entry.getKey());
580 }
581 }
582 //
583
584 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
585 export.addActionListener(new ActionListener() {
586 @Override
587 public void actionPerformed(ActionEvent e) {
588 if (selectedBook != null) {
589 fc.showDialog(GuiReaderFrame.this, "Save");
590 if (fc.getSelectedFile() != null) {
591 final OutputType type = filters.get(fc.getFileFilter());
592 final String path = fc.getSelectedFile()
593 .getAbsolutePath()
594 + type.getDefaultExtension(false);
595 final Progress pg = new Progress();
596 outOfUi(pg, new Runnable() {
597 @Override
598 public void run() {
599 try {
600 reader.getLibrary().export(
601 selectedBook.getMeta().getLuid(),
602 type, path, pg);
603 } catch (IOException e) {
604 Instance.getTraceHandler().error(e);
605 }
606 }
607 });
608 }
609 }
610 }
611 });
612
613 return export;
614 }
615
616 /**
617 * Create a {@link FileFilter} that accepts all files and return the given
618 * description.
619 *
620 * @param desc
621 * the description
622 *
623 * @return the filter
624 */
625 private FileFilter createAllFilter(final String desc) {
626 return new FileFilter() {
627 @Override
628 public String getDescription() {
629 return desc;
630 }
631
632 @Override
633 public boolean accept(File f) {
634 return true;
635 }
636 };
637 }
638
639 /**
640 * Create the refresh (delete cache) menu item.
641 *
642 * @return the item
643 */
644 private JMenuItem createMenuItemClearCache() {
645 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
646 refresh.addActionListener(new ActionListener() {
647 @Override
648 public void actionPerformed(ActionEvent e) {
649 if (selectedBook != null) {
650 outOfUi(null, new Runnable() {
651 @Override
652 public void run() {
653 reader.clearLocalReaderCache(selectedBook.getMeta()
654 .getLuid());
655 selectedBook.setCached(false);
656 GuiReaderBook.clearIcon(selectedBook.getMeta());
657 SwingUtilities.invokeLater(new Runnable() {
658 @Override
659 public void run() {
660 selectedBook.repaint();
661 }
662 });
663 }
664 });
665 }
666 }
667 });
668
669 return refresh;
670 }
671
672 /**
673 * Create the delete menu item.
674 *
675 * @param libOk
676 * the library can be queried
677 *
678 * @return the item
679 */
680 private JMenuItem createMenuItemMove(boolean libOk) {
681 JMenu moveTo = new JMenu("Move to...");
682 moveTo.setMnemonic(KeyEvent.VK_M);
683
684 List<String> types = new ArrayList<String>();
685 types.add(null);
686 if (libOk) {
687 types.addAll(reader.getLibrary().getSources());
688 }
689
690 for (String type : types) {
691 JMenuItem item = new JMenuItem(type == null ? "New type..." : type);
692
693 moveTo.add(item);
694 if (type == null) {
695 moveTo.addSeparator();
696 }
697
698 final String ftype = type;
699 item.addActionListener(new ActionListener() {
700 @Override
701 public void actionPerformed(ActionEvent e) {
702 if (selectedBook != null) {
703 String type = ftype;
704 if (type == null) {
705 Object rep = JOptionPane.showInputDialog(
706 GuiReaderFrame.this, "Move to:",
707 "Moving story",
708 JOptionPane.QUESTION_MESSAGE, null, null,
709 selectedBook.getMeta().getSource());
710
711 if (rep == null) {
712 return;
713 }
714
715 type = rep.toString();
716 }
717
718 final String ftype = type;
719 outOfUi(null, new Runnable() {
720 @Override
721 public void run() {
722 reader.changeSource(selectedBook.getMeta()
723 .getLuid(), ftype);
724
725 selectedBook = null;
726
727 SwingUtilities.invokeLater(new Runnable() {
728 @Override
729 public void run() {
730 setJMenuBar(createMenu(true));
731 }
732 });
733 }
734 });
735 }
736 }
737 });
738 }
739
740 return moveTo;
741 }
742
743 /**
744 * Create the redownload (then delete original) menu item.
745 *
746 * @return the item
747 */
748 private JMenuItem createMenuItemRedownload() {
749 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
750 refresh.addActionListener(new ActionListener() {
751 @Override
752 public void actionPerformed(ActionEvent e) {
753 if (selectedBook != null) {
754 final MetaData meta = selectedBook.getMeta();
755 imprt(meta.getUrl(), new StoryRunnable() {
756 @Override
757 public void run(Story story) {
758 reader.delete(meta.getLuid());
759 GuiReaderFrame.this.selectedBook = null;
760 MetaData newMeta = story.getMeta();
761 if (!newMeta.getSource().equals(meta.getSource())) {
762 reader.changeSource(newMeta.getLuid(),
763 meta.getSource());
764 }
765 }
766 }, "Removing old copy");
767 }
768 }
769 });
770
771 return refresh;
772 }
773
774 /**
775 * Create the delete menu item.
776 *
777 * @return the item
778 */
779 private JMenuItem createMenuItemDelete() {
780 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
781 delete.addActionListener(new ActionListener() {
782 @Override
783 public void actionPerformed(ActionEvent e) {
784 if (selectedBook != null) {
785 outOfUi(null, new Runnable() {
786 @Override
787 public void run() {
788 reader.delete(selectedBook.getMeta().getLuid());
789 selectedBook = null;
790 }
791 });
792 }
793 }
794 });
795
796 return delete;
797 }
798
799 /**
800 * Create the open menu item for a book or a source (no LUID).
801 *
802 * @return the item
803 */
804 private JMenuItem createMenuItemOpenBook() {
805 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
806 open.addActionListener(new ActionListener() {
807 @Override
808 public void actionPerformed(ActionEvent e) {
809 if (selectedBook != null) {
810 if (selectedBook.getMeta().getLuid() == null) {
811 removeBookPanes();
812 addBookPane(selectedBook.getMeta().getSource(), true);
813 refreshBooks();
814 } else {
815 openBook(selectedBook);
816 }
817 }
818 }
819 });
820
821 return open;
822 }
823
824 /**
825 * Create the SetCover menu item for a book to change the linked source
826 * cover.
827 *
828 * @return the item
829 */
830 private JMenuItem createMenuItemSetCover() {
831 JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
832 open.addActionListener(new ActionListener() {
833 @Override
834 public void actionPerformed(ActionEvent e) {
835 if (selectedBook != null) {
836 reader.getLibrary().setSourceCover(
837 selectedBook.getMeta().getSource(),
838 selectedBook.getMeta().getLuid());
839 MetaData source = selectedBook.getMeta().clone();
840 source.setLuid(null);
841 GuiReaderBook.clearIcon(source);
842 }
843 }
844 });
845
846 return open;
847 }
848
849 /**
850 * Open a {@link GuiReaderBook} item.
851 *
852 * @param book
853 * the {@link GuiReaderBook} to open
854 */
855 private void openBook(final GuiReaderBook book) {
856 final Progress pg = new Progress();
857 outOfUi(pg, new Runnable() {
858 @Override
859 public void run() {
860 try {
861 reader.read(book.getMeta().getLuid(), pg);
862 SwingUtilities.invokeLater(new Runnable() {
863 @Override
864 public void run() {
865 book.setCached(true);
866 }
867 });
868 } catch (IOException e) {
869 // TODO: error message?
870 Instance.getTraceHandler().error(e);
871 }
872 }
873 });
874 }
875
876 /**
877 * Process the given action out of the Swing UI thread and link the given
878 * {@link ProgressBar} to the action.
879 * <p>
880 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
881 * to done when the action is done.
882 *
883 * @param progress
884 * the {@link ProgressBar} or NULL
885 * @param run
886 * the action to run
887 */
888 private void outOfUi(Progress progress, final Runnable run) {
889 final Progress pg = new Progress();
890 final Progress reload = new Progress("Reload books");
891 if (progress == null) {
892 progress = new Progress();
893 }
894
895 pg.addProgress(progress, 90);
896 pg.addProgress(reload, 10);
897
898 invalidate();
899 pgBar.setProgress(pg);
900 validate();
901 setEnabled(false);
902
903 new Thread(new Runnable() {
904 @Override
905 public void run() {
906 try {
907 run.run();
908 refreshBooks();
909 } finally {
910 reload.done();
911 if (!pg.isDone()) {
912 // will trigger pgBar ActionListener:
913 pg.done();
914 }
915 }
916 }
917 }, "outOfUi thread").start();
918 }
919
920 /**
921 * Import a {@link Story} into the main {@link LocalLibrary}.
922 * <p>
923 * Should be called inside the UI thread.
924 *
925 * @param askUrl
926 * TRUE for an {@link URL}, false for a {@link File}
927 */
928 private void imprt(boolean askUrl) {
929 JFileChooser fc = new JFileChooser();
930
931 Object url;
932 if (askUrl) {
933 String clipboard = "";
934 try {
935 clipboard = ("" + Toolkit.getDefaultToolkit()
936 .getSystemClipboard().getData(DataFlavor.stringFlavor))
937 .trim();
938 } catch (Exception e) {
939 // No data will be handled
940 }
941
942 if (clipboard == null || !clipboard.startsWith("http")) {
943 clipboard = "";
944 }
945
946 url = JOptionPane.showInputDialog(GuiReaderFrame.this,
947 "url of the story to import?", "Importing from URL",
948 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
949 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
950 url = fc.getSelectedFile().getAbsolutePath();
951 } else {
952 url = null;
953 }
954
955 if (url != null && !url.toString().isEmpty()) {
956 imprt(url.toString(), null, null);
957 }
958 }
959
960 /**
961 * Actually import the {@link Story} into the main {@link LocalLibrary}.
962 * <p>
963 * Should be called inside the UI thread.
964 *
965 * @param url
966 * the {@link Story} to import by {@link URL}
967 * @param onSuccess
968 * Action to execute on success
969 */
970 private void imprt(final String url, final StoryRunnable onSuccess,
971 String onSuccessPgName) {
972 final Progress pg = new Progress();
973 final Progress pgImprt = new Progress();
974 final Progress pgOnSuccess = new Progress(onSuccessPgName);
975 pg.addProgress(pgImprt, 95);
976 pg.addProgress(pgOnSuccess, 5);
977
978 outOfUi(pg, new Runnable() {
979 @Override
980 public void run() {
981 Exception ex = null;
982 Story story = null;
983 try {
984 story = reader.getLibrary().imprt(BasicReader.getUrl(url),
985 pgImprt);
986 } catch (IOException e) {
987 ex = e;
988 }
989
990 final Exception e = ex;
991
992 final boolean ok = (e == null);
993
994 pgOnSuccess.setProgress(0);
995 if (!ok) {
996 if (e instanceof UnknownHostException) {
997 error("URL not supported: " + url, "Cannot import URL",
998 null);
999 } else {
1000 error("Failed to import " + url + ": \n"
1001 + e.getMessage(), "Cannot import URL", e);
1002 }
1003 } else {
1004 if (onSuccess != null) {
1005 onSuccess.run(story);
1006 }
1007 }
1008 pgOnSuccess.done();
1009 }
1010 });
1011 }
1012
1013 /**
1014 * Enables or disables this component, depending on the value of the
1015 * parameter <code>b</code>. An enabled component can respond to user input
1016 * and generate events. Components are enabled initially by default.
1017 * <p>
1018 * Disabling this component will also affect its children.
1019 *
1020 * @param b
1021 * If <code>true</code>, this component is enabled; otherwise
1022 * this component is disabled
1023 */
1024 @Override
1025 public void setEnabled(boolean b) {
1026 if (bar != null) {
1027 bar.setEnabled(b);
1028 }
1029
1030 for (GuiReaderGroup group : booksByType.keySet()) {
1031 group.setEnabled(b);
1032 }
1033 for (GuiReaderGroup group : booksByAuthor.keySet()) {
1034 group.setEnabled(b);
1035 }
1036 super.setEnabled(b);
1037 repaint();
1038 }
1039
1040 /**
1041 * Display an error message and log the linked {@link Exception}.
1042 *
1043 * @param message
1044 * the message
1045 * @param title
1046 * the title of the error message
1047 * @param e
1048 * the exception to log if any
1049 */
1050 private void error(final String message, final String title, Exception e) {
1051 Instance.getTraceHandler().error(title + ": " + message);
1052 if (e != null) {
1053 Instance.getTraceHandler().error(e);
1054 }
1055
1056 SwingUtilities.invokeLater(new Runnable() {
1057 @Override
1058 public void run() {
1059 JOptionPane.showMessageDialog(GuiReaderFrame.this, message,
1060 title, JOptionPane.ERROR_MESSAGE);
1061 }
1062 });
1063 }
1064 }