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