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