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