f4c5eb3997cc97373788fe21c8bb2d1d3d52c061
[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(createMenuItemMoveTo(true));
311 popup.add(createMenuItemSetCover());
312 popup.add(createMenuItemClearCache());
313 popup.add(createMenuItemRedownload());
314 popup.addSeparator();
315 popup.add(createMenuItemRename(true));
316 popup.add(createMenuItemSetAuthor(true));
317 popup.addSeparator();
318 popup.add(createMenuItemDelete());
319 popup.addSeparator();
320 popup.add(createMenuItemProperties());
321 popup.show(e.getComponent(), e.getX(), e.getY());
322 }
323
324 @Override
325 public void action(final GuiReaderBook book) {
326 openBook(book);
327 }
328 });
329 }
330
331 private void removeBookPanes() {
332 booksByType.clear();
333 booksByAuthor.clear();
334 pane.invalidate();
335 this.invalidate();
336 pane.removeAll();
337 pane.validate();
338 this.validate();
339 }
340
341 /**
342 * Refresh the list of {@link GuiReaderBook}s from disk.
343 */
344 private void refreshBooks() {
345 for (GuiReaderGroup group : booksByType.keySet()) {
346 List<MetaData> stories = reader.getLibrary().getListBySource(
347 booksByType.get(group));
348 group.refreshBooks(stories, words);
349 }
350
351 for (GuiReaderGroup group : booksByAuthor.keySet()) {
352 List<MetaData> stories = reader.getLibrary().getListByAuthor(
353 booksByAuthor.get(group));
354 group.refreshBooks(stories, words);
355 }
356
357 pane.repaint();
358 this.repaint();
359 }
360
361 /**
362 * Create the main menu bar.
363 *
364 * @param libOk
365 * the library can be queried
366 *
367 * @return the bar
368 */
369 private JMenuBar createMenu(boolean libOk) {
370 bar = new JMenuBar();
371
372 JMenu file = new JMenu("File");
373 file.setMnemonic(KeyEvent.VK_F);
374
375 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
376 imprt.addActionListener(new ActionListener() {
377 @Override
378 public void actionPerformed(ActionEvent e) {
379 imprt(true);
380 }
381 });
382 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
383 imprtF.addActionListener(new ActionListener() {
384 @Override
385 public void actionPerformed(ActionEvent e) {
386 imprt(false);
387 }
388 });
389 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
390 exit.addActionListener(new ActionListener() {
391 @Override
392 public void actionPerformed(ActionEvent e) {
393 GuiReaderFrame.this.dispatchEvent(new WindowEvent(
394 GuiReaderFrame.this, WindowEvent.WINDOW_CLOSING));
395 }
396 });
397
398 file.add(createMenuItemOpenBook());
399 file.add(createMenuItemExport());
400 file.add(createMenuItemMoveTo(libOk));
401 file.addSeparator();
402 file.add(imprt);
403 file.add(imprtF);
404 file.addSeparator();
405 file.add(createMenuItemRename(libOk));
406 file.add(createMenuItemSetAuthor(libOk));
407 file.addSeparator();
408 file.add(exit);
409
410 bar.add(file);
411
412 JMenu edit = new JMenu("Edit");
413 edit.setMnemonic(KeyEvent.VK_E);
414
415 edit.add(createMenuItemClearCache());
416 edit.add(createMenuItemRedownload());
417 edit.addSeparator();
418 edit.add(createMenuItemDelete());
419
420 bar.add(edit);
421
422 JMenu view = new JMenu("View");
423 view.setMnemonic(KeyEvent.VK_V);
424 JMenuItem vauthors = new JMenuItem("Author");
425 vauthors.setMnemonic(KeyEvent.VK_A);
426 vauthors.addActionListener(new ActionListener() {
427 @Override
428 public void actionPerformed(ActionEvent e) {
429 words = false;
430 refreshBooks();
431 }
432 });
433 view.add(vauthors);
434 JMenuItem vwords = new JMenuItem("Word count");
435 vwords.setMnemonic(KeyEvent.VK_W);
436 vwords.addActionListener(new ActionListener() {
437 @Override
438 public void actionPerformed(ActionEvent e) {
439 words = true;
440 refreshBooks();
441 }
442 });
443 view.add(vwords);
444 bar.add(view);
445
446 JMenu sources = new JMenu("Sources");
447 sources.setMnemonic(KeyEvent.VK_S);
448
449 List<String> tt = new ArrayList<String>();
450 if (libOk) {
451 tt.addAll(reader.getLibrary().getSources());
452 }
453 tt.add(0, null);
454
455 for (final String type : tt) {
456 JMenuItem item = new JMenuItem(type == null ? "All" : type);
457 item.addActionListener(new ActionListener() {
458 @Override
459 public void actionPerformed(ActionEvent e) {
460 removeBookPanes();
461 addBookPane(type, true);
462 refreshBooks();
463 }
464 });
465 sources.add(item);
466
467 if (type == null) {
468 sources.addSeparator();
469 }
470 }
471
472 bar.add(sources);
473
474 JMenu authors = new JMenu("Authors");
475 authors.setMnemonic(KeyEvent.VK_A);
476
477 List<Entry<String, List<String>>> authorGroups = reader.getLibrary()
478 .getAuthorsGrouped();
479 if (authorGroups.size() > 1) {
480 // Multiple groups
481
482 // null -> "All" authors special item
483 populateMenuAuthorList(authors, Arrays.asList((String) null));
484
485 for (Entry<String, List<String>> group : authorGroups) {
486 JMenu thisGroup = new JMenu(group.getKey());
487 populateMenuAuthorList(thisGroup, group.getValue());
488 authors.add(thisGroup);
489 }
490 } else {
491 // Only one group
492
493 // null -> "All" authors special item
494 List<String> authorNames = new ArrayList<String>();
495 authorNames.add(null);
496 if (authorGroups.size() > 0) {
497 authorNames.addAll(authorGroups.get(0).getValue());
498 }
499 populateMenuAuthorList(authors, authorNames);
500 }
501
502 bar.add(authors);
503
504 JMenu options = new JMenu("Options");
505 options.setMnemonic(KeyEvent.VK_O);
506 options.add(createMenuItemConfig());
507 options.add(createMenuItemUiConfig());
508 bar.add(options);
509
510 return bar;
511 }
512
513 /**
514 * Populate a list of authors as {@link JMenuItem}s into the given
515 * {@link JMenu}.
516 * <p>
517 * Each item will select the author when clicked.
518 *
519 * @param authors
520 * the parent {@link JMenuItem}
521 * @param names
522 * the authors' names
523 */
524 private void populateMenuAuthorList(JMenu authors, List<String> names) {
525 for (final String name : names) {
526 JMenuItem item = new JMenuItem(name == null ? "All"
527 : name.isEmpty() ? "[unknown]" : name);
528 item.addActionListener(new ActionListener() {
529 @Override
530 public void actionPerformed(ActionEvent e) {
531 removeBookPanes();
532 addBookPane(name, false);
533 refreshBooks();
534 }
535 });
536 authors.add(item);
537
538 if (name == null || name.isEmpty()) {
539 authors.addSeparator();
540 }
541 }
542 }
543
544 /**
545 * Create the Fanfix Configuration menu item.
546 *
547 * @return the item
548 */
549 private JMenuItem createMenuItemConfig() {
550 final String title = "Fanfix Configuration";
551 JMenuItem item = new JMenuItem(title);
552 item.setMnemonic(KeyEvent.VK_F);
553
554 item.addActionListener(new ActionListener() {
555 @Override
556 public void actionPerformed(ActionEvent e) {
557 ConfigEditor<Config> ed = new ConfigEditor<Config>(
558 Config.class, Instance.getConfig(),
559 "This is where you configure the options of the program.");
560 JFrame frame = new JFrame(title);
561 frame.add(ed);
562 frame.setSize(800, 600);
563 frame.setVisible(true);
564 }
565 });
566
567 return item;
568 }
569
570 /**
571 * Create the UI Configuration menu item.
572 *
573 * @return the item
574 */
575 private JMenuItem createMenuItemUiConfig() {
576 final String title = "UI Configuration";
577 JMenuItem item = new JMenuItem(title);
578 item.setMnemonic(KeyEvent.VK_U);
579
580 item.addActionListener(new ActionListener() {
581 @Override
582 public void actionPerformed(ActionEvent e) {
583 ConfigEditor<UiConfig> ed = new ConfigEditor<UiConfig>(
584 UiConfig.class, Instance.getUiConfig(),
585 "This is where you configure the graphical appearence of the program.");
586 JFrame frame = new JFrame(title);
587 frame.add(ed);
588 frame.setSize(800, 600);
589 frame.setVisible(true);
590 }
591 });
592
593 return item;
594 }
595
596 /**
597 * Create the export menu item.
598 *
599 * @return the item
600 */
601 private JMenuItem createMenuItemExport() {
602 final JFileChooser fc = new JFileChooser();
603 fc.setAcceptAllFileFilterUsed(false);
604
605 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
606 for (OutputType type : OutputType.values()) {
607 String ext = type.getDefaultExtension(false);
608 String desc = type.getDesc(false);
609
610 if (ext == null || ext.isEmpty()) {
611 filters.put(createAllFilter(desc), type);
612 } else {
613 filters.put(new FileNameExtensionFilter(desc, ext), type);
614 }
615 }
616
617 // First the "ALL" filters, then, the extension filters
618 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
619 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
620 fc.addChoosableFileFilter(entry.getKey());
621 }
622 }
623 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
624 if (entry.getKey() instanceof FileNameExtensionFilter) {
625 fc.addChoosableFileFilter(entry.getKey());
626 }
627 }
628 //
629
630 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
631 export.addActionListener(new ActionListener() {
632 @Override
633 public void actionPerformed(ActionEvent e) {
634 if (selectedBook != null) {
635 fc.showDialog(GuiReaderFrame.this, "Save");
636 if (fc.getSelectedFile() != null) {
637 final OutputType type = filters.get(fc.getFileFilter());
638 final String path = fc.getSelectedFile()
639 .getAbsolutePath()
640 + type.getDefaultExtension(false);
641 final Progress pg = new Progress();
642 outOfUi(pg, new Runnable() {
643 @Override
644 public void run() {
645 try {
646 reader.getLibrary().export(
647 selectedBook.getMeta().getLuid(),
648 type, path, pg);
649 } catch (IOException e) {
650 Instance.getTraceHandler().error(e);
651 }
652 }
653 });
654 }
655 }
656 }
657 });
658
659 return export;
660 }
661
662 /**
663 * Create a {@link FileFilter} that accepts all files and return the given
664 * description.
665 *
666 * @param desc
667 * the description
668 *
669 * @return the filter
670 */
671 private FileFilter createAllFilter(final String desc) {
672 return new FileFilter() {
673 @Override
674 public String getDescription() {
675 return desc;
676 }
677
678 @Override
679 public boolean accept(File f) {
680 return true;
681 }
682 };
683 }
684
685 /**
686 * Create the refresh (delete cache) menu item.
687 *
688 * @return the item
689 */
690 private JMenuItem createMenuItemClearCache() {
691 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
692 refresh.addActionListener(new ActionListener() {
693 @Override
694 public void actionPerformed(ActionEvent e) {
695 if (selectedBook != null) {
696 outOfUi(null, new Runnable() {
697 @Override
698 public void run() {
699 reader.clearLocalReaderCache(selectedBook.getMeta()
700 .getLuid());
701 selectedBook.setCached(false);
702 GuiReaderCoverImager.clearIcon(selectedBook
703 .getMeta());
704 SwingUtilities.invokeLater(new Runnable() {
705 @Override
706 public void run() {
707 selectedBook.repaint();
708 }
709 });
710 }
711 });
712 }
713 }
714 });
715
716 return refresh;
717 }
718
719 /**
720 * Create the "move to" menu item.
721 *
722 * @param libOk
723 * the library can be queried
724 *
725 * @return the item
726 */
727 private JMenuItem createMenuItemMoveTo(boolean libOk) {
728 JMenuItem changeTo = new JMenu("Move to");
729 changeTo.setMnemonic(KeyEvent.VK_M);
730
731 List<String> values = new ArrayList<String>();
732 values.add(null);
733 if (libOk) {
734 values.addAll(reader.getLibrary().getSources());
735 }
736
737 for (String value : values) {
738 JMenuItem item = new JMenuItem(value == null ? "New type..."
739 : value);
740
741 item.addActionListener(createMoveAction("SOURCE", value));
742
743 changeTo.add(item);
744 if (value == null) {
745 ((JMenu) changeTo).addSeparator();
746 }
747 }
748
749 return changeTo;
750 }
751
752 /**
753 * Create the "set author" menu item.
754 *
755 * @param libOk
756 * the library can be queried
757 *
758 * @return the item
759 */
760 private JMenuItem createMenuItemSetAuthor(boolean libOk) {
761 JMenu changeTo = new JMenu("Set author");
762 changeTo.setMnemonic(KeyEvent.VK_A);
763
764 // New author
765 JMenuItem newItem = new JMenuItem("New author...");
766 changeTo.add(newItem);
767 changeTo.addSeparator();
768 newItem.addActionListener(createMoveAction("AUTHOR", null));
769
770 // Existing authors
771 if (libOk) {
772 List<Entry<String, List<String>>> authorGroups = reader
773 .getLibrary().getAuthorsGrouped();
774
775 if (authorGroups.size() > 1) {
776 for (Entry<String, List<String>> entry : authorGroups) {
777 JMenu group = new JMenu(entry.getKey());
778 for (String value : entry.getValue()) {
779 JMenuItem item = new JMenuItem(value);
780 item.addActionListener(createMoveAction("AUTHOR", value));
781 group.add(item);
782 }
783 changeTo.add(group);
784 }
785 } else if (authorGroups.size() == 1) {
786 for (String value : authorGroups.get(0).getValue()) {
787 JMenuItem item = new JMenuItem(value);
788 item.addActionListener(createMoveAction("AUTHOR", value));
789 changeTo.add(item);
790 }
791 }
792 }
793
794 return changeTo;
795 }
796
797 /**
798 * Create the "rename" menu item.
799 *
800 * @param libOk
801 * the library can be queried
802 *
803 * @return the item
804 */
805 private JMenuItem createMenuItemRename(
806 @SuppressWarnings("unused") boolean libOk) {
807 JMenuItem changeTo = new JMenuItem("Rename...");
808 changeTo.setMnemonic(KeyEvent.VK_R);
809 changeTo.addActionListener(createMoveAction("TITLE", null));
810 return changeTo;
811 }
812
813 private ActionListener createMoveAction(final String what, final String type) {
814 return new ActionListener() {
815 @Override
816 public void actionPerformed(ActionEvent e) {
817 if (selectedBook != null) {
818 String changeTo = type;
819 if (type == null) {
820 String init = "";
821 if (what.equals("SOURCE")) {
822 init = selectedBook.getMeta().getSource();
823 } else if (what.equals("TITLE")) {
824 init = selectedBook.getMeta().getTitle();
825 } else if (what.equals("AUTHOR")) {
826 init = selectedBook.getMeta().getAuthor();
827 }
828
829 Object rep = JOptionPane.showInputDialog(
830 GuiReaderFrame.this, "Move to:",
831 "Moving story", JOptionPane.QUESTION_MESSAGE,
832 null, null, init);
833
834 if (rep == null) {
835 return;
836 }
837
838 changeTo = rep.toString();
839 }
840
841 final String fChangeTo = changeTo;
842 outOfUi(null, new Runnable() {
843 @Override
844 public void run() {
845 if (what.equals("SOURCE")) {
846 reader.changeSource(selectedBook.getMeta()
847 .getLuid(), fChangeTo);
848 } else if (what.equals("TITLE")) {
849 reader.changeTitle(selectedBook.getMeta()
850 .getLuid(), fChangeTo);
851 } else if (what.equals("AUTHOR")) {
852 reader.changeAuthor(selectedBook.getMeta()
853 .getLuid(), fChangeTo);
854 }
855
856 selectedBook = null;
857
858 SwingUtilities.invokeLater(new Runnable() {
859 @Override
860 public void run() {
861 setJMenuBar(createMenu(true));
862 }
863 });
864 }
865 });
866 }
867 }
868 };
869 }
870
871 /**
872 * Create the redownload (then delete original) menu item.
873 *
874 * @return the item
875 */
876 private JMenuItem createMenuItemRedownload() {
877 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
878 refresh.addActionListener(new ActionListener() {
879 @Override
880 public void actionPerformed(ActionEvent e) {
881 if (selectedBook != null) {
882 final MetaData meta = selectedBook.getMeta();
883 imprt(meta.getUrl(), new StoryRunnable() {
884 @Override
885 public void run(Story story) {
886 reader.delete(meta.getLuid());
887 GuiReaderFrame.this.selectedBook = null;
888 MetaData newMeta = story.getMeta();
889 if (!newMeta.getSource().equals(meta.getSource())) {
890 reader.changeSource(newMeta.getLuid(),
891 meta.getSource());
892 }
893 }
894 }, "Removing old copy");
895 }
896 }
897 });
898
899 return refresh;
900 }
901
902 /**
903 * Create the delete menu item.
904 *
905 * @return the item
906 */
907 private JMenuItem createMenuItemDelete() {
908 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
909 delete.addActionListener(new ActionListener() {
910 @Override
911 public void actionPerformed(ActionEvent e) {
912 if (selectedBook != null) {
913 outOfUi(null, new Runnable() {
914 @Override
915 public void run() {
916 reader.delete(selectedBook.getMeta().getLuid());
917 selectedBook = null;
918 }
919 });
920 }
921 }
922 });
923
924 return delete;
925 }
926
927 /**
928 * Create the properties menu item.
929 *
930 * @return the item
931 */
932 private JMenuItem createMenuItemProperties() {
933 JMenuItem delete = new JMenuItem("Properties", KeyEvent.VK_P);
934 delete.addActionListener(new ActionListener() {
935 @Override
936 public void actionPerformed(ActionEvent e) {
937 if (selectedBook != null) {
938 outOfUi(null, new Runnable() {
939 @Override
940 public void run() {
941 final MetaData meta = selectedBook.getMeta();
942 new JFrame() {
943 private static final long serialVersionUID = 1L;
944 @SuppressWarnings("unused")
945 private Object init = init();
946
947 private Object init() {
948 // Borders
949 int top = 20;
950 int space = 10;
951
952 // Image
953 ImageIcon img = GuiReaderCoverImager
954 .generateCoverIcon(
955 reader.getLibrary(), meta);
956
957 // frame
958 setTitle(meta.getLuid() + ": "
959 + meta.getTitle());
960
961 setSize(800, img.getIconHeight() + 2 * top);
962 setLayout(new BorderLayout());
963
964 // Main panel
965 JPanel mainPanel = new JPanel(
966 new BorderLayout());
967 JPanel mainPanelKeys = new JPanel();
968 mainPanelKeys.setLayout(new BoxLayout(
969 mainPanelKeys, BoxLayout.Y_AXIS));
970 JPanel mainPanelValues = new JPanel();
971 mainPanelValues.setLayout(new BoxLayout(
972 mainPanelValues, BoxLayout.Y_AXIS));
973
974 mainPanel.add(mainPanelKeys,
975 BorderLayout.WEST);
976 mainPanel.add(mainPanelValues,
977 BorderLayout.CENTER);
978
979 List<Entry<String, String>> infos = BasicReader
980 .getMetaDesc(meta);
981
982 Color trans = new Color(0, 0, 0, 1);
983 for (Entry<String, String> info : infos) {
984 JTextArea key = new JTextArea(info
985 .getKey());
986 key.setFont(new Font(key.getFont()
987 .getFontName(), Font.BOLD, key
988 .getFont().getSize()));
989 key.setEditable(false);
990 key.setLineWrap(false);
991 key.setBackground(trans);
992 mainPanelKeys.add(key);
993
994 JTextArea value = new JTextArea(info
995 .getValue());
996 value.setEditable(false);
997 value.setLineWrap(false);
998 value.setBackground(trans);
999 mainPanelValues.add(value);
1000 }
1001
1002 // Image
1003 JLabel imgLabel = new JLabel(img);
1004 imgLabel.setVerticalAlignment(JLabel.TOP);
1005
1006 // Borders
1007 mainPanelKeys.setBorder(BorderFactory
1008 .createEmptyBorder(top, space, 0, 0));
1009 mainPanelValues.setBorder(BorderFactory
1010 .createEmptyBorder(top, space, 0, 0));
1011 imgLabel.setBorder(BorderFactory
1012 .createEmptyBorder(0, space, 0, 0));
1013
1014 // Add all
1015 add(imgLabel, BorderLayout.WEST);
1016 add(mainPanel, BorderLayout.CENTER);
1017
1018 return null;
1019 }
1020
1021 }.setVisible(true);
1022 }
1023 });
1024 }
1025 }
1026 });
1027
1028 return delete;
1029 }
1030
1031 /**
1032 * Create the open menu item for a book or a source (no LUID).
1033 *
1034 * @return the item
1035 */
1036 private JMenuItem createMenuItemOpenBook() {
1037 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
1038 open.addActionListener(new ActionListener() {
1039 @Override
1040 public void actionPerformed(ActionEvent e) {
1041 if (selectedBook != null) {
1042 if (selectedBook.getMeta().getLuid() == null) {
1043 removeBookPanes();
1044 addBookPane(selectedBook.getMeta().getSource(), true);
1045 refreshBooks();
1046 } else {
1047 openBook(selectedBook);
1048 }
1049 }
1050 }
1051 });
1052
1053 return open;
1054 }
1055
1056 /**
1057 * Create the SetCover menu item for a book to change the linked source
1058 * cover.
1059 *
1060 * @return the item
1061 */
1062 private JMenuItem createMenuItemSetCover() {
1063 JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
1064 open.addActionListener(new ActionListener() {
1065 @Override
1066 public void actionPerformed(ActionEvent e) {
1067 if (selectedBook != null) {
1068 reader.getLibrary().setSourceCover(
1069 selectedBook.getMeta().getSource(),
1070 selectedBook.getMeta().getLuid());
1071 MetaData source = selectedBook.getMeta().clone();
1072 source.setLuid(null);
1073 GuiReaderCoverImager.clearIcon(source);
1074 }
1075 }
1076 });
1077
1078 return open;
1079 }
1080
1081 /**
1082 * Open a {@link GuiReaderBook} item.
1083 *
1084 * @param book
1085 * the {@link GuiReaderBook} to open
1086 */
1087 private void openBook(final GuiReaderBook book) {
1088 final Progress pg = new Progress();
1089 outOfUi(pg, new Runnable() {
1090 @Override
1091 public void run() {
1092 try {
1093 reader.read(book.getMeta().getLuid(), false, pg);
1094 SwingUtilities.invokeLater(new Runnable() {
1095 @Override
1096 public void run() {
1097 book.setCached(true);
1098 }
1099 });
1100 } catch (IOException e) {
1101 // TODO: error message?
1102 Instance.getTraceHandler().error(e);
1103 }
1104 }
1105 });
1106 }
1107
1108 /**
1109 * Process the given action out of the Swing UI thread and link the given
1110 * {@link ProgressBar} to the action.
1111 * <p>
1112 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
1113 * to done when the action is done.
1114 *
1115 * @param progress
1116 * the {@link ProgressBar} or NULL
1117 * @param run
1118 * the action to run
1119 */
1120 private void outOfUi(Progress progress, final Runnable run) {
1121 final Progress pg = new Progress();
1122 final Progress reload = new Progress("Reload books");
1123 if (progress == null) {
1124 progress = new Progress();
1125 }
1126
1127 pg.addProgress(progress, 90);
1128 pg.addProgress(reload, 10);
1129
1130 invalidate();
1131 pgBar.setProgress(pg);
1132 validate();
1133 setEnabled(false);
1134
1135 new Thread(new Runnable() {
1136 @Override
1137 public void run() {
1138 try {
1139 run.run();
1140 refreshBooks();
1141 } finally {
1142 reload.done();
1143 if (!pg.isDone()) {
1144 // will trigger pgBar ActionListener:
1145 pg.done();
1146 }
1147 }
1148 }
1149 }, "outOfUi thread").start();
1150 }
1151
1152 /**
1153 * Import a {@link Story} into the main {@link LocalLibrary}.
1154 * <p>
1155 * Should be called inside the UI thread.
1156 *
1157 * @param askUrl
1158 * TRUE for an {@link URL}, false for a {@link File}
1159 */
1160 private void imprt(boolean askUrl) {
1161 JFileChooser fc = new JFileChooser();
1162
1163 Object url;
1164 if (askUrl) {
1165 String clipboard = "";
1166 try {
1167 clipboard = ("" + Toolkit.getDefaultToolkit()
1168 .getSystemClipboard().getData(DataFlavor.stringFlavor))
1169 .trim();
1170 } catch (Exception e) {
1171 // No data will be handled
1172 }
1173
1174 if (clipboard == null || !clipboard.startsWith("http")) {
1175 clipboard = "";
1176 }
1177
1178 url = JOptionPane.showInputDialog(GuiReaderFrame.this,
1179 "url of the story to import?", "Importing from URL",
1180 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
1181 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
1182 url = fc.getSelectedFile().getAbsolutePath();
1183 } else {
1184 url = null;
1185 }
1186
1187 if (url != null && !url.toString().isEmpty()) {
1188 imprt(url.toString(), null, null);
1189 }
1190 }
1191
1192 /**
1193 * Actually import the {@link Story} into the main {@link LocalLibrary}.
1194 * <p>
1195 * Should be called inside the UI thread.
1196 *
1197 * @param url
1198 * the {@link Story} to import by {@link URL}
1199 * @param onSuccess
1200 * Action to execute on success
1201 */
1202 private void imprt(final String url, final StoryRunnable onSuccess,
1203 String onSuccessPgName) {
1204 final Progress pg = new Progress();
1205 final Progress pgImprt = new Progress();
1206 final Progress pgOnSuccess = new Progress(onSuccessPgName);
1207 pg.addProgress(pgImprt, 95);
1208 pg.addProgress(pgOnSuccess, 5);
1209
1210 outOfUi(pg, new Runnable() {
1211 @Override
1212 public void run() {
1213 Exception ex = null;
1214 Story story = null;
1215 try {
1216 story = reader.getLibrary().imprt(BasicReader.getUrl(url),
1217 pgImprt);
1218 } catch (IOException e) {
1219 ex = e;
1220 }
1221
1222 final Exception e = ex;
1223
1224 final boolean ok = (e == null);
1225
1226 pgOnSuccess.setProgress(0);
1227 if (!ok) {
1228 if (e instanceof UnknownHostException) {
1229 error("URL not supported: " + url, "Cannot import URL",
1230 null);
1231 } else {
1232 error("Failed to import " + url + ": \n"
1233 + e.getMessage(), "Cannot import URL", e);
1234 }
1235 } else {
1236 if (onSuccess != null) {
1237 onSuccess.run(story);
1238 }
1239 }
1240 pgOnSuccess.done();
1241 }
1242 });
1243 }
1244
1245 /**
1246 * Enables or disables this component, depending on the value of the
1247 * parameter <code>b</code>. An enabled component can respond to user input
1248 * and generate events. Components are enabled initially by default.
1249 * <p>
1250 * Disabling this component will also affect its children.
1251 *
1252 * @param b
1253 * If <code>true</code>, this component is enabled; otherwise
1254 * this component is disabled
1255 */
1256 @Override
1257 public void setEnabled(boolean b) {
1258 if (bar != null) {
1259 bar.setEnabled(b);
1260 }
1261
1262 for (GuiReaderGroup group : booksByType.keySet()) {
1263 group.setEnabled(b);
1264 }
1265 for (GuiReaderGroup group : booksByAuthor.keySet()) {
1266 group.setEnabled(b);
1267 }
1268 super.setEnabled(b);
1269 repaint();
1270 }
1271
1272 /**
1273 * Display an error message and log the linked {@link Exception}.
1274 *
1275 * @param message
1276 * the message
1277 * @param title
1278 * the title of the error message
1279 * @param e
1280 * the exception to log if any
1281 */
1282 private void error(final String message, final String title, Exception e) {
1283 Instance.getTraceHandler().error(title + ": " + message);
1284 if (e != null) {
1285 Instance.getTraceHandler().error(e);
1286 }
1287
1288 SwingUtilities.invokeLater(new Runnable() {
1289 @Override
1290 public void run() {
1291 JOptionPane.showMessageDialog(GuiReaderFrame.this, message,
1292 title, JOptionPane.ERROR_MESSAGE);
1293 }
1294 });
1295 }
1296 }