Version 1.2.4: fixes, new "Re-download" UI option
[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;
a6395bef
NR
6import java.awt.event.ActionEvent;
7import java.awt.event.ActionListener;
333f0e7b 8import java.awt.event.KeyEvent;
9843a5e5 9import java.awt.event.MouseEvent;
333f0e7b
NR
10import java.awt.event.WindowEvent;
11import java.io.File;
a6395bef 12import java.io.IOException;
4d205683 13import java.net.URL;
333f0e7b 14import java.util.ArrayList;
4d205683 15import java.util.HashMap;
a6395bef 16import java.util.List;
4d205683
NR
17import java.util.Map;
18import java.util.Map.Entry;
a6395bef 19
10d558d2 20import javax.swing.JFileChooser;
a6395bef 21import javax.swing.JFrame;
333f0e7b
NR
22import javax.swing.JMenu;
23import javax.swing.JMenuBar;
24import javax.swing.JMenuItem;
25import javax.swing.JOptionPane;
26import javax.swing.JPanel;
9843a5e5 27import javax.swing.JPopupMenu;
d3c84ac3 28import javax.swing.JScrollPane;
3b2b638f 29import javax.swing.SwingUtilities;
4d205683
NR
30import javax.swing.filechooser.FileFilter;
31import javax.swing.filechooser.FileNameExtensionFilter;
a6395bef
NR
32
33import be.nikiroo.fanfix.Instance;
4d205683 34import be.nikiroo.fanfix.Library;
b4dc6ab5 35import be.nikiroo.fanfix.bundles.UiConfig;
a6395bef 36import be.nikiroo.fanfix.data.MetaData;
4d205683
NR
37import be.nikiroo.fanfix.data.Story;
38import be.nikiroo.fanfix.output.BasicOutput.OutputType;
92fb0719 39import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener;
3b2b638f
NR
40import be.nikiroo.utils.Progress;
41import be.nikiroo.utils.ui.ProgressBar;
33ab6d12 42import be.nikiroo.utils.ui.WrapLayout;
a6395bef 43
4d205683
NR
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 */
a6395bef
NR
52class LocalReaderFrame extends JFrame {
53 private static final long serialVersionUID = 1L;
54 private LocalReader reader;
333f0e7b
NR
55 private List<MetaData> stories;
56 private List<LocalReaderBook> books;
57 private JPanel bookPane;
58 private String type;
b4dc6ab5 59 private Color color;
3b2b638f
NR
60 private ProgressBar pgBar;
61 private JMenuBar bar;
10d558d2 62 private LocalReaderBook selectedBook;
a6395bef 63
4d205683
NR
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 */
333f0e7b 73 public LocalReaderFrame(LocalReader reader, String type) {
b4dc6ab5 74 super("Fanfix Library");
a6395bef
NR
75
76 this.reader = reader;
77
78 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
79 setSize(800, 600);
d3c84ac3 80 setLayout(new BorderLayout());
a6395bef 81
333f0e7b 82 books = new ArrayList<LocalReaderBook>();
b4dc6ab5
NR
83 bookPane = new JPanel(new WrapLayout(WrapLayout.LEADING, 5, 5));
84
3b2b638f 85 color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR);
b4dc6ab5
NR
86
87 if (color != null) {
88 setBackground(color);
89 bookPane.setBackground(color);
90 }
d3c84ac3 91
b4dc6ab5
NR
92 JScrollPane scroll = new JScrollPane(bookPane);
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 refreshBooks(type);
100 setJMenuBar(createMenu());
101
102 setVisible(true);
103 }
104
4d205683
NR
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 */
333f0e7b
NR
111 private void refreshBooks(String type) {
112 this.type = type;
113 stories = Instance.getLibrary().getList(type);
114 books.clear();
edd46289 115 bookPane.invalidate();
333f0e7b
NR
116 bookPane.removeAll();
117 for (MetaData meta : stories) {
10d558d2
NR
118 LocalReaderBook book = new LocalReaderBook(meta,
119 reader.isCached(meta.getLuid()));
b4dc6ab5
NR
120 if (color != null) {
121 book.setBackground(color);
122 }
123
333f0e7b 124 books.add(book);
4d205683 125
92fb0719 126 book.addActionListener(new BookActionListener() {
333f0e7b 127 public void select(LocalReaderBook book) {
10d558d2 128 selectedBook = book;
333f0e7b
NR
129 for (LocalReaderBook abook : books) {
130 abook.setSelected(abook == book);
131 }
132 }
133
9843a5e5
NR
134 public void popupRequested(LocalReaderBook book, MouseEvent e) {
135 JPopupMenu popup = new JPopupMenu();
136 popup.add(createMenuItemOpenBook());
137 popup.addSeparator();
138 popup.add(createMenuItemExport());
22848428
NR
139 popup.add(createMenuItemClearCache());
140 popup.add(createMenuItemRedownload());
9843a5e5
NR
141 popup.addSeparator();
142 popup.add(createMenuItemDelete());
143 popup.show(e.getComponent(), e.getX(), e.getY());
144 }
145
10d558d2 146 public void action(final LocalReaderBook book) {
9843a5e5 147 openBook(book);
a6395bef
NR
148 }
149 });
150
333f0e7b 151 bookPane.add(book);
a6395bef
NR
152 }
153
333f0e7b
NR
154 bookPane.validate();
155 bookPane.repaint();
156 }
157
4d205683
NR
158 /**
159 * Create the main menu bar.
160 *
161 * @return the bar
162 */
333f0e7b 163 private JMenuBar createMenu() {
3b2b638f 164 bar = new JMenuBar();
333f0e7b
NR
165
166 JMenu file = new JMenu("File");
10d558d2 167 file.setMnemonic(KeyEvent.VK_F);
333f0e7b 168
22848428 169 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
333f0e7b
NR
170 imprt.addActionListener(new ActionListener() {
171 public void actionPerformed(ActionEvent e) {
10d558d2
NR
172 imprt(true);
173 }
174 });
22848428 175 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
10d558d2
NR
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 });
3b2b638f 188
9843a5e5
NR
189 file.add(createMenuItemOpenBook());
190 file.add(createMenuItemExport());
191 file.addSeparator();
10d558d2
NR
192 file.add(imprt);
193 file.add(imprtF);
194 file.addSeparator();
195 file.add(exit);
3b2b638f 196
10d558d2
NR
197 bar.add(file);
198
199 JMenu edit = new JMenu("Edit");
200 edit.setMnemonic(KeyEvent.VK_E);
201
22848428
NR
202 edit.add(createMenuItemClearCache());
203 edit.add(createMenuItemRedownload());
9843a5e5
NR
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) {
9843a5e5
NR
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 }
10d558d2 227
9843a5e5
NR
228 bar.add(view);
229
230 return bar;
231 }
232
4d205683
NR
233 /**
234 * Create the export menu item.
235 *
236 * @return the item
237 */
9843a5e5 238 private JMenuItem createMenuItemExport() {
4d205683
NR
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 //
9843a5e5 265
4d205683 266 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
10d558d2
NR
267 export.addActionListener(new ActionListener() {
268 public void actionPerformed(ActionEvent e) {
4d205683
NR
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(
22848428
NR
279 selectedBook.getMeta().getLuid(), type,
280 path, pg);
4d205683
NR
281 } catch (IOException e) {
282 Instance.syserr(e);
283 }
284 }
285 });
286 }
10d558d2
NR
287 }
288 });
289
9843a5e5
NR
290 return export;
291 }
292
edd46289
NR
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 */
4d205683
NR
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 */
22848428 321 private JMenuItem createMenuItemClearCache() {
4d205683 322 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
10d558d2
NR
323 refresh.addActionListener(new ActionListener() {
324 public void actionPerformed(ActionEvent e) {
325 if (selectedBook != null) {
326 outOfUi(null, new Runnable() {
327 public void run() {
22848428 328 reader.refresh(selectedBook.getMeta().getLuid());
10d558d2 329 selectedBook.setCached(false);
3b2b638f
NR
330 SwingUtilities.invokeLater(new Runnable() {
331 public void run() {
10d558d2 332 selectedBook.repaint();
3b2b638f
NR
333 }
334 });
335 }
336 });
333f0e7b
NR
337 }
338 }
339 });
10d558d2 340
9843a5e5
NR
341 return refresh;
342 }
343
22848428
NR
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
4d205683
NR
367 /**
368 * Create the delete menu item.
369 *
370 * @return the item
371 */
9843a5e5 372 private JMenuItem createMenuItemDelete() {
10d558d2
NR
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() {
22848428 379 reader.delete(selectedBook.getMeta().getLuid());
10d558d2
NR
380 selectedBook = null;
381 SwingUtilities.invokeLater(new Runnable() {
382 public void run() {
383 refreshBooks(type);
384 }
385 });
386 }
387 });
388 }
389 }
390 });
391
9843a5e5
NR
392 return delete;
393 }
10d558d2 394
4d205683
NR
395 /**
396 * Create the open menu item.
397 *
398 * @return the item
399 */
9843a5e5
NR
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 });
10d558d2 409
9843a5e5
NR
410 return open;
411 }
10d558d2 412
4d205683
NR
413 /**
414 * Open a {@link LocalReaderBook} item.
415 *
416 * @param book
417 * the {@link LocalReaderBook} to open
418 */
9843a5e5
NR
419 private void openBook(final LocalReaderBook book) {
420 final Progress pg = new Progress();
421 outOfUi(pg, new Runnable() {
422 public void run() {
423 try {
22848428 424 reader.open(book.getMeta().getLuid(), pg);
edd46289
NR
425 SwingUtilities.invokeLater(new Runnable() {
426 public void run() {
427 book.setCached(true);
9843a5e5 428 }
edd46289 429 });
9843a5e5 430 } catch (IOException e) {
edd46289 431 // TODO: error message?
9843a5e5 432 Instance.syserr(e);
333f0e7b 433 }
10d558d2 434 }
9843a5e5 435 });
a6395bef 436 }
3b2b638f 437
4d205683
NR
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 */
3b2b638f
NR
450 private void outOfUi(final Progress pg, final Runnable run) {
451 pgBar.setProgress(pg);
452
453 SwingUtilities.invokeLater(new Runnable() {
454 public void run() {
4d205683 455 setEnabled(false);
3b2b638f
NR
456 pgBar.addActioListener(new ActionListener() {
457 public void actionPerformed(ActionEvent e) {
458 pgBar.setProgress(null);
4d205683 459 setEnabled(true);
3b2b638f
NR
460 }
461 });
462 }
463 });
464
465 new Thread(new Runnable() {
466 public void run() {
467 run.run();
10d558d2
NR
468 if (pg == null) {
469 SwingUtilities.invokeLater(new Runnable() {
470 public void run() {
4d205683 471 setEnabled(true);
10d558d2
NR
472 }
473 });
474 } else if (!pg.isDone()) {
3b2b638f
NR
475 pg.setProgress(pg.getMax());
476 }
477 }
478 }).start();
479 }
480
4d205683
NR
481 /**
482 * Import a {@link Story} into the main {@link Library}.
22848428
NR
483 * <p>
484 * Should be called inside the UI thread.
4d205683
NR
485 *
486 * @param askUrl
487 * TRUE for an {@link URL}, false for a {@link File}
488 */
10d558d2
NR
489 private void imprt(boolean askUrl) {
490 JFileChooser fc = new JFileChooser();
491
22848428 492 String url;
10d558d2
NR
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()) {
22848428
NR
504 imprt(url, null);
505 }
506 }
10d558d2 507
22848428
NR
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 }
10d558d2 528
22848428
NR
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();
10d558d2
NR
545 refreshBooks(type);
546 }
547 }
22848428
NR
548 }
549 });
550 }
551 });
10d558d2
NR
552 }
553
4d205683
NR
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) {
3b2b638f 567 for (LocalReaderBook book : books) {
4d205683 568 book.setEnabled(b);
3b2b638f
NR
569 book.repaint();
570 }
10d558d2 571
4d205683
NR
572 bar.setEnabled(b);
573 bookPane.setEnabled(b);
3b2b638f 574 bookPane.repaint();
10d558d2 575
4d205683 576 super.setEnabled(b);
3b2b638f
NR
577 repaint();
578 }
a6395bef 579}