Fix breaks (<hr>) CSS style, GUI update bug fixed
[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;
a6395bef 5import java.awt.Desktop;
4d205683 6import java.awt.Frame;
a6395bef
NR
7import java.awt.event.ActionEvent;
8import java.awt.event.ActionListener;
333f0e7b 9import java.awt.event.KeyEvent;
9843a5e5 10import java.awt.event.MouseEvent;
333f0e7b
NR
11import java.awt.event.WindowEvent;
12import java.io.File;
a6395bef 13import java.io.IOException;
4d205683 14import java.net.URL;
333f0e7b 15import java.util.ArrayList;
4d205683 16import java.util.HashMap;
a6395bef 17import java.util.List;
4d205683
NR
18import java.util.Map;
19import java.util.Map.Entry;
a6395bef 20
10d558d2 21import javax.swing.JFileChooser;
a6395bef 22import javax.swing.JFrame;
333f0e7b
NR
23import javax.swing.JMenu;
24import javax.swing.JMenuBar;
25import javax.swing.JMenuItem;
26import javax.swing.JOptionPane;
27import javax.swing.JPanel;
9843a5e5 28import javax.swing.JPopupMenu;
d3c84ac3 29import javax.swing.JScrollPane;
3b2b638f 30import javax.swing.SwingUtilities;
4d205683
NR
31import javax.swing.filechooser.FileFilter;
32import javax.swing.filechooser.FileNameExtensionFilter;
a6395bef
NR
33
34import be.nikiroo.fanfix.Instance;
4d205683 35import be.nikiroo.fanfix.Library;
b4dc6ab5 36import be.nikiroo.fanfix.bundles.UiConfig;
a6395bef 37import be.nikiroo.fanfix.data.MetaData;
4d205683
NR
38import be.nikiroo.fanfix.data.Story;
39import be.nikiroo.fanfix.output.BasicOutput.OutputType;
92fb0719 40import be.nikiroo.fanfix.reader.LocalReaderBook.BookActionListener;
3b2b638f
NR
41import be.nikiroo.utils.Progress;
42import be.nikiroo.utils.ui.ProgressBar;
33ab6d12 43import be.nikiroo.utils.ui.WrapLayout;
a6395bef 44
4d205683
NR
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 */
a6395bef
NR
53class LocalReaderFrame extends JFrame {
54 private static final long serialVersionUID = 1L;
55 private LocalReader reader;
333f0e7b
NR
56 private List<MetaData> stories;
57 private List<LocalReaderBook> books;
58 private JPanel bookPane;
59 private String type;
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) {
b4dc6ab5 75 super("Fanfix Library");
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
333f0e7b 83 books = new ArrayList<LocalReaderBook>();
b4dc6ab5
NR
84 bookPane = new JPanel(new WrapLayout(WrapLayout.LEADING, 5, 5));
85
3b2b638f 86 color = Instance.getUiConfig().getColor(UiConfig.BACKGROUND_COLOR);
b4dc6ab5
NR
87
88 if (color != null) {
89 setBackground(color);
90 bookPane.setBackground(color);
91 }
d3c84ac3 92
b4dc6ab5
NR
93 JScrollPane scroll = new JScrollPane(bookPane);
94 scroll.getVerticalScrollBar().setUnitIncrement(16);
95 add(scroll, BorderLayout.CENTER);
a6395bef 96
3b2b638f
NR
97 pgBar = new ProgressBar();
98 add(pgBar, BorderLayout.SOUTH);
99
333f0e7b
NR
100 refreshBooks(type);
101 setJMenuBar(createMenu());
102
103 setVisible(true);
104 }
105
4d205683
NR
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 */
333f0e7b
NR
112 private void refreshBooks(String type) {
113 this.type = type;
114 stories = Instance.getLibrary().getList(type);
115 books.clear();
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());
139 popup.add(createMenuItemRefresh());
140 popup.addSeparator();
141 popup.add(createMenuItemDelete());
142 popup.show(e.getComponent(), e.getX(), e.getY());
143 }
144
10d558d2 145 public void action(final LocalReaderBook book) {
9843a5e5 146 openBook(book);
a6395bef
NR
147 }
148 });
149
333f0e7b 150 bookPane.add(book);
a6395bef
NR
151 }
152
333f0e7b
NR
153 bookPane.validate();
154 bookPane.repaint();
155 }
156
4d205683
NR
157 /**
158 * Create the main menu bar.
159 *
160 * @return the bar
161 */
333f0e7b 162 private JMenuBar createMenu() {
3b2b638f 163 bar = new JMenuBar();
333f0e7b
NR
164
165 JMenu file = new JMenu("File");
10d558d2 166 file.setMnemonic(KeyEvent.VK_F);
333f0e7b 167
10d558d2 168 JMenuItem imprt = new JMenuItem("Import URL", KeyEvent.VK_U);
333f0e7b
NR
169 imprt.addActionListener(new ActionListener() {
170 public void actionPerformed(ActionEvent e) {
10d558d2
NR
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 });
3b2b638f 187
9843a5e5
NR
188 file.add(createMenuItemOpenBook());
189 file.add(createMenuItemExport());
190 file.addSeparator();
10d558d2
NR
191 file.add(imprt);
192 file.add(imprtF);
193 file.addSeparator();
194 file.add(exit);
3b2b638f 195
10d558d2
NR
196 bar.add(file);
197
198 JMenu edit = new JMenu("Edit");
199 edit.setMnemonic(KeyEvent.VK_E);
200
9843a5e5
NR
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
214 JMenuItem item = new JMenuItem(type == null ? "All books" : type);
215 item.addActionListener(new ActionListener() {
216 public void actionPerformed(ActionEvent e) {
217 refreshBooks(type);
218 }
219 });
220 view.add(item);
221
222 if (type == null) {
223 view.addSeparator();
224 }
225 }
10d558d2 226
9843a5e5
NR
227 bar.add(view);
228
229 return bar;
230 }
231
4d205683
NR
232 /**
233 * Create the export menu item.
234 *
235 * @return the item
236 */
9843a5e5 237 private JMenuItem createMenuItemExport() {
4d205683
NR
238 final JFileChooser fc = new JFileChooser();
239 fc.setAcceptAllFileFilterUsed(false);
240
241 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
242 for (OutputType type : OutputType.values()) {
243 String ext = type.getDefaultExtension(false);
244 String desc = type.getDesc(false);
245 if (ext == null || ext.isEmpty()) {
246 filters.put(createAllFilter(desc), type);
247 } else {
248 filters.put(new FileNameExtensionFilter(desc, ext), type);
249 }
250 }
251
252 // First the "ALL" filters, then, the extension filters
253 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
254 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
255 fc.addChoosableFileFilter(entry.getKey());
256 }
257 }
258 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
259 if (entry.getKey() instanceof FileNameExtensionFilter) {
260 fc.addChoosableFileFilter(entry.getKey());
261 }
262 }
263 //
9843a5e5 264
4d205683 265 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
10d558d2
NR
266 export.addActionListener(new ActionListener() {
267 public void actionPerformed(ActionEvent e) {
4d205683
NR
268 if (selectedBook != null) {
269 fc.showDialog(LocalReaderFrame.this, "Save");
270 final OutputType type = filters.get(fc.getFileFilter());
271 final String path = fc.getSelectedFile().getAbsolutePath()
272 + type.getDefaultExtension(false);
273 final Progress pg = new Progress();
274 outOfUi(pg, new Runnable() {
275 public void run() {
276 try {
277 Instance.getLibrary().export(
278 selectedBook.getLuid(), type, path, pg);
279 } catch (IOException e) {
280 Instance.syserr(e);
281 }
282 }
283 });
284 }
10d558d2
NR
285 }
286 });
287
9843a5e5
NR
288 return export;
289 }
290
4d205683
NR
291 private FileFilter createAllFilter(final String desc) {
292 return new FileFilter() {
293 @Override
294 public String getDescription() {
295 return desc;
296 }
297
298 @Override
299 public boolean accept(File f) {
300 return true;
301 }
302 };
303 }
304
305 /**
306 * Create the refresh (delete cache) menu item.
307 *
308 * @return the item
309 */
9843a5e5 310 private JMenuItem createMenuItemRefresh() {
4d205683 311 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
10d558d2
NR
312 refresh.addActionListener(new ActionListener() {
313 public void actionPerformed(ActionEvent e) {
314 if (selectedBook != null) {
315 outOfUi(null, new Runnable() {
316 public void run() {
317 reader.refresh(selectedBook.getLuid());
318 selectedBook.setCached(false);
3b2b638f
NR
319 SwingUtilities.invokeLater(new Runnable() {
320 public void run() {
10d558d2 321 selectedBook.repaint();
3b2b638f
NR
322 }
323 });
324 }
325 });
333f0e7b
NR
326 }
327 }
328 });
10d558d2 329
9843a5e5
NR
330 return refresh;
331 }
332
4d205683
NR
333 /**
334 * Create the delete menu item.
335 *
336 * @return the item
337 */
9843a5e5 338 private JMenuItem createMenuItemDelete() {
10d558d2
NR
339 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
340 delete.addActionListener(new ActionListener() {
341 public void actionPerformed(ActionEvent e) {
342 if (selectedBook != null) {
343 outOfUi(null, new Runnable() {
344 public void run() {
345 reader.delete(selectedBook.getLuid());
346 selectedBook = null;
347 SwingUtilities.invokeLater(new Runnable() {
348 public void run() {
349 refreshBooks(type);
350 }
351 });
352 }
353 });
354 }
355 }
356 });
357
9843a5e5
NR
358 return delete;
359 }
10d558d2 360
4d205683
NR
361 /**
362 * Create the open menu item.
363 *
364 * @return the item
365 */
9843a5e5
NR
366 private JMenuItem createMenuItemOpenBook() {
367 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
368 open.addActionListener(new ActionListener() {
369 public void actionPerformed(ActionEvent e) {
370 if (selectedBook != null) {
371 openBook(selectedBook);
372 }
373 }
374 });
10d558d2 375
9843a5e5
NR
376 return open;
377 }
10d558d2 378
4d205683
NR
379 /**
380 * Open a {@link LocalReaderBook} item.
381 *
382 * @param book
383 * the {@link LocalReaderBook} to open
384 */
9843a5e5
NR
385 private void openBook(final LocalReaderBook book) {
386 final Progress pg = new Progress();
387 outOfUi(pg, new Runnable() {
388 public void run() {
389 try {
390 File target = LocalReaderFrame.this.reader.getTarget(
391 book.getLuid(), pg);
392 book.setCached(true);
393 // TODO: allow custom programs, with
394 // Desktop/xdg-open fallback
395 try {
396 Desktop.getDesktop().browse(target.toURI());
397 } catch (UnsupportedOperationException e) {
398 String browsers[] = new String[] { "xdg-open",
399 "epiphany", "konqueror", "firefox", "chrome",
400 "google-chrome", "mozilla" };
401
402 Runtime runtime = Runtime.getRuntime();
403 for (String browser : browsers) {
404 try {
405 runtime.exec(new String[] { browser,
406 target.getAbsolutePath() });
407 runtime = null;
408 break;
409 } catch (IOException ioe) {
410 // continue, try next browser
411 }
412 }
10d558d2 413
9843a5e5
NR
414 if (runtime != null) {
415 throw new IOException(
416 "Cannot find a working GUI browser...");
417 }
418 }
419 } catch (IOException e) {
420 Instance.syserr(e);
333f0e7b 421 }
10d558d2 422 }
9843a5e5 423 });
a6395bef 424 }
3b2b638f 425
4d205683
NR
426 /**
427 * Process the given action out of the Swing UI thread and link the given
428 * {@link ProgressBar} to the action.
429 * <p>
430 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
431 * to done when the action is done.
432 *
433 * @param pg
434 * the {@link ProgressBar} or NULL
435 * @param run
436 * the action to run
437 */
3b2b638f
NR
438 private void outOfUi(final Progress pg, final Runnable run) {
439 pgBar.setProgress(pg);
440
441 SwingUtilities.invokeLater(new Runnable() {
442 public void run() {
4d205683 443 setEnabled(false);
3b2b638f
NR
444 pgBar.addActioListener(new ActionListener() {
445 public void actionPerformed(ActionEvent e) {
446 pgBar.setProgress(null);
4d205683 447 setEnabled(true);
3b2b638f
NR
448 }
449 });
450 }
451 });
452
453 new Thread(new Runnable() {
454 public void run() {
455 run.run();
10d558d2
NR
456 if (pg == null) {
457 SwingUtilities.invokeLater(new Runnable() {
458 public void run() {
4d205683 459 setEnabled(true);
10d558d2
NR
460 }
461 });
462 } else if (!pg.isDone()) {
3b2b638f
NR
463 pg.setProgress(pg.getMax());
464 }
465 }
466 }).start();
467 }
468
4d205683
NR
469 /**
470 * Import a {@link Story} into the main {@link Library}.
471 *
472 * @param askUrl
473 * TRUE for an {@link URL}, false for a {@link File}
474 */
10d558d2
NR
475 private void imprt(boolean askUrl) {
476 JFileChooser fc = new JFileChooser();
477
478 final String url;
479 if (askUrl) {
480 url = JOptionPane.showInputDialog(LocalReaderFrame.this,
481 "url of the story to import?", "Importing from URL",
482 JOptionPane.QUESTION_MESSAGE);
483 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
484 url = fc.getSelectedFile().getAbsolutePath();
485 } else {
486 url = null;
487 }
488
489 if (url != null && !url.isEmpty()) {
490 final Progress pg = new Progress("Importing " + url);
491 outOfUi(pg, new Runnable() {
492 public void run() {
493 Exception ex = null;
494 try {
495 Instance.getLibrary()
496 .imprt(BasicReader.getUrl(url), pg);
497 } catch (IOException e) {
498 ex = e;
499 }
500
501 final Exception e = ex;
502
503 final boolean ok = (e == null);
504 SwingUtilities.invokeLater(new Runnable() {
505 public void run() {
506 if (!ok) {
507 JOptionPane.showMessageDialog(
508 LocalReaderFrame.this,
509 "Cannot import: " + url,
510 e.getMessage(),
511 JOptionPane.ERROR_MESSAGE);
512
4d205683 513 setEnabled(true);
10d558d2
NR
514 } else {
515 refreshBooks(type);
516 }
517 }
518 });
519 }
520 });
521 }
522 }
523
4d205683
NR
524 /**
525 * Enables or disables this component, depending on the value of the
526 * parameter <code>b</code>. An enabled component can respond to user input
527 * and generate events. Components are enabled initially by default.
528 * <p>
529 * Disabling this component will also affect its children.
530 *
531 * @param b
532 * If <code>true</code>, this component is enabled; otherwise
533 * this component is disabled
534 */
535 @Override
536 public void setEnabled(boolean b) {
3b2b638f 537 for (LocalReaderBook book : books) {
4d205683 538 book.setEnabled(b);
3b2b638f
NR
539 book.validate();
540 book.repaint();
541 }
10d558d2 542
4d205683
NR
543 bar.setEnabled(b);
544 bookPane.setEnabled(b);
3b2b638f
NR
545 bookPane.validate();
546 bookPane.repaint();
10d558d2 547
4d205683 548 super.setEnabled(b);
3b2b638f
NR
549 validate();
550 repaint();
551 }
a6395bef 552}