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