gui: add a Properties page
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderFrame.java
CommitLineData
16a81ef7 1package be.nikiroo.fanfix.reader.ui;
a6395bef 2
d3c84ac3 3import java.awt.BorderLayout;
b4dc6ab5 4import java.awt.Color;
df6e2d88 5import java.awt.Font;
4d205683 6import java.awt.Frame;
5130ce84
NR
7import java.awt.Toolkit;
8import java.awt.datatransfer.DataFlavor;
a6395bef
NR
9import java.awt.event.ActionEvent;
10import java.awt.event.ActionListener;
333f0e7b 11import java.awt.event.KeyEvent;
9843a5e5 12import java.awt.event.MouseEvent;
333f0e7b
NR
13import java.awt.event.WindowEvent;
14import java.io.File;
a6395bef 15import java.io.IOException;
4d205683 16import java.net.URL;
fd1d31c2 17import java.net.UnknownHostException;
70c9b112 18import java.util.ArrayList;
4d205683 19import java.util.HashMap;
a6395bef 20import java.util.List;
4d205683
NR
21import java.util.Map;
22import java.util.Map.Entry;
a6395bef 23
df6e2d88 24import javax.swing.BorderFactory;
4310bae9 25import javax.swing.BoxLayout;
df6e2d88 26import javax.swing.ImageIcon;
10d558d2 27import javax.swing.JFileChooser;
a6395bef 28import javax.swing.JFrame;
99ccbdf6 29import javax.swing.JLabel;
333f0e7b
NR
30import javax.swing.JMenu;
31import javax.swing.JMenuBar;
32import javax.swing.JMenuItem;
33import javax.swing.JOptionPane;
34import javax.swing.JPanel;
9843a5e5 35import javax.swing.JPopupMenu;
d3c84ac3 36import javax.swing.JScrollPane;
df6e2d88 37import javax.swing.JTextArea;
99ccbdf6 38import javax.swing.SwingConstants;
3b2b638f 39import javax.swing.SwingUtilities;
4d205683
NR
40import javax.swing.filechooser.FileFilter;
41import javax.swing.filechooser.FileNameExtensionFilter;
a6395bef
NR
42
43import be.nikiroo.fanfix.Instance;
e8eeea0a 44import be.nikiroo.fanfix.bundles.Config;
b4dc6ab5 45import be.nikiroo.fanfix.bundles.UiConfig;
a6395bef 46import be.nikiroo.fanfix.data.MetaData;
4d205683 47import be.nikiroo.fanfix.data.Story;
e6249b0f
NR
48import be.nikiroo.fanfix.library.BasicLibrary;
49import be.nikiroo.fanfix.library.BasicLibrary.Status;
e42573a0 50import be.nikiroo.fanfix.library.LocalLibrary;
4d205683 51import be.nikiroo.fanfix.output.BasicOutput.OutputType;
16a81ef7
NR
52import be.nikiroo.fanfix.reader.BasicReader;
53import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
3b2b638f 54import be.nikiroo.utils.Progress;
39c3c689 55import be.nikiroo.utils.Version;
e8eeea0a 56import be.nikiroo.utils.ui.ConfigEditor;
3b2b638f 57import be.nikiroo.utils.ui.ProgressBar;
a6395bef 58
4d205683 59/**
5dd985cf 60 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
4d205683 61 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
5dd985cf
NR
62 * way to copy them to the {@link GuiReader} cache (
63 * {@link BasicReader#getLibrary()}), read them, delete them...
4d205683
NR
64 *
65 * @author niki
66 */
5dd985cf 67class GuiReaderFrame extends JFrame {
a6395bef 68 private static final long serialVersionUID = 1L;
5dd985cf
NR
69 private GuiReader reader;
70 private Map<GuiReaderGroup, String> booksByType;
71 private Map<GuiReaderGroup, String> booksByAuthor;
4310bae9 72 private JPanel pane;
b4dc6ab5 73 private Color color;
3b2b638f
NR
74 private ProgressBar pgBar;
75 private JMenuBar bar;
5dd985cf 76 private GuiReaderBook selectedBook;
793f1071 77 private boolean words; // words or authors (secondary info on books)
a6395bef 78
c8faa52a
NR
79 /**
80 * A {@link Runnable} with a {@link Story} parameter.
81 *
82 * @author niki
83 */
84 private interface StoryRunnable {
85 /**
86 * Run the action.
87 *
88 * @param story
89 * the story
90 */
91 public void run(Story story);
92 }
93
4d205683 94 /**
5dd985cf 95 * Create a new {@link GuiReaderFrame}.
4d205683
NR
96 *
97 * @param reader
5dd985cf
NR
98 * the associated {@link GuiReader} to forward some commands and
99 * access its {@link LocalLibrary}
4d205683
NR
100 * @param type
101 * the type of {@link Story} to load, or NULL for all types
102 */
5dd985cf 103 public GuiReaderFrame(GuiReader reader, String type) {
39c3c689 104 super(String.format("Fanfix %s Library", Version.getCurrentVersion()));
a6395bef
NR
105
106 this.reader = reader;
107
108 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
109 setSize(800, 600);
d3c84ac3 110 setLayout(new BorderLayout());
a6395bef 111
4310bae9
NR
112 pane = new JPanel();
113 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
b4dc6ab5 114
16a81ef7
NR
115 Integer icolor = Instance.getUiConfig().getColor(
116 UiConfig.BACKGROUND_COLOR);
117 if (icolor != null) {
118 color = new Color(icolor);
b4dc6ab5 119 setBackground(color);
4310bae9 120 pane.setBackground(color);
b4dc6ab5 121 }
d3c84ac3 122
4310bae9 123 JScrollPane scroll = new JScrollPane(pane);
b4dc6ab5
NR
124 scroll.getVerticalScrollBar().setUnitIncrement(16);
125 add(scroll, BorderLayout.CENTER);
a6395bef 126
99ccbdf6
NR
127 String message = reader.getLibrary().getLibraryName();
128 if (!message.isEmpty()) {
129 JLabel name = new JLabel(message, SwingConstants.CENTER);
130 add(name, BorderLayout.NORTH);
131 }
132
3b2b638f
NR
133 pgBar = new ProgressBar();
134 add(pgBar, BorderLayout.SOUTH);
135
754a5bc2 136 pgBar.addActionListener(new ActionListener() {
211f7ddb 137 @Override
754a5bc2
NR
138 public void actionPerformed(ActionEvent e) {
139 invalidate();
140 pgBar.setProgress(null);
141 validate();
142 setEnabled(true);
143 }
144 });
145
146 pgBar.addUpdateListener(new ActionListener() {
211f7ddb 147 @Override
754a5bc2 148 public void actionPerformed(ActionEvent e) {
80791ae8
NR
149 invalidate();
150 validate();
754a5bc2
NR
151 repaint();
152 }
153 });
154
5dd985cf
NR
155 booksByType = new HashMap<GuiReaderGroup, String>();
156 booksByAuthor = new HashMap<GuiReaderGroup, String>();
4310bae9 157
4f661b2b
NR
158 pane.setVisible(false);
159 final Progress pg = new Progress();
160 final String typeF = type;
161 outOfUi(pg, new Runnable() {
211f7ddb 162 @Override
4f661b2b 163 public void run() {
e6249b0f
NR
164 BasicLibrary lib = GuiReaderFrame.this.reader.getLibrary();
165 Status status = lib.getStatus();
166
167 if (status == Status.READY) {
168 lib.refresh(pg);
169 invalidate();
170 setJMenuBar(createMenu(true));
171 addBookPane(typeF, true);
172 refreshBooks();
173 validate();
174 pane.setVisible(true);
175 } else {
176 invalidate();
177 setJMenuBar(createMenu(false));
178 validate();
179
180 String err = lib.getLibraryName() + "\n";
181 switch (status) {
182 case INVALID:
183 err += "Library not valid";
184 break;
185
186 case UNAUTORIZED:
187 err += "You are not allowed to access this library";
188 break;
189
190 case UNAVAILABLE:
c3b229a1 191 err += "Library currently unavailable";
e6249b0f
NR
192 break;
193
194 default:
195 err += "An error occured when contacting the library";
196 break;
197 }
198
199 error(err, "Library error", null);
200 }
4f661b2b
NR
201 }
202 });
4310bae9 203
333f0e7b
NR
204 setVisible(true);
205 }
206
14b57448
NR
207 private void addSourcePanes() {
208 // Sources -> i18n
209 GuiReaderGroup bookPane = new GuiReaderGroup(reader, "Sources", color);
210
211 List<MetaData> sources = new ArrayList<MetaData>();
212 for (String source : reader.getLibrary().getSources()) {
213 MetaData mSource = new MetaData();
214 mSource.setLuid(null);
215 mSource.setTitle(source);
216 mSource.setSource(source);
217 sources.add(mSource);
218 }
219
220 bookPane.refreshBooks(sources, false);
221
222 this.invalidate();
223 pane.invalidate();
224 pane.add(bookPane);
225 pane.validate();
226 this.validate();
227
228 bookPane.setActionListener(new BookActionListener() {
229 @Override
230 public void select(GuiReaderBook book) {
231 selectedBook = book;
232 }
233
234 @Override
235 public void popupRequested(GuiReaderBook book, MouseEvent e) {
236 JPopupMenu popup = new JPopupMenu();
237 popup.add(createMenuItemOpenBook());
238 popup.show(e.getComponent(), e.getX(), e.getY());
239 }
240
241 @Override
242 public void action(final GuiReaderBook book) {
243 removeBookPanes();
244 addBookPane(book.getMeta().getSource(), true);
245 refreshBooks();
246 }
247 });
248 }
249
4d205683 250 /**
5dd985cf
NR
251 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
252 * selected type or author.
4d205683 253 *
4310bae9 254 * @param value
70c9b112
NR
255 * the author or the type, or NULL to get all the
256 * authors-or-types
4d205683 257 * @param type
4310bae9 258 * TRUE for type, FALSE for author
4d205683 259 */
4310bae9
NR
260 private void addBookPane(String value, boolean type) {
261 if (value == null) {
262 if (type) {
14b57448
NR
263 if (Instance.getUiConfig().getBoolean(UiConfig.SOURCE_PAGE,
264 false)) {
265 addSourcePanes();
266 } else {
267 for (String tt : reader.getLibrary().getSources()) {
268 if (tt != null) {
269 addBookPane(tt, type);
270 }
4310bae9
NR
271 }
272 }
273 } else {
e42573a0 274 for (String tt : reader.getLibrary().getAuthors()) {
4310bae9
NR
275 if (tt != null) {
276 addBookPane(tt, type);
277 }
278 }
b4dc6ab5
NR
279 }
280
4310bae9
NR
281 return;
282 }
4d205683 283
5dd985cf 284 GuiReaderGroup bookPane = new GuiReaderGroup(reader, value, color);
4310bae9
NR
285 if (type) {
286 booksByType.put(bookPane, value);
287 } else {
288 booksByAuthor.put(bookPane, value);
289 }
333f0e7b 290
4310bae9
NR
291 this.invalidate();
292 pane.invalidate();
293 pane.add(bookPane);
294 pane.validate();
295 this.validate();
9843a5e5 296
4310bae9 297 bookPane.setActionListener(new BookActionListener() {
211f7ddb 298 @Override
5dd985cf 299 public void select(GuiReaderBook book) {
4310bae9
NR
300 selectedBook = book;
301 }
302
211f7ddb 303 @Override
5dd985cf 304 public void popupRequested(GuiReaderBook book, MouseEvent e) {
4310bae9
NR
305 JPopupMenu popup = new JPopupMenu();
306 popup.add(createMenuItemOpenBook());
307 popup.addSeparator();
308 popup.add(createMenuItemExport());
e6249b0f 309 popup.add(createMenuItemMove(true));
14b57448 310 popup.add(createMenuItemSetCover());
4310bae9
NR
311 popup.add(createMenuItemClearCache());
312 popup.add(createMenuItemRedownload());
313 popup.addSeparator();
314 popup.add(createMenuItemDelete());
df6e2d88
NR
315 popup.addSeparator();
316 popup.add(createMenuItemProperties());
4310bae9
NR
317 popup.show(e.getComponent(), e.getX(), e.getY());
318 }
319
211f7ddb 320 @Override
5dd985cf 321 public void action(final GuiReaderBook book) {
4310bae9
NR
322 openBook(book);
323 }
324 });
325 }
a6395bef 326
4310bae9
NR
327 private void removeBookPanes() {
328 booksByType.clear();
329 booksByAuthor.clear();
330 pane.invalidate();
331 this.invalidate();
332 pane.removeAll();
333 pane.validate();
334 this.validate();
335 }
336
337 /**
5dd985cf 338 * Refresh the list of {@link GuiReaderBook}s from disk.
4310bae9
NR
339 */
340 private void refreshBooks() {
5dd985cf 341 for (GuiReaderGroup group : booksByType.keySet()) {
e42573a0 342 List<MetaData> stories = reader.getLibrary().getListBySource(
4310bae9 343 booksByType.get(group));
793f1071 344 group.refreshBooks(stories, words);
4310bae9
NR
345 }
346
5dd985cf 347 for (GuiReaderGroup group : booksByAuthor.keySet()) {
e42573a0 348 List<MetaData> stories = reader.getLibrary().getListByAuthor(
4310bae9 349 booksByAuthor.get(group));
793f1071 350 group.refreshBooks(stories, words);
a6395bef
NR
351 }
352
4310bae9
NR
353 pane.repaint();
354 this.repaint();
333f0e7b
NR
355 }
356
4d205683
NR
357 /**
358 * Create the main menu bar.
359 *
e6249b0f
NR
360 * @param libOk
361 * the library can be queried
362 *
4d205683
NR
363 * @return the bar
364 */
e6249b0f 365 private JMenuBar createMenu(boolean libOk) {
3b2b638f 366 bar = new JMenuBar();
333f0e7b
NR
367
368 JMenu file = new JMenu("File");
10d558d2 369 file.setMnemonic(KeyEvent.VK_F);
333f0e7b 370
22848428 371 JMenuItem imprt = new JMenuItem("Import URL...", KeyEvent.VK_U);
333f0e7b 372 imprt.addActionListener(new ActionListener() {
211f7ddb 373 @Override
333f0e7b 374 public void actionPerformed(ActionEvent e) {
10d558d2
NR
375 imprt(true);
376 }
377 });
22848428 378 JMenuItem imprtF = new JMenuItem("Import File...", KeyEvent.VK_F);
10d558d2 379 imprtF.addActionListener(new ActionListener() {
211f7ddb 380 @Override
10d558d2
NR
381 public void actionPerformed(ActionEvent e) {
382 imprt(false);
383 }
384 });
385 JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X);
386 exit.addActionListener(new ActionListener() {
211f7ddb 387 @Override
10d558d2 388 public void actionPerformed(ActionEvent e) {
5dd985cf
NR
389 GuiReaderFrame.this.dispatchEvent(new WindowEvent(
390 GuiReaderFrame.this, WindowEvent.WINDOW_CLOSING));
10d558d2
NR
391 }
392 });
3b2b638f 393
9843a5e5
NR
394 file.add(createMenuItemOpenBook());
395 file.add(createMenuItemExport());
e6249b0f 396 file.add(createMenuItemMove(libOk));
9843a5e5 397 file.addSeparator();
10d558d2
NR
398 file.add(imprt);
399 file.add(imprtF);
400 file.addSeparator();
401 file.add(exit);
3b2b638f 402
10d558d2
NR
403 bar.add(file);
404
405 JMenu edit = new JMenu("Edit");
406 edit.setMnemonic(KeyEvent.VK_E);
407
22848428
NR
408 edit.add(createMenuItemClearCache());
409 edit.add(createMenuItemRedownload());
9843a5e5
NR
410 edit.addSeparator();
411 edit.add(createMenuItemDelete());
412
413 bar.add(edit);
414
793f1071
NR
415 JMenu view = new JMenu("View");
416 view.setMnemonic(KeyEvent.VK_V);
417 JMenuItem vauthors = new JMenuItem("Author");
418 vauthors.setMnemonic(KeyEvent.VK_A);
419 vauthors.addActionListener(new ActionListener() {
211f7ddb 420 @Override
793f1071
NR
421 public void actionPerformed(ActionEvent e) {
422 words = false;
423 refreshBooks();
424 }
425 });
426 view.add(vauthors);
427 JMenuItem vwords = new JMenuItem("Word count");
428 vwords.setMnemonic(KeyEvent.VK_W);
429 vwords.addActionListener(new ActionListener() {
211f7ddb 430 @Override
793f1071
NR
431 public void actionPerformed(ActionEvent e) {
432 words = true;
433 refreshBooks();
434 }
435 });
436 view.add(vwords);
437 bar.add(view);
438
4310bae9
NR
439 JMenu sources = new JMenu("Sources");
440 sources.setMnemonic(KeyEvent.VK_S);
9843a5e5 441
e6249b0f
NR
442 List<String> tt = new ArrayList<String>();
443 if (libOk) {
444 tt.addAll(reader.getLibrary().getSources());
445 }
9843a5e5 446 tt.add(0, null);
e6249b0f 447
9843a5e5 448 for (final String type : tt) {
4310bae9 449 JMenuItem item = new JMenuItem(type == null ? "All" : type);
9843a5e5 450 item.addActionListener(new ActionListener() {
211f7ddb 451 @Override
9843a5e5 452 public void actionPerformed(ActionEvent e) {
4310bae9
NR
453 removeBookPanes();
454 addBookPane(type, true);
455 refreshBooks();
9843a5e5
NR
456 }
457 });
4310bae9 458 sources.add(item);
9843a5e5
NR
459
460 if (type == null) {
4310bae9 461 sources.addSeparator();
9843a5e5
NR
462 }
463 }
10d558d2 464
4310bae9
NR
465 bar.add(sources);
466
467 JMenu authors = new JMenu("Authors");
468 authors.setMnemonic(KeyEvent.VK_A);
469
e6249b0f
NR
470 List<String> aa = new ArrayList<String>();
471 if (libOk) {
472 aa.addAll(reader.getLibrary().getAuthors());
473 }
4310bae9
NR
474 aa.add(0, null);
475 for (final String author : aa) {
476 JMenuItem item = new JMenuItem(author == null ? "All"
477 : author.isEmpty() ? "[unknown]" : author);
478 item.addActionListener(new ActionListener() {
211f7ddb 479 @Override
4310bae9
NR
480 public void actionPerformed(ActionEvent e) {
481 removeBookPanes();
482 addBookPane(author, false);
483 refreshBooks();
484 }
485 });
486 authors.add(item);
487
488 if (author == null || author.isEmpty()) {
489 authors.addSeparator();
490 }
491 }
492
493 bar.add(authors);
9843a5e5 494
e8eeea0a
NR
495 JMenu options = new JMenu("Options");
496 options.setMnemonic(KeyEvent.VK_O);
497 options.add(createMenuItemConfig());
498 options.add(createMenuItemUiConfig());
499 bar.add(options);
500
9843a5e5
NR
501 return bar;
502 }
503
e8eeea0a
NR
504 /**
505 * Create the Fanfix Configuration menu item.
506 *
507 * @return the item
508 */
509 private JMenuItem createMenuItemConfig() {
510 final String title = "Fanfix Configuration";
511 JMenuItem item = new JMenuItem(title);
512 item.setMnemonic(KeyEvent.VK_F);
513
514 item.addActionListener(new ActionListener() {
211f7ddb 515 @Override
e8eeea0a
NR
516 public void actionPerformed(ActionEvent e) {
517 ConfigEditor<Config> ed = new ConfigEditor<Config>(
518 Config.class, Instance.getConfig(),
519 "This is where you configure the options of the program.");
520 JFrame frame = new JFrame(title);
521 frame.add(ed);
522 frame.setSize(800, 600);
523 frame.setVisible(true);
524 }
525 });
526
527 return item;
528 }
529
530 /**
531 * Create the UI Configuration menu item.
532 *
533 * @return the item
534 */
535 private JMenuItem createMenuItemUiConfig() {
536 final String title = "UI Configuration";
537 JMenuItem item = new JMenuItem(title);
538 item.setMnemonic(KeyEvent.VK_U);
539
540 item.addActionListener(new ActionListener() {
211f7ddb 541 @Override
e8eeea0a
NR
542 public void actionPerformed(ActionEvent e) {
543 ConfigEditor<UiConfig> ed = new ConfigEditor<UiConfig>(
544 UiConfig.class, Instance.getUiConfig(),
545 "This is where you configure the graphical appearence of the program.");
546 JFrame frame = new JFrame(title);
547 frame.add(ed);
548 frame.setSize(800, 600);
549 frame.setVisible(true);
550 }
551 });
552
553 return item;
554 }
555
4d205683
NR
556 /**
557 * Create the export menu item.
558 *
559 * @return the item
560 */
9843a5e5 561 private JMenuItem createMenuItemExport() {
4d205683
NR
562 final JFileChooser fc = new JFileChooser();
563 fc.setAcceptAllFileFilterUsed(false);
564
565 final Map<FileFilter, OutputType> filters = new HashMap<FileFilter, OutputType>();
566 for (OutputType type : OutputType.values()) {
567 String ext = type.getDefaultExtension(false);
568 String desc = type.getDesc(false);
b2612f9d 569
4d205683
NR
570 if (ext == null || ext.isEmpty()) {
571 filters.put(createAllFilter(desc), type);
572 } else {
573 filters.put(new FileNameExtensionFilter(desc, ext), type);
574 }
575 }
576
577 // First the "ALL" filters, then, the extension filters
578 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
579 if (!(entry.getKey() instanceof FileNameExtensionFilter)) {
580 fc.addChoosableFileFilter(entry.getKey());
581 }
582 }
583 for (Entry<FileFilter, OutputType> entry : filters.entrySet()) {
584 if (entry.getKey() instanceof FileNameExtensionFilter) {
585 fc.addChoosableFileFilter(entry.getKey());
586 }
587 }
588 //
9843a5e5 589
4d205683 590 JMenuItem export = new JMenuItem("Save as...", KeyEvent.VK_S);
10d558d2 591 export.addActionListener(new ActionListener() {
211f7ddb 592 @Override
10d558d2 593 public void actionPerformed(ActionEvent e) {
4d205683 594 if (selectedBook != null) {
5dd985cf 595 fc.showDialog(GuiReaderFrame.this, "Save");
b2612f9d
NR
596 if (fc.getSelectedFile() != null) {
597 final OutputType type = filters.get(fc.getFileFilter());
598 final String path = fc.getSelectedFile()
599 .getAbsolutePath()
600 + type.getDefaultExtension(false);
601 final Progress pg = new Progress();
602 outOfUi(pg, new Runnable() {
211f7ddb 603 @Override
b2612f9d
NR
604 public void run() {
605 try {
e42573a0 606 reader.getLibrary().export(
b2612f9d
NR
607 selectedBook.getMeta().getLuid(),
608 type, path, pg);
609 } catch (IOException e) {
62c63b07 610 Instance.getTraceHandler().error(e);
b2612f9d 611 }
4d205683 612 }
b2612f9d
NR
613 });
614 }
4d205683 615 }
10d558d2
NR
616 }
617 });
618
9843a5e5
NR
619 return export;
620 }
621
edd46289
NR
622 /**
623 * Create a {@link FileFilter} that accepts all files and return the given
624 * description.
625 *
626 * @param desc
627 * the description
628 *
629 * @return the filter
630 */
4d205683
NR
631 private FileFilter createAllFilter(final String desc) {
632 return new FileFilter() {
633 @Override
634 public String getDescription() {
635 return desc;
636 }
637
638 @Override
639 public boolean accept(File f) {
640 return true;
641 }
642 };
643 }
644
645 /**
646 * Create the refresh (delete cache) menu item.
647 *
648 * @return the item
649 */
22848428 650 private JMenuItem createMenuItemClearCache() {
4d205683 651 JMenuItem refresh = new JMenuItem("Clear cache", KeyEvent.VK_C);
10d558d2 652 refresh.addActionListener(new ActionListener() {
211f7ddb 653 @Override
10d558d2
NR
654 public void actionPerformed(ActionEvent e) {
655 if (selectedBook != null) {
656 outOfUi(null, new Runnable() {
211f7ddb 657 @Override
10d558d2 658 public void run() {
754a5bc2
NR
659 reader.clearLocalReaderCache(selectedBook.getMeta()
660 .getLuid());
10d558d2 661 selectedBook.setCached(false);
df6e2d88
NR
662 GuiReaderCoverImager.clearIcon(selectedBook
663 .getMeta());
3b2b638f 664 SwingUtilities.invokeLater(new Runnable() {
211f7ddb 665 @Override
3b2b638f 666 public void run() {
10d558d2 667 selectedBook.repaint();
3b2b638f
NR
668 }
669 });
670 }
671 });
333f0e7b
NR
672 }
673 }
674 });
10d558d2 675
9843a5e5
NR
676 return refresh;
677 }
678
70c9b112
NR
679 /**
680 * Create the delete menu item.
681 *
e6249b0f
NR
682 * @param libOk
683 * the library can be queried
684 *
70c9b112
NR
685 * @return the item
686 */
e6249b0f 687 private JMenuItem createMenuItemMove(boolean libOk) {
70c9b112
NR
688 JMenu moveTo = new JMenu("Move to...");
689 moveTo.setMnemonic(KeyEvent.VK_M);
690
691 List<String> types = new ArrayList<String>();
692 types.add(null);
e6249b0f
NR
693 if (libOk) {
694 types.addAll(reader.getLibrary().getSources());
695 }
70c9b112
NR
696
697 for (String type : types) {
698 JMenuItem item = new JMenuItem(type == null ? "New type..." : type);
699
700 moveTo.add(item);
701 if (type == null) {
702 moveTo.addSeparator();
703 }
704
705 final String ftype = type;
706 item.addActionListener(new ActionListener() {
211f7ddb 707 @Override
70c9b112
NR
708 public void actionPerformed(ActionEvent e) {
709 if (selectedBook != null) {
710 String type = ftype;
711 if (type == null) {
712 Object rep = JOptionPane.showInputDialog(
5dd985cf 713 GuiReaderFrame.this, "Move to:",
70c9b112
NR
714 "Moving story",
715 JOptionPane.QUESTION_MESSAGE, null, null,
716 selectedBook.getMeta().getSource());
14b57448 717
70c9b112
NR
718 if (rep == null) {
719 return;
70c9b112 720 }
211f7ddb
NR
721
722 type = rep.toString();
70c9b112
NR
723 }
724
725 final String ftype = type;
726 outOfUi(null, new Runnable() {
211f7ddb 727 @Override
70c9b112 728 public void run() {
c3b229a1 729 reader.changeSource(selectedBook.getMeta()
70c9b112
NR
730 .getLuid(), ftype);
731
732 selectedBook = null;
733
734 SwingUtilities.invokeLater(new Runnable() {
211f7ddb 735 @Override
70c9b112 736 public void run() {
e6249b0f 737 setJMenuBar(createMenu(true));
70c9b112
NR
738 }
739 });
740 }
741 });
742 }
743 }
744 });
745 }
746
747 return moveTo;
748 }
749
22848428
NR
750 /**
751 * Create the redownload (then delete original) menu item.
752 *
753 * @return the item
754 */
755 private JMenuItem createMenuItemRedownload() {
756 JMenuItem refresh = new JMenuItem("Redownload", KeyEvent.VK_R);
757 refresh.addActionListener(new ActionListener() {
211f7ddb 758 @Override
22848428
NR
759 public void actionPerformed(ActionEvent e) {
760 if (selectedBook != null) {
754a5bc2 761 final MetaData meta = selectedBook.getMeta();
c8faa52a 762 imprt(meta.getUrl(), new StoryRunnable() {
211f7ddb 763 @Override
c8faa52a 764 public void run(Story story) {
754a5bc2 765 reader.delete(meta.getLuid());
5dd985cf 766 GuiReaderFrame.this.selectedBook = null;
c8faa52a
NR
767 MetaData newMeta = story.getMeta();
768 if (!newMeta.getSource().equals(meta.getSource())) {
c3b229a1 769 reader.changeSource(newMeta.getLuid(),
c8faa52a
NR
770 meta.getSource());
771 }
22848428 772 }
754a5bc2 773 }, "Removing old copy");
22848428
NR
774 }
775 }
776 });
777
778 return refresh;
779 }
780
4d205683
NR
781 /**
782 * Create the delete menu item.
783 *
784 * @return the item
785 */
9843a5e5 786 private JMenuItem createMenuItemDelete() {
10d558d2
NR
787 JMenuItem delete = new JMenuItem("Delete", KeyEvent.VK_D);
788 delete.addActionListener(new ActionListener() {
211f7ddb 789 @Override
10d558d2
NR
790 public void actionPerformed(ActionEvent e) {
791 if (selectedBook != null) {
792 outOfUi(null, new Runnable() {
211f7ddb 793 @Override
10d558d2 794 public void run() {
22848428 795 reader.delete(selectedBook.getMeta().getLuid());
10d558d2 796 selectedBook = null;
10d558d2
NR
797 }
798 });
799 }
800 }
801 });
802
9843a5e5
NR
803 return delete;
804 }
10d558d2 805
df6e2d88
NR
806 /**
807 * Create the properties menu item.
808 *
809 * @return the item
810 */
811 private JMenuItem createMenuItemProperties() {
812 JMenuItem delete = new JMenuItem("Properties", KeyEvent.VK_P);
813 delete.addActionListener(new ActionListener() {
814 @Override
815 public void actionPerformed(ActionEvent e) {
816 if (selectedBook != null) {
817 outOfUi(null, new Runnable() {
818 @Override
819 public void run() {
820 final MetaData meta = selectedBook.getMeta();
821 new JFrame() {
822 private static final long serialVersionUID = 1L;
823 @SuppressWarnings("unused")
824 private Object init = init();
825
826 private Object init() {
827 // Borders
828 int top = 20;
829 int space = 10;
830
831 // Image
832 ImageIcon img = GuiReaderCoverImager
833 .generateCoverIcon(
834 reader.getLibrary(), meta);
835
836 // frame
837 setTitle(meta.getLuid() + ": "
838 + meta.getTitle());
839
840 setSize(800, img.getIconHeight() + 2 * top);
841 setLayout(new BorderLayout());
842
843 // Main panel
844 JPanel mainPanel = new JPanel(
845 new BorderLayout());
846 JPanel mainPanelKeys = new JPanel();
847 mainPanelKeys.setLayout(new BoxLayout(
848 mainPanelKeys, BoxLayout.Y_AXIS));
849 JPanel mainPanelValues = new JPanel();
850 mainPanelValues.setLayout(new BoxLayout(
851 mainPanelValues, BoxLayout.Y_AXIS));
852
853 mainPanel.add(mainPanelKeys,
854 BorderLayout.WEST);
855 mainPanel.add(mainPanelValues,
856 BorderLayout.CENTER);
857
858 List<Entry<String, String>> infos = BasicReader
859 .getMetaDesc(meta);
860
861 Color trans = new Color(0, 0, 0, 1);
862 for (Entry<String, String> info : infos) {
863 JTextArea key = new JTextArea(info
864 .getKey());
865 key.setFont(new Font(key.getFont()
866 .getFontName(), Font.BOLD, key
867 .getFont().getSize()));
868 key.setEditable(false);
869 key.setLineWrap(false);
870 key.setBackground(trans);
871 mainPanelKeys.add(key);
872
873 JTextArea value = new JTextArea(info
874 .getValue());
875 value.setEditable(false);
876 value.setLineWrap(false);
877 value.setBackground(trans);
878 mainPanelValues.add(value);
879 }
880
881 // Image
882 JLabel imgLabel = new JLabel(img);
883 imgLabel.setVerticalAlignment(JLabel.TOP);
884
885 // Borders
886 mainPanelKeys.setBorder(BorderFactory
887 .createEmptyBorder(top, space, 0, 0));
888 mainPanelValues.setBorder(BorderFactory
889 .createEmptyBorder(top, space, 0, 0));
890 imgLabel.setBorder(BorderFactory
891 .createEmptyBorder(0, space, 0, 0));
892
893 // Add all
894 add(imgLabel, BorderLayout.WEST);
895 add(mainPanel, BorderLayout.CENTER);
896
897 return null;
898 }
899
900 }.setVisible(true);
901 }
902 });
903 }
904 }
905 });
906
907 return delete;
908 }
909
4d205683 910 /**
14b57448 911 * Create the open menu item for a book or a source (no LUID).
4d205683
NR
912 *
913 * @return the item
914 */
9843a5e5
NR
915 private JMenuItem createMenuItemOpenBook() {
916 JMenuItem open = new JMenuItem("Open", KeyEvent.VK_O);
917 open.addActionListener(new ActionListener() {
211f7ddb 918 @Override
9843a5e5
NR
919 public void actionPerformed(ActionEvent e) {
920 if (selectedBook != null) {
14b57448
NR
921 if (selectedBook.getMeta().getLuid() == null) {
922 removeBookPanes();
923 addBookPane(selectedBook.getMeta().getSource(), true);
924 refreshBooks();
925 } else {
926 openBook(selectedBook);
927 }
928 }
929 }
930 });
931
932 return open;
933 }
934
935 /**
936 * Create the SetCover menu item for a book to change the linked source
937 * cover.
938 *
939 * @return the item
940 */
941 private JMenuItem createMenuItemSetCover() {
942 JMenuItem open = new JMenuItem("Set as cover for source", KeyEvent.VK_C);
943 open.addActionListener(new ActionListener() {
944 @Override
945 public void actionPerformed(ActionEvent e) {
946 if (selectedBook != null) {
947 reader.getLibrary().setSourceCover(
948 selectedBook.getMeta().getSource(),
949 selectedBook.getMeta().getLuid());
085a2f9a
NR
950 MetaData source = selectedBook.getMeta().clone();
951 source.setLuid(null);
df6e2d88 952 GuiReaderCoverImager.clearIcon(source);
9843a5e5
NR
953 }
954 }
955 });
10d558d2 956
9843a5e5
NR
957 return open;
958 }
10d558d2 959
4d205683 960 /**
5dd985cf 961 * Open a {@link GuiReaderBook} item.
4d205683
NR
962 *
963 * @param book
5dd985cf 964 * the {@link GuiReaderBook} to open
4d205683 965 */
5dd985cf 966 private void openBook(final GuiReaderBook book) {
9843a5e5
NR
967 final Progress pg = new Progress();
968 outOfUi(pg, new Runnable() {
211f7ddb 969 @Override
9843a5e5
NR
970 public void run() {
971 try {
350bc060 972 reader.read(book.getMeta().getLuid(), false, pg);
edd46289 973 SwingUtilities.invokeLater(new Runnable() {
211f7ddb 974 @Override
edd46289
NR
975 public void run() {
976 book.setCached(true);
9843a5e5 977 }
edd46289 978 });
9843a5e5 979 } catch (IOException e) {
edd46289 980 // TODO: error message?
62c63b07 981 Instance.getTraceHandler().error(e);
333f0e7b 982 }
10d558d2 983 }
9843a5e5 984 });
a6395bef 985 }
3b2b638f 986
4d205683
NR
987 /**
988 * Process the given action out of the Swing UI thread and link the given
989 * {@link ProgressBar} to the action.
990 * <p>
991 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
992 * to done when the action is done.
993 *
5dd985cf 994 * @param progress
4d205683
NR
995 * the {@link ProgressBar} or NULL
996 * @param run
997 * the action to run
998 */
754a5bc2
NR
999 private void outOfUi(Progress progress, final Runnable run) {
1000 final Progress pg = new Progress();
d5a7153c 1001 final Progress reload = new Progress("Reload books");
754a5bc2
NR
1002 if (progress == null) {
1003 progress = new Progress();
1004 }
3b2b638f 1005
754a5bc2
NR
1006 pg.addProgress(progress, 90);
1007 pg.addProgress(reload, 10);
1008
1009 invalidate();
1010 pgBar.setProgress(pg);
1011 validate();
a4143cd7 1012 setEnabled(false);
3b2b638f
NR
1013
1014 new Thread(new Runnable() {
211f7ddb 1015 @Override
3b2b638f 1016 public void run() {
925298fd
NR
1017 try {
1018 run.run();
1019 refreshBooks();
1020 } finally {
1021 reload.done();
1022 if (!pg.isDone()) {
1023 // will trigger pgBar ActionListener:
1024 pg.done();
1025 }
3b2b638f
NR
1026 }
1027 }
754a5bc2 1028 }, "outOfUi thread").start();
3b2b638f
NR
1029 }
1030
4d205683 1031 /**
68e2c6d2 1032 * Import a {@link Story} into the main {@link LocalLibrary}.
22848428
NR
1033 * <p>
1034 * Should be called inside the UI thread.
4d205683
NR
1035 *
1036 * @param askUrl
1037 * TRUE for an {@link URL}, false for a {@link File}
1038 */
10d558d2
NR
1039 private void imprt(boolean askUrl) {
1040 JFileChooser fc = new JFileChooser();
1041
5130ce84 1042 Object url;
10d558d2 1043 if (askUrl) {
5130ce84
NR
1044 String clipboard = "";
1045 try {
1046 clipboard = ("" + Toolkit.getDefaultToolkit()
1047 .getSystemClipboard().getData(DataFlavor.stringFlavor))
1048 .trim();
1049 } catch (Exception e) {
1050 // No data will be handled
1051 }
1052
1053 if (clipboard == null || !clipboard.startsWith("http")) {
1054 clipboard = "";
1055 }
1056
5dd985cf 1057 url = JOptionPane.showInputDialog(GuiReaderFrame.this,
10d558d2 1058 "url of the story to import?", "Importing from URL",
5130ce84 1059 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
10d558d2
NR
1060 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
1061 url = fc.getSelectedFile().getAbsolutePath();
1062 } else {
1063 url = null;
1064 }
1065
5130ce84 1066 if (url != null && !url.toString().isEmpty()) {
754a5bc2 1067 imprt(url.toString(), null, null);
22848428
NR
1068 }
1069 }
10d558d2 1070
22848428 1071 /**
68e2c6d2 1072 * Actually import the {@link Story} into the main {@link LocalLibrary}.
22848428
NR
1073 * <p>
1074 * Should be called inside the UI thread.
1075 *
1076 * @param url
1077 * the {@link Story} to import by {@link URL}
1078 * @param onSuccess
1079 * Action to execute on success
1080 */
c8faa52a 1081 private void imprt(final String url, final StoryRunnable onSuccess,
754a5bc2
NR
1082 String onSuccessPgName) {
1083 final Progress pg = new Progress();
1084 final Progress pgImprt = new Progress();
1085 final Progress pgOnSuccess = new Progress(onSuccessPgName);
1086 pg.addProgress(pgImprt, 95);
1087 pg.addProgress(pgOnSuccess, 5);
1088
22848428 1089 outOfUi(pg, new Runnable() {
211f7ddb 1090 @Override
22848428
NR
1091 public void run() {
1092 Exception ex = null;
c8faa52a 1093 Story story = null;
22848428 1094 try {
c8faa52a
NR
1095 story = reader.getLibrary().imprt(BasicReader.getUrl(url),
1096 pgImprt);
22848428
NR
1097 } catch (IOException e) {
1098 ex = e;
1099 }
10d558d2 1100
22848428
NR
1101 final Exception e = ex;
1102
1103 final boolean ok = (e == null);
754a5bc2
NR
1104
1105 pgOnSuccess.setProgress(0);
1106 if (!ok) {
fd1d31c2 1107 if (e instanceof UnknownHostException) {
925298fd 1108 error("URL not supported: " + url, "Cannot import URL",
fd1d31c2
NR
1109 null);
1110 } else {
1111 error("Failed to import " + url + ": \n"
1112 + e.getMessage(), "Cannot import URL", e);
1113 }
754a5bc2
NR
1114 } else {
1115 if (onSuccess != null) {
c8faa52a 1116 onSuccess.run(story);
22848428 1117 }
754a5bc2 1118 }
5ce869b8 1119 pgOnSuccess.done();
22848428
NR
1120 }
1121 });
10d558d2
NR
1122 }
1123
4d205683
NR
1124 /**
1125 * Enables or disables this component, depending on the value of the
1126 * parameter <code>b</code>. An enabled component can respond to user input
1127 * and generate events. Components are enabled initially by default.
1128 * <p>
1129 * Disabling this component will also affect its children.
1130 *
1131 * @param b
1132 * If <code>true</code>, this component is enabled; otherwise
1133 * this component is disabled
1134 */
1135 @Override
1136 public void setEnabled(boolean b) {
4f661b2b
NR
1137 if (bar != null) {
1138 bar.setEnabled(b);
1139 }
1140
5dd985cf 1141 for (GuiReaderGroup group : booksByType.keySet()) {
4310bae9
NR
1142 group.setEnabled(b);
1143 }
5dd985cf 1144 for (GuiReaderGroup group : booksByAuthor.keySet()) {
4310bae9
NR
1145 group.setEnabled(b);
1146 }
4d205683 1147 super.setEnabled(b);
3b2b638f
NR
1148 repaint();
1149 }
e6249b0f
NR
1150
1151 /**
1152 * Display an error message and log the linked {@link Exception}.
1153 *
1154 * @param message
1155 * the message
1156 * @param title
1157 * the title of the error message
1158 * @param e
1159 * the exception to log if any
1160 */
1161 private void error(final String message, final String title, Exception e) {
1162 Instance.getTraceHandler().error(title + ": " + message);
1163 if (e != null) {
1164 Instance.getTraceHandler().error(e);
1165 }
1166
1167 SwingUtilities.invokeLater(new Runnable() {
1168 @Override
1169 public void run() {
1170 JOptionPane.showMessageDialog(GuiReaderFrame.this, message,
1171 title, JOptionPane.ERROR_MESSAGE);
1172 }
1173 });
1174 }
a6395bef 1175}