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