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