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