(README update)
[fanfix.git] / src / be / nikiroo / fanfix / reader / LocalReaderFrame.java
CommitLineData
a6395bef
NR
1package be.nikiroo.fanfix.reader;
2
d3c84ac3 3import java.awt.BorderLayout;
b4dc6ab5 4import java.awt.Color;
4d205683 5import java.awt.Frame;
5130ce84
NR
6import java.awt.Toolkit;
7import java.awt.datatransfer.DataFlavor;
a6395bef
NR
8import java.awt.event.ActionEvent;
9import java.awt.event.ActionListener;
333f0e7b 10import java.awt.event.KeyEvent;
9843a5e5 11import java.awt.event.MouseEvent;
333f0e7b
NR
12import java.awt.event.WindowEvent;
13import java.io.File;
a6395bef 14import java.io.IOException;
4d205683 15import java.net.URL;
4d205683 16import java.util.HashMap;
a6395bef 17import java.util.List;
4d205683
NR
18import java.util.Map;
19import java.util.Map.Entry;
a6395bef 20
4310bae9 21import javax.swing.BoxLayout;
10d558d2 22import javax.swing.JFileChooser;
a6395bef 23import javax.swing.JFrame;
333f0e7b
NR
24import javax.swing.JMenu;
25import javax.swing.JMenuBar;
26import javax.swing.JMenuItem;
27import javax.swing.JOptionPane;
28import javax.swing.JPanel;
9843a5e5 29import javax.swing.JPopupMenu;
d3c84ac3 30import javax.swing.JScrollPane;
3b2b638f 31import javax.swing.SwingUtilities;
4d205683
NR
32import javax.swing.filechooser.FileFilter;
33import javax.swing.filechooser.FileNameExtensionFilter;
a6395bef
NR
34
35import be.nikiroo.fanfix.Instance;
4d205683 36import be.nikiroo.fanfix.Library;
b4dc6ab5 37import be.nikiroo.fanfix.bundles.UiConfig;
a6395bef 38import be.nikiroo.fanfix.data.MetaData;
4d205683
NR
39import be.nikiroo.fanfix.data.Story;
40import be.nikiroo.fanfix.output.BasicOutput.OutputType;
92fb0719 41import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener;
3b2b638f 42import be.nikiroo.utils.Progress;
39c3c689 43import be.nikiroo.utils.Version;
3b2b638f 44import be.nikiroo.utils.ui.ProgressBar;
a6395bef 45
4d205683
NR
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 */
a6395bef
NR
54class LocalReaderFrame extends JFrame {
55 private static final long serialVersionUID = 1L;
56 private LocalReader reader;
4310bae9
NR
57 private Map<LocalReaderGroup, String> booksByType;
58 private Map<LocalReaderGroup, String> booksByAuthor;
59 private JPanel pane;
b4dc6ab5 60 private Color color;
3b2b638f
NR
61 private ProgressBar pgBar;
62 private JMenuBar bar;
10d558d2 63 private LocalReaderBook selectedBook;
a6395bef 64
4d205683
NR
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 */
333f0e7b 74 public LocalReaderFrame(LocalReader reader, String type) {
39c3c689 75 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
a6395bef
NR
76
77 this.reader = reader;
78
79 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
80 setSize(800, 600);
d3c84ac3 81 setLayout(new BorderLayout());
a6395bef 82
4310bae9
NR
83 pane = new JPanel();
84 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
b4dc6ab5 85
3b2b638f 86 color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR);
b4dc6ab5
NR
87 if (color != null) {
88 setBackground(color);
4310bae9 89 pane.setBackground(color);
b4dc6ab5 90 }
d3c84ac3 91
4310bae9 92 JScrollPane scroll = new JScrollPane(pane);
b4dc6ab5
NR
93 scroll.getVerticalScrollBar().setUnitIncrement(16);
94 add(scroll, BorderLayout.CENTER);
a6395bef 95
3b2b638f
NR
96 pgBar = new ProgressBar();
97 add(pgBar, BorderLayout.SOUTH);
98
333f0e7b
NR
99 setJMenuBar(createMenu());
100
4310bae9
NR
101 booksByType = new HashMap<LocalReaderGroup, String>();
102 booksByAuthor = new HashMap<LocalReaderGroup, String>();
103
104 addBookPane(type, true);
105 refreshBooks();
106
333f0e7b
NR
107 setVisible(true);
108 }
109
4d205683 110 /**
4310bae9
NR
111 * Add a new {@link LocalReaderGroup} on the frame to display the books of
112 * the selected type or author.
4d205683 113 *
4310bae9
NR
114 * @param value
115 * the author or the type
4d205683 116 * @param type
4310bae9 117 * TRUE for type, FALSE for author
4d205683 118 */
4310bae9
NR
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 }
b4dc6ab5
NR
133 }
134
4310bae9
NR
135 return;
136 }
4d205683 137
4310bae9
NR
138 LocalReaderGroup bookPane = new LocalReaderGroup(reader, value, color);
139 if (type) {
140 booksByType.put(bookPane, value);
141 } else {
142 booksByAuthor.put(bookPane, value);
143 }
333f0e7b 144
4310bae9
NR
145 this.invalidate();
146 pane.invalidate();
147 pane.add(bookPane);
148 pane.validate();
149 this.validate();
9843a5e5 150
4310bae9
NR
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 }
a6395bef 173
4310bae9
NR
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);
a6395bef
NR
201 }
202
4310bae9
NR
203 pane.repaint();
204 this.repaint();
333f0e7b
NR
205 }
206
4d205683
NR
207 /**
208 * Create the main menu bar.
209 *
210 * @return the bar
211 */
333f0e7b 212 private JMenuBar createMenu() {
3b2b638f 213 bar = new JMenuBar();
333f0e7b
NR
214
215 JMenu file = new JMenu("File");
10d558d2 216 file.setMnemonic(KeyEvent.VK_F);
333f0e7b 217
22848428 218 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
333f0e7b
NR
219 imprt.addActionListener(new ActionListener() {
220 public void actionPerformed(ActionEvent e) {
10d558d2
NR
221 imprt(true);
222 }
223 });
22848428 224 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
10d558d2
NR
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 });
3b2b638f 237
9843a5e5
NR
238 file.add(createMenuItemOpenBook());
239 file.add(createMenuItemExport());
240 file.addSeparator();
10d558d2
NR
241 file.add(imprt);
242 file.add(imprtF);
243 file.addSeparator();
244 file.add(exit);
3b2b638f 245
10d558d2
NR
246 bar.add(file);
247
248 JMenu edit = new JMenu("Edit");
249 edit.setMnemonic(KeyEvent.VK_E);
250
22848428
NR
251 edit.add(createMenuItemClearCache());
252 edit.add(createMenuItemRedownload());
9843a5e5
NR
253 edit.addSeparator();
254 edit.add(createMenuItemDelete());
255
256 bar.add(edit);
257
4310bae9
NR
258 JMenu sources = new JMenu("Sources");
259 sources.setMnemonic(KeyEvent.VK_S);
9843a5e5
NR
260
261 List<String> tt = Instance.getLibrary().getTypes();
262 tt.add(0, null);
263 for (final String type : tt) {
4310bae9 264 JMenuItem item = new JMenuItem(type == null ? "All" : type);
9843a5e5
NR
265 item.addActionListener(new ActionListener() {
266 public void actionPerformed(ActionEvent e) {
4310bae9
NR
267 removeBookPanes();
268 addBookPane(type, true);
269 refreshBooks();
9843a5e5
NR
270 }
271 });
4310bae9 272 sources.add(item);
9843a5e5
NR
273
274 if (type == null) {
4310bae9 275 sources.addSeparator();
9843a5e5
NR
276 }
277 }
10d558d2 278
4310bae9
NR
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);
9843a5e5
NR
304
305 return bar;
306 }
307
4d205683
NR
308 /**
309 * Create the export menu item.
310 *
311 * @return the item
312 */
9843a5e5 313 private JMenuItem createMenuItemExport() {
4d205683
NR
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);
b2612f9d 321
4d205683
NR
322 if (ext == null || ext.isEmpty()) {
323 filters.put(createAllFilter(desc), type);
324 } else {
325 filters.put(new FileNameExtensionFilter(desc, ext), type);
326 }
327 }
328
329 // First the "ALL" filters, then, the extension filters
330 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
331 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
332 fc.addChoosableFileFilter(entry.getKey());
333 }
334 }
335 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
336 if (entry.getKey() instanceof FileNameExtensionFilter) {
337 fc.addChoosableFileFilter(entry.getKey());
338 }
339 }
340 //
9843a5e5 341
4d205683 342 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
10d558d2
NR
343 export.addActionListener(new ActionListener() {
344 public void actionPerformed(ActionEvent e) {
4d205683
NR
345 if (selectedBook != null) {
346 fc.showDialog(LocalReaderFrame.this, "Save");
b2612f9d
NR
347 if (fc.getSelectedFile() != null) {
348 final OutputType type = filters.get(fc.getFileFilter());
349 final String path = fc.getSelectedFile()
350 .getAbsolutePath()
351 + type.getDefaultExtension(false);
352 final Progress pg = new Progress();
353 outOfUi(pg, new Runnable() {
354 public void run() {
355 try {
356 Instance.getLibrary().export(
357 selectedBook.getMeta().getLuid(),
358 type, path, pg);
359 } catch (IOException e) {
360 Instance.syserr(e);
361 }
4d205683 362 }
b2612f9d
NR
363 });
364 }
4d205683 365 }
10d558d2
NR
366 }
367 });
368
9843a5e5
NR
369 return export;
370 }
371
edd46289
NR
372 /**
373 * Create a {@link FileFilter} that accepts all files and return the given
374 * description.
375 *
376 * @param desc
377 * the description
378 *
379 * @return the filter
380 */
4d205683
NR
381 private FileFilter createAllFilter(final String desc) {
382 return new FileFilter() {
383 @Override
384 public String getDescription() {
385 return desc;
386 }
387
388 @Override
389 public boolean accept(File f) {
390 return true;
391 }
392 };
393 }
394
395 /**
396 * Create the refresh (delete cache) menu item.
397 *
398 * @return the item
399 */
22848428 400 private JMenuItem createMenuItemClearCache() {
4d205683 401 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
10d558d2
NR
402 refresh.addActionListener(new ActionListener() {
403 public void actionPerformed(ActionEvent e) {
404 if (selectedBook != null) {
405 outOfUi(null, new Runnable() {
406 public void run() {
22848428 407 reader.refresh(selectedBook.getMeta().getLuid());
10d558d2 408 selectedBook.setCached(false);
3b2b638f
NR
409 SwingUtilities.invokeLater(new Runnable() {
410 public void run() {
10d558d2 411 selectedBook.repaint();
3b2b638f
NR
412 }
413 });
414 }
415 });
333f0e7b
NR
416 }
417 }
418 });
10d558d2 419
9843a5e5
NR
420 return refresh;
421 }
422
22848428
NR
423 /**
424 * Create the redownload (then delete original) menu item.
425 *
426 * @return the item
427 */
428 private JMenuItem createMenuItemRedownload() {
429 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
430 refresh.addActionListener(new ActionListener() {
431 public void actionPerformed(ActionEvent e) {
432 if (selectedBook != null) {
433 imprt(selectedBook.getMeta().getUrl(), new Runnable() {
434 public void run() {
435 reader.delete(selectedBook.getMeta().getLuid());
436 selectedBook = null;
437 }
438 });
439 }
440 }
441 });
442
443 return refresh;
444 }
445
4d205683
NR
446 /**
447 * Create the delete menu item.
448 *
449 * @return the item
450 */
9843a5e5 451 private JMenuItem createMenuItemDelete() {
10d558d2
NR
452 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
453 delete.addActionListener(new ActionListener() {
454 public void actionPerformed(ActionEvent e) {
455 if (selectedBook != null) {
456 outOfUi(null, new Runnable() {
457 public void run() {
22848428 458 reader.delete(selectedBook.getMeta().getLuid());
10d558d2
NR
459 selectedBook = null;
460 SwingUtilities.invokeLater(new Runnable() {
461 public void run() {
4310bae9 462 refreshBooks();
10d558d2
NR
463 }
464 });
465 }
466 });
467 }
468 }
469 });
470
9843a5e5
NR
471 return delete;
472 }
10d558d2 473
4d205683
NR
474 /**
475 * Create the open menu item.
476 *
477 * @return the item
478 */
9843a5e5
NR
479 private JMenuItem createMenuItemOpenBook() {
480 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
481 open.addActionListener(new ActionListener() {
482 public void actionPerformed(ActionEvent e) {
483 if (selectedBook != null) {
484 openBook(selectedBook);
485 }
486 }
487 });
10d558d2 488
9843a5e5
NR
489 return open;
490 }
10d558d2 491
4d205683
NR
492 /**
493 * Open a {@link LocalReaderBook} item.
494 *
495 * @param book
496 * the {@link LocalReaderBook} to open
497 */
9843a5e5
NR
498 private void openBook(final LocalReaderBook book) {
499 final Progress pg = new Progress();
500 outOfUi(pg, new Runnable() {
501 public void run() {
502 try {
22848428 503 reader.open(book.getMeta().getLuid(), pg);
edd46289
NR
504 SwingUtilities.invokeLater(new Runnable() {
505 public void run() {
506 book.setCached(true);
9843a5e5 507 }
edd46289 508 });
9843a5e5 509 } catch (IOException e) {
edd46289 510 // TODO: error message?
9843a5e5 511 Instance.syserr(e);
333f0e7b 512 }
10d558d2 513 }
9843a5e5 514 });
a6395bef 515 }
3b2b638f 516
4d205683
NR
517 /**
518 * Process the given action out of the Swing UI thread and link the given
519 * {@link ProgressBar} to the action.
520 * <p>
521 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
522 * to done when the action is done.
523 *
524 * @param pg
525 * the {@link ProgressBar} or NULL
526 * @param run
527 * the action to run
528 */
3b2b638f
NR
529 private void outOfUi(final Progress pg, final Runnable run) {
530 pgBar.setProgress(pg);
531
a4143cd7
NR
532 setEnabled(false);
533 pgBar.addActioListener(new ActionListener() {
534 public void actionPerformed(ActionEvent e) {
535 pgBar.setProgress(null);
536 setEnabled(true);
3b2b638f
NR
537 }
538 });
539
540 new Thread(new Runnable() {
541 public void run() {
542 run.run();
10d558d2
NR
543 if (pg == null) {
544 SwingUtilities.invokeLater(new Runnable() {
545 public void run() {
4d205683 546 setEnabled(true);
10d558d2
NR
547 }
548 });
549 } else if (!pg.isDone()) {
3b2b638f
NR
550 pg.setProgress(pg.getMax());
551 }
552 }
553 }).start();
554 }
555
4d205683
NR
556 /**
557 * Import a {@link Story} into the main {@link Library}.
22848428
NR
558 * <p>
559 * Should be called inside the UI thread.
4d205683
NR
560 *
561 * @param askUrl
562 * TRUE for an {@link URL}, false for a {@link File}
563 */
10d558d2
NR
564 private void imprt(boolean askUrl) {
565 JFileChooser fc = new JFileChooser();
566
5130ce84 567 Object url;
10d558d2 568 if (askUrl) {
5130ce84
NR
569 String clipboard = "";
570 try {
571 clipboard = ("" + Toolkit.getDefaultToolkit()
572 .getSystemClipboard().getData(DataFlavor.stringFlavor))
573 .trim();
574 } catch (Exception e) {
575 // No data will be handled
576 }
577
578 if (clipboard == null || !clipboard.startsWith("http")) {
579 clipboard = "";
580 }
581
10d558d2
NR
582 url = JOptionPane.showInputDialog(LocalReaderFrame.this,
583 "url of the story to import?", "Importing from URL",
5130ce84 584 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
10d558d2
NR
585 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
586 url = fc.getSelectedFile().getAbsolutePath();
587 } else {
588 url = null;
589 }
590
5130ce84
NR
591 if (url != null && !url.toString().isEmpty()) {
592 imprt(url.toString(), null);
22848428
NR
593 }
594 }
10d558d2 595
22848428
NR
596 /**
597 * Actually import the {@link Story} into the main {@link Library}.
598 * <p>
599 * Should be called inside the UI thread.
600 *
601 * @param url
602 * the {@link Story} to import by {@link URL}
603 * @param onSuccess
604 * Action to execute on success
605 */
606 private void imprt(final String url, final Runnable onSuccess) {
607 final Progress pg = new Progress("Importing " + url);
608 outOfUi(pg, new Runnable() {
609 public void run() {
610 Exception ex = null;
611 try {
612 Instance.getLibrary().imprt(BasicReader.getUrl(url), pg);
613 } catch (IOException e) {
614 ex = e;
615 }
10d558d2 616
22848428
NR
617 final Exception e = ex;
618
619 final boolean ok = (e == null);
620 SwingUtilities.invokeLater(new Runnable() {
621 public void run() {
622 if (!ok) {
a4143cd7 623 Instance.syserr(e);
22848428
NR
624 JOptionPane.showMessageDialog(
625 LocalReaderFrame.this, "Cannot import: "
626 + url, e.getMessage(),
627 JOptionPane.ERROR_MESSAGE);
628
629 setEnabled(true);
630 } else {
4310bae9 631 refreshBooks();
22848428
NR
632 if (onSuccess != null) {
633 onSuccess.run();
4310bae9 634 refreshBooks();
10d558d2
NR
635 }
636 }
22848428
NR
637 }
638 });
639 }
640 });
10d558d2
NR
641 }
642
4d205683
NR
643 /**
644 * Enables or disables this component, depending on the value of the
645 * parameter <code>b</code>. An enabled component can respond to user input
646 * and generate events. Components are enabled initially by default.
647 * <p>
648 * Disabling this component will also affect its children.
649 *
650 * @param b
651 * If <code>true</code>, this component is enabled; otherwise
652 * this component is disabled
653 */
654 @Override
655 public void setEnabled(boolean b) {
4d205683 656 bar.setEnabled(b);
4310bae9
NR
657 for (LocalReaderGroup group : booksByType.keySet()) {
658 group.setEnabled(b);
659 }
660 for (LocalReaderGroup group : booksByAuthor.keySet()) {
661 group.setEnabled(b);
662 }
4d205683 663 super.setEnabled(b);
3b2b638f
NR
664 repaint();
665 }
a6395bef 666}