Wordcount (including UI), date of creation
[fanfix.git] / src / be / nikiroo / fanfix / reader / LocalReaderFrame.java
1 package be.nikiroo.fanfix.reader;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Frame;
6 import java.awt.Toolkit;
7 import java.awt.datatransfer.DataFlavor;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.KeyEvent;
11 import java.awt.event.MouseEvent;
12 import java.awt.event.WindowEvent;
13 import java.io.File;
14 import java.io.IOException;
15 import java.net.URL;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Map.Entry;
20
21 import javax.swing.BoxLayout;
22 import javax.swing.JFileChooser;
23 import javax.swing.JFrame;
24 import javax.swing.JMenu;
25 import javax.swing.JMenuBar;
26 import javax.swing.JMenuItem;
27 import javax.swing.JOptionPane;
28 import javax.swing.JPanel;
29 import javax.swing.JPopupMenu;
30 import javax.swing.JScrollPane;
31 import javax.swing.SwingUtilities;
32 import javax.swing.filechooser.FileFilter;
33 import javax.swing.filechooser.FileNameExtensionFilter;
34
35 import be.nikiroo.fanfix.Instance;
36 import be.nikiroo.fanfix.Library;
37 import be.nikiroo.fanfix.bundles.UiConfig;
38 import be.nikiroo.fanfix.data.MetaData;
39 import be.nikiroo.fanfix.data.Story;
40 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
41 import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener;
42 import be.nikiroo.utils.Progress;
43 import be.nikiroo.utils.Version;
44 import be.nikiroo.utils.ui.ProgressBar;
45
46 /**
47 * A {@link Frame} that will show a {@link LocalReaderBook} item for each
48 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
49 * way to copy them to the {@link LocalReader} cache ({@link LocalReader#lib}),
50 * read them, delete them...
51 *
52 * @author niki
53 */
54 class LocalReaderFrame extends JFrame {
55 private static final long serialVersionUID = 1L;
56 private LocalReader reader;
57 private Map<LocalReaderGroup, String> booksByType;
58 private Map<LocalReaderGroup, String> booksByAuthor;
59 private JPanel pane;
60 private Color color;
61 private ProgressBar pgBar;
62 private JMenuBar bar;
63 private LocalReaderBook selectedBook;
64 private boolean words; // words or authors (secondary info on books)
65
66 /**
67 * Create a new {@link LocalReaderFrame}.
68 *
69 * @param reader
70 * the associated {@link LocalReader} to forward some commands
71 * and access its {@link Library}
72 * @param type
73 * the type of {@link Story} to load, or NULL for all types
74 */
75 public LocalReaderFrame(LocalReader reader, String type) {
76 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
77
78 this.reader = reader;
79
80 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
81 setSize(800, 600);
82 setLayout(new BorderLayout());
83
84 pane = new JPanel();
85 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
86
87 color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR);
88 if (color != null) {
89 setBackground(color);
90 pane.setBackground(color);
91 }
92
93 JScrollPane scroll = new JScrollPane(pane);
94 scroll.getVerticalScrollBar().setUnitIncrement(16);
95 add(scroll, BorderLayout.CENTER);
96
97 pgBar = new ProgressBar();
98 add(pgBar, BorderLayout.SOUTH);
99
100 setJMenuBar(createMenu());
101
102 booksByType = new HashMap<LocalReaderGroup, String>();
103 booksByAuthor = new HashMap<LocalReaderGroup, String>();
104
105 addBookPane(type, true);
106 refreshBooks();
107
108 setVisible(true);
109 }
110
111 /**
112 * Add a new {@link LocalReaderGroup} on the frame to display the books of
113 * the selected type or author.
114 *
115 * @param value
116 * the author or the type
117 * @param type
118 * TRUE for type, FALSE for author
119 */
120 private void addBookPane(String value, boolean type) {
121 if (value == null) {
122 if (type) {
123 for (String tt : Instance.getLibrary().getTypes()) {
124 if (tt != null) {
125 addBookPane(tt, type);
126 }
127 }
128 } else {
129 for (String tt : Instance.getLibrary().getAuthors()) {
130 if (tt != null) {
131 addBookPane(tt, type);
132 }
133 }
134 }
135
136 return;
137 }
138
139 LocalReaderGroup bookPane = new LocalReaderGroup(reader, value, color);
140 if (type) {
141 booksByType.put(bookPane, value);
142 } else {
143 booksByAuthor.put(bookPane, value);
144 }
145
146 this.invalidate();
147 pane.invalidate();
148 pane.add(bookPane);
149 pane.validate();
150 this.validate();
151
152 bookPane.setActionListener(new BookActionListener() {
153 public void select(LocalReaderBook book) {
154 selectedBook = book;
155 }
156
157 public void popupRequested(LocalReaderBook book, MouseEvent e) {
158 JPopupMenu popup = new JPopupMenu();
159 popup.add(createMenuItemOpenBook());
160 popup.addSeparator();
161 popup.add(createMenuItemExport());
162 popup.add(createMenuItemClearCache());
163 popup.add(createMenuItemRedownload());
164 popup.addSeparator();
165 popup.add(createMenuItemDelete());
166 popup.show(e.getComponent(), e.getX(), e.getY());
167 }
168
169 public void action(final LocalReaderBook book) {
170 openBook(book);
171 }
172 });
173 }
174
175 private void removeBookPanes() {
176 booksByType.clear();
177 booksByAuthor.clear();
178 pane.invalidate();
179 this.invalidate();
180 pane.removeAll();
181 pane.validate();
182 this.validate();
183 }
184
185 /**
186 * Refresh the list of {@link LocalReaderBook}s from disk.
187 *
188 * @param type
189 * the type of {@link Story} to load, or NULL for all types
190 */
191 private void refreshBooks() {
192 for (LocalReaderGroup group : booksByType.keySet()) {
193 List<MetaData> stories = Instance.getLibrary().getListByType(
194 booksByType.get(group));
195 group.refreshBooks(stories, words);
196 }
197
198 for (LocalReaderGroup group : booksByAuthor.keySet()) {
199 List<MetaData> stories = Instance.getLibrary().getListByAuthor(
200 booksByAuthor.get(group));
201 group.refreshBooks(stories, words);
202 }
203
204 pane.repaint();
205 this.repaint();
206 }
207
208 /**
209 * Create the main menu bar.
210 *
211 * @return the bar
212 */
213 private JMenuBar createMenu() {
214 bar = new JMenuBar();
215
216 JMenu file = new JMenu("File");
217 file.setMnemonic(KeyEvent.VK_F);
218
219 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
220 imprt.addActionListener(new ActionListener() {
221 public void actionPerformed(ActionEvent e) {
222 imprt(true);
223 }
224 });
225 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
226 imprtF.addActionListener(new ActionListener() {
227 public void actionPerformed(ActionEvent e) {
228 imprt(false);
229 }
230 });
231 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
232 exit.addActionListener(new ActionListener() {
233 public void actionPerformed(ActionEvent e) {
234 LocalReaderFrame.this.dispatchEvent(new WindowEvent(
235 LocalReaderFrame.this, WindowEvent.WINDOW_CLOSING));
236 }
237 });
238
239 file.add(createMenuItemOpenBook());
240 file.add(createMenuItemExport());
241 file.addSeparator();
242 file.add(imprt);
243 file.add(imprtF);
244 file.addSeparator();
245 file.add(exit);
246
247 bar.add(file);
248
249 JMenu edit = new JMenu("Edit");
250 edit.setMnemonic(KeyEvent.VK_E);
251
252 edit.add(createMenuItemClearCache());
253 edit.add(createMenuItemRedownload());
254 edit.addSeparator();
255 edit.add(createMenuItemDelete());
256
257 bar.add(edit);
258
259 JMenu view = new JMenu("View");
260 view.setMnemonic(KeyEvent.VK_V);
261 JMenuItem vauthors = new JMenuItem("Author");
262 vauthors.setMnemonic(KeyEvent.VK_A);
263 vauthors.addActionListener(new ActionListener() {
264 public void actionPerformed(ActionEvent e) {
265 words = false;
266 refreshBooks();
267 }
268 });
269 view.add(vauthors);
270 JMenuItem vwords = new JMenuItem("Word count");
271 vwords.setMnemonic(KeyEvent.VK_W);
272 vwords.addActionListener(new ActionListener() {
273 public void actionPerformed(ActionEvent e) {
274 words = true;
275 refreshBooks();
276 }
277 });
278 view.add(vwords);
279 bar.add(view);
280
281 JMenu sources = new JMenu("Sources");
282 sources.setMnemonic(KeyEvent.VK_S);
283
284 List<String> tt = Instance.getLibrary().getTypes();
285 tt.add(0, null);
286 for (final String type : tt) {
287 JMenuItem item = new JMenuItem(type == null ? "All" : type);
288 item.addActionListener(new ActionListener() {
289 public void actionPerformed(ActionEvent e) {
290 removeBookPanes();
291 addBookPane(type, true);
292 refreshBooks();
293 }
294 });
295 sources.add(item);
296
297 if (type == null) {
298 sources.addSeparator();
299 }
300 }
301
302 bar.add(sources);
303
304 JMenu authors = new JMenu("Authors");
305 authors.setMnemonic(KeyEvent.VK_A);
306
307 List<String> aa = Instance.getLibrary().getAuthors();
308 aa.add(0, null);
309 for (final String author : aa) {
310 JMenuItem item = new JMenuItem(author == null ? "All"
311 : author.isEmpty() ? "[unknown]" : author);
312 item.addActionListener(new ActionListener() {
313 public void actionPerformed(ActionEvent e) {
314 removeBookPanes();
315 addBookPane(author, false);
316 refreshBooks();
317 }
318 });
319 authors.add(item);
320
321 if (author == null || author.isEmpty()) {
322 authors.addSeparator();
323 }
324 }
325
326 bar.add(authors);
327
328 return bar;
329 }
330
331 /**
332 * Create the export menu item.
333 *
334 * @return the item
335 */
336 private JMenuItem createMenuItemExport() {
337 final JFileChooser fc = new JFileChooser();
338 fc.setAcceptAllFileFilterUsed(false);
339
340 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
341 for (OutputType type : OutputType.values()) {
342 String ext = type.getDefaultExtension(false);
343 String desc = type.getDesc(false);
344
345 if (ext == null || ext.isEmpty()) {
346 filters.put(createAllFilter(desc), type);
347 } else {
348 filters.put(new FileNameExtensionFilter(desc, ext), type);
349 }
350 }
351
352 // First the "ALL" filters, then, the extension filters
353 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
354 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
355 fc.addChoosableFileFilter(entry.getKey());
356 }
357 }
358 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
359 if (entry.getKey() instanceof FileNameExtensionFilter) {
360 fc.addChoosableFileFilter(entry.getKey());
361 }
362 }
363 //
364
365 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
366 export.addActionListener(new ActionListener() {
367 public void actionPerformed(ActionEvent e) {
368 if (selectedBook != null) {
369 fc.showDialog(LocalReaderFrame.this, "Save");
370 if (fc.getSelectedFile() != null) {
371 final OutputType type = filters.get(fc.getFileFilter());
372 final String path = fc.getSelectedFile()
373 .getAbsolutePath()
374 + type.getDefaultExtension(false);
375 final Progress pg = new Progress();
376 outOfUi(pg, new Runnable() {
377 public void run() {
378 try {
379 Instance.getLibrary().export(
380 selectedBook.getMeta().getLuid(),
381 type, path, pg);
382 } catch (IOException e) {
383 Instance.syserr(e);
384 }
385 }
386 });
387 }
388 }
389 }
390 });
391
392 return export;
393 }
394
395 /**
396 * Create a {@link FileFilter} that accepts all files and return the given
397 * description.
398 *
399 * @param desc
400 * the description
401 *
402 * @return the filter
403 */
404 private FileFilter createAllFilter(final String desc) {
405 return new FileFilter() {
406 @Override
407 public String getDescription() {
408 return desc;
409 }
410
411 @Override
412 public boolean accept(File f) {
413 return true;
414 }
415 };
416 }
417
418 /**
419 * Create the refresh (delete cache) menu item.
420 *
421 * @return the item
422 */
423 private JMenuItem createMenuItemClearCache() {
424 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
425 refresh.addActionListener(new ActionListener() {
426 public void actionPerformed(ActionEvent e) {
427 if (selectedBook != null) {
428 outOfUi(null, new Runnable() {
429 public void run() {
430 reader.refresh(selectedBook.getMeta().getLuid());
431 selectedBook.setCached(false);
432 SwingUtilities.invokeLater(new Runnable() {
433 public void run() {
434 selectedBook.repaint();
435 }
436 });
437 }
438 });
439 }
440 }
441 });
442
443 return refresh;
444 }
445
446 /**
447 * Create the redownload (then delete original) menu item.
448 *
449 * @return the item
450 */
451 private JMenuItem createMenuItemRedownload() {
452 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
453 refresh.addActionListener(new ActionListener() {
454 public void actionPerformed(ActionEvent e) {
455 if (selectedBook != null) {
456 imprt(selectedBook.getMeta().getUrl(), new Runnable() {
457 public void run() {
458 reader.delete(selectedBook.getMeta().getLuid());
459 selectedBook = null;
460 }
461 });
462 }
463 }
464 });
465
466 return refresh;
467 }
468
469 /**
470 * Create the delete menu item.
471 *
472 * @return the item
473 */
474 private JMenuItem createMenuItemDelete() {
475 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
476 delete.addActionListener(new ActionListener() {
477 public void actionPerformed(ActionEvent e) {
478 if (selectedBook != null) {
479 outOfUi(null, new Runnable() {
480 public void run() {
481 reader.delete(selectedBook.getMeta().getLuid());
482 selectedBook = null;
483 SwingUtilities.invokeLater(new Runnable() {
484 public void run() {
485 refreshBooks();
486 }
487 });
488 }
489 });
490 }
491 }
492 });
493
494 return delete;
495 }
496
497 /**
498 * Create the open menu item.
499 *
500 * @return the item
501 */
502 private JMenuItem createMenuItemOpenBook() {
503 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
504 open.addActionListener(new ActionListener() {
505 public void actionPerformed(ActionEvent e) {
506 if (selectedBook != null) {
507 openBook(selectedBook);
508 }
509 }
510 });
511
512 return open;
513 }
514
515 /**
516 * Open a {@link LocalReaderBook} item.
517 *
518 * @param book
519 * the {@link LocalReaderBook} to open
520 */
521 private void openBook(final LocalReaderBook book) {
522 final Progress pg = new Progress();
523 outOfUi(pg, new Runnable() {
524 public void run() {
525 try {
526 reader.open(book.getMeta().getLuid(), pg);
527 SwingUtilities.invokeLater(new Runnable() {
528 public void run() {
529 book.setCached(true);
530 }
531 });
532 } catch (IOException e) {
533 // TODO: error message?
534 Instance.syserr(e);
535 }
536 }
537 });
538 }
539
540 /**
541 * Process the given action out of the Swing UI thread and link the given
542 * {@link ProgressBar} to the action.
543 * <p>
544 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
545 * to done when the action is done.
546 *
547 * @param pg
548 * the {@link ProgressBar} or NULL
549 * @param run
550 * the action to run
551 */
552 private void outOfUi(final Progress pg, final Runnable run) {
553 pgBar.setProgress(pg);
554
555 setEnabled(false);
556 pgBar.addActioListener(new ActionListener() {
557 public void actionPerformed(ActionEvent e) {
558 pgBar.setProgress(null);
559 setEnabled(true);
560 }
561 });
562
563 new Thread(new Runnable() {
564 public void run() {
565 run.run();
566 if (pg == null) {
567 SwingUtilities.invokeLater(new Runnable() {
568 public void run() {
569 setEnabled(true);
570 }
571 });
572 } else if (!pg.isDone()) {
573 pg.setProgress(pg.getMax());
574 }
575 }
576 }).start();
577 }
578
579 /**
580 * Import a {@link Story} into the main {@link Library}.
581 * <p>
582 * Should be called inside the UI thread.
583 *
584 * @param askUrl
585 * TRUE for an {@link URL}, false for a {@link File}
586 */
587 private void imprt(boolean askUrl) {
588 JFileChooser fc = new JFileChooser();
589
590 Object url;
591 if (askUrl) {
592 String clipboard = "";
593 try {
594 clipboard = ("" + Toolkit.getDefaultToolkit()
595 .getSystemClipboard().getData(DataFlavor.stringFlavor))
596 .trim();
597 } catch (Exception e) {
598 // No data will be handled
599 }
600
601 if (clipboard == null || !clipboard.startsWith("http")) {
602 clipboard = "";
603 }
604
605 url = JOptionPane.showInputDialog(LocalReaderFrame.this,
606 "url of the story to import?", "Importing from URL",
607 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
608 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
609 url = fc.getSelectedFile().getAbsolutePath();
610 } else {
611 url = null;
612 }
613
614 if (url != null && !url.toString().isEmpty()) {
615 imprt(url.toString(), null);
616 }
617 }
618
619 /**
620 * Actually import the {@link Story} into the main {@link Library}.
621 * <p>
622 * Should be called inside the UI thread.
623 *
624 * @param url
625 * the {@link Story} to import by {@link URL}
626 * @param onSuccess
627 * Action to execute on success
628 */
629 private void imprt(final String url, final Runnable onSuccess) {
630 final Progress pg = new Progress("Importing " + url);
631 outOfUi(pg, new Runnable() {
632 public void run() {
633 Exception ex = null;
634 try {
635 Instance.getLibrary().imprt(BasicReader.getUrl(url), pg);
636 } catch (IOException e) {
637 ex = e;
638 }
639
640 final Exception e = ex;
641
642 final boolean ok = (e == null);
643 SwingUtilities.invokeLater(new Runnable() {
644 public void run() {
645 if (!ok) {
646 Instance.syserr(e);
647 JOptionPane.showMessageDialog(
648 LocalReaderFrame.this, "Cannot import: "
649 + url, e.getMessage(),
650 JOptionPane.ERROR_MESSAGE);
651
652 setEnabled(true);
653 } else {
654 refreshBooks();
655 if (onSuccess != null) {
656 onSuccess.run();
657 refreshBooks();
658 }
659 }
660 }
661 });
662 }
663 });
664 }
665
666 /**
667 * Enables or disables this component, depending on the value of the
668 * parameter <code>b</code>. An enabled component can respond to user input
669 * and generate events. Components are enabled initially by default.
670 * <p>
671 * Disabling this component will also affect its children.
672 *
673 * @param b
674 * If <code>true</code>, this component is enabled; otherwise
675 * this component is disabled
676 */
677 @Override
678 public void setEnabled(boolean b) {
679 bar.setEnabled(b);
680 for (LocalReaderGroup group : booksByType.keySet()) {
681 group.setEnabled(b);
682 }
683 for (LocalReaderGroup group : booksByAuthor.keySet()) {
684 group.setEnabled(b);
685 }
686 super.setEnabled(b);
687 repaint();
688 }
689 }