7d5e5ddcca45ab18b0fc36cf87b8cf2e7f3fcdec
[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(createMenuItemRefresh());
140 popup.addSeparator();
141 popup.add(createMenuItemDelete());
142 popup.show(e.getComponent(), e.getX(), e.getY());
143 }
144
145 public void action(final LocalReaderBook book) {
146 openBook(book);
147 }
148 });
149
150 bookPane.add(book);
151 }
152
153 bookPane.validate();
154 bookPane.repaint();
155 }
156
157 /**
158 * Create the main menu bar.
159 *
160 * @return the bar
161 */
162 private JMenuBar createMenu() {
163 bar = new JMenuBar();
164
165 JMenu file = new JMenu("File");
166 file.setMnemonic(KeyEvent.VK_F);
167
168 JMenuItem imprt = new JMenuItem("Import URL", KeyEvent.VK_U);
169 imprt.addActionListener(new ActionListener() {
170 public void actionPerformed(ActionEvent e) {
171 imprt(true);
172 }
173 });
174 JMenuItem imprtF = new JMenuItem("Import File", KeyEvent.VK_F);
175 imprtF.addActionListener(new ActionListener() {
176 public void actionPerformed(ActionEvent e) {
177 imprt(false);
178 }
179 });
180 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
181 exit.addActionListener(new ActionListener() {
182 public void actionPerformed(ActionEvent e) {
183 LocalReaderFrame.this.dispatchEvent(new WindowEvent(
184 LocalReaderFrame.this, WindowEvent.WINDOW_CLOSING));
185 }
186 });
187
188 file.add(createMenuItemOpenBook());
189 file.add(createMenuItemExport());
190 file.addSeparator();
191 file.add(imprt);
192 file.add(imprtF);
193 file.addSeparator();
194 file.add(exit);
195
196 bar.add(file);
197
198 JMenu edit = new JMenu("Edit");
199 edit.setMnemonic(KeyEvent.VK_E);
200
201 edit.add(createMenuItemRefresh());
202 edit.addSeparator();
203 edit.add(createMenuItemDelete());
204
205 bar.add(edit);
206
207 JMenu view = new JMenu("View");
208 view.setMnemonic(KeyEvent.VK_V);
209
210 List<String> tt = Instance.getLibrary().getTypes();
211 tt.add(0, null);
212 for (final String type : tt) {
213 JMenuItem item = new JMenuItem(type == null ? "All books" : type);
214 item.addActionListener(new ActionListener() {
215 public void actionPerformed(ActionEvent e) {
216 refreshBooks(type);
217 }
218 });
219 view.add(item);
220
221 if (type == null) {
222 view.addSeparator();
223 }
224 }
225
226 bar.add(view);
227
228 return bar;
229 }
230
231 /**
232 * Create the export menu item.
233 *
234 * @return the item
235 */
236 private JMenuItem createMenuItemExport() {
237 final JFileChooser fc = new JFileChooser();
238 fc.setAcceptAllFileFilterUsed(false);
239
240 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
241 for (OutputType type : OutputType.values()) {
242 String ext = type.getDefaultExtension(false);
243 String desc = type.getDesc(false);
244 if (ext == null || ext.isEmpty()) {
245 filters.put(createAllFilter(desc), type);
246 } else {
247 filters.put(new FileNameExtensionFilter(desc, ext), type);
248 }
249 }
250
251 // First the "ALL" filters, then, the extension filters
252 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
253 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
254 fc.addChoosableFileFilter(entry.getKey());
255 }
256 }
257 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
258 if (entry.getKey() instanceof FileNameExtensionFilter) {
259 fc.addChoosableFileFilter(entry.getKey());
260 }
261 }
262 //
263
264 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
265 export.addActionListener(new ActionListener() {
266 public void actionPerformed(ActionEvent e) {
267 if (selectedBook != null) {
268 fc.showDialog(LocalReaderFrame.this, "Save");
269 final OutputType type = filters.get(fc.getFileFilter());
270 final String path = fc.getSelectedFile().getAbsolutePath()
271 + type.getDefaultExtension(false);
272 final Progress pg = new Progress();
273 outOfUi(pg, new Runnable() {
274 public void run() {
275 try {
276 Instance.getLibrary().export(
277 selectedBook.getLuid(), type, path, pg);
278 } catch (IOException e) {
279 Instance.syserr(e);
280 }
281 }
282 });
283 }
284 }
285 });
286
287 return export;
288 }
289
290 /**
291 * Create a {@link FileFilter} that accepts all files and return the given
292 * description.
293 *
294 * @param desc
295 * the description
296 *
297 * @return the filter
298 */
299 private FileFilter createAllFilter(final String desc) {
300 return new FileFilter() {
301 @Override
302 public String getDescription() {
303 return desc;
304 }
305
306 @Override
307 public boolean accept(File f) {
308 return true;
309 }
310 };
311 }
312
313 /**
314 * Create the refresh (delete cache) menu item.
315 *
316 * @return the item
317 */
318 private JMenuItem createMenuItemRefresh() {
319 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
320 refresh.addActionListener(new ActionListener() {
321 public void actionPerformed(ActionEvent e) {
322 if (selectedBook != null) {
323 outOfUi(null, new Runnable() {
324 public void run() {
325 reader.refresh(selectedBook.getLuid());
326 selectedBook.setCached(false);
327 SwingUtilities.invokeLater(new Runnable() {
328 public void run() {
329 selectedBook.repaint();
330 }
331 });
332 }
333 });
334 }
335 }
336 });
337
338 return refresh;
339 }
340
341 /**
342 * Create the delete menu item.
343 *
344 * @return the item
345 */
346 private JMenuItem createMenuItemDelete() {
347 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
348 delete.addActionListener(new ActionListener() {
349 public void actionPerformed(ActionEvent e) {
350 if (selectedBook != null) {
351 outOfUi(null, new Runnable() {
352 public void run() {
353 reader.delete(selectedBook.getLuid());
354 selectedBook = null;
355 SwingUtilities.invokeLater(new Runnable() {
356 public void run() {
357 refreshBooks(type);
358 }
359 });
360 }
361 });
362 }
363 }
364 });
365
366 return delete;
367 }
368
369 /**
370 * Create the open menu item.
371 *
372 * @return the item
373 */
374 private JMenuItem createMenuItemOpenBook() {
375 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
376 open.addActionListener(new ActionListener() {
377 public void actionPerformed(ActionEvent e) {
378 if (selectedBook != null) {
379 openBook(selectedBook);
380 }
381 }
382 });
383
384 return open;
385 }
386
387 /**
388 * Open a {@link LocalReaderBook} item.
389 *
390 * @param book
391 * the {@link LocalReaderBook} to open
392 */
393 private void openBook(final LocalReaderBook book) {
394 final Progress pg = new Progress();
395 outOfUi(pg, new Runnable() {
396 public void run() {
397 try {
398 reader.open(book.getLuid(), pg);
399 SwingUtilities.invokeLater(new Runnable() {
400 public void run() {
401 book.setCached(true);
402 }
403 });
404 } catch (IOException e) {
405 // TODO: error message?
406 Instance.syserr(e);
407 }
408 }
409 });
410 }
411
412 /**
413 * Process the given action out of the Swing UI thread and link the given
414 * {@link ProgressBar} to the action.
415 * <p>
416 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
417 * to done when the action is done.
418 *
419 * @param pg
420 * the {@link ProgressBar} or NULL
421 * @param run
422 * the action to run
423 */
424 private void outOfUi(final Progress pg, final Runnable run) {
425 pgBar.setProgress(pg);
426
427 SwingUtilities.invokeLater(new Runnable() {
428 public void run() {
429 setEnabled(false);
430 pgBar.addActioListener(new ActionListener() {
431 public void actionPerformed(ActionEvent e) {
432 pgBar.setProgress(null);
433 setEnabled(true);
434 }
435 });
436 }
437 });
438
439 new Thread(new Runnable() {
440 public void run() {
441 run.run();
442 if (pg == null) {
443 SwingUtilities.invokeLater(new Runnable() {
444 public void run() {
445 setEnabled(true);
446 }
447 });
448 } else if (!pg.isDone()) {
449 pg.setProgress(pg.getMax());
450 }
451 }
452 }).start();
453 }
454
455 /**
456 * Import a {@link Story} into the main {@link Library}.
457 *
458 * @param askUrl
459 * TRUE for an {@link URL}, false for a {@link File}
460 */
461 private void imprt(boolean askUrl) {
462 JFileChooser fc = new JFileChooser();
463
464 final String url;
465 if (askUrl) {
466 url = JOptionPane.showInputDialog(LocalReaderFrame.this,
467 "url of the story to import?", "Importing from URL",
468 JOptionPane.QUESTION_MESSAGE);
469 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
470 url = fc.getSelectedFile().getAbsolutePath();
471 } else {
472 url = null;
473 }
474
475 if (url != null && !url.isEmpty()) {
476 final Progress pg = new Progress("Importing " + url);
477 outOfUi(pg, new Runnable() {
478 public void run() {
479 Exception ex = null;
480 try {
481 Instance.getLibrary()
482 .imprt(BasicReader.getUrl(url), pg);
483 } catch (IOException e) {
484 ex = e;
485 }
486
487 final Exception e = ex;
488
489 final boolean ok = (e == null);
490 SwingUtilities.invokeLater(new Runnable() {
491 public void run() {
492 if (!ok) {
493 JOptionPane.showMessageDialog(
494 LocalReaderFrame.this, e.getMessage(),
495 "Cannot import: " + url,
496 JOptionPane.ERROR_MESSAGE);
497 } else {
498 refreshBooks(type);
499 }
500 }
501 });
502 }
503 });
504 }
505 }
506
507 /**
508 * Enables or disables this component, depending on the value of the
509 * parameter <code>b</code>. An enabled component can respond to user input
510 * and generate events. Components are enabled initially by default.
511 * <p>
512 * Disabling this component will also affect its children.
513 *
514 * @param b
515 * If <code>true</code>, this component is enabled; otherwise
516 * this component is disabled
517 */
518 @Override
519 public void setEnabled(boolean b) {
520 for (LocalReaderBook book : books) {
521 book.setEnabled(b);
522 book.repaint();
523 }
524
525 bar.setEnabled(b);
526 bookPane.setEnabled(b);
527 bookPane.repaint();
528
529 super.setEnabled(b);
530 repaint();
531 }
532 }