ea564ab2370df877345dca234b0c0bd2c30816f7
[nikiroo-utils.git] / src / be / nikiroo / fanfix / reader / ui / GuiReaderMainPanel.java
1 package be.nikiroo.fanfix.reader.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Frame;
6 import java.awt.Toolkit;
7 import java.awt.datatransfer.DataFlavor;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.MouseEvent;
11 import java.io.File;
12 import java.io.IOException;
13 import java.net.URL;
14 import java.net.UnknownHostException;
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.TreeMap;
19
20 import javax.swing.BoxLayout;
21 import javax.swing.JFileChooser;
22 import javax.swing.JLabel;
23 import javax.swing.JMenuBar;
24 import javax.swing.JOptionPane;
25 import javax.swing.JPanel;
26 import javax.swing.JPopupMenu;
27 import javax.swing.JScrollPane;
28 import javax.swing.SwingConstants;
29 import javax.swing.SwingUtilities;
30
31 import be.nikiroo.fanfix.Instance;
32 import be.nikiroo.fanfix.bundles.UiConfig;
33 import be.nikiroo.fanfix.data.MetaData;
34 import be.nikiroo.fanfix.data.Story;
35 import be.nikiroo.fanfix.library.BasicLibrary;
36 import be.nikiroo.fanfix.library.BasicLibrary.Status;
37 import be.nikiroo.fanfix.library.LocalLibrary;
38 import be.nikiroo.fanfix.reader.BasicReader;
39 import be.nikiroo.fanfix.reader.ui.GuiReaderBook.BookActionListener;
40 import be.nikiroo.utils.Progress;
41 import be.nikiroo.utils.ui.ProgressBar;
42
43 /**
44 * A {@link Frame} that will show a {@link GuiReaderBook} item for each
45 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
46 * way to copy them to the {@link GuiReader} cache (
47 * {@link BasicReader#getLibrary()}), read them, delete them...
48 *
49 * @author niki
50 */
51 class GuiReaderMainPanel extends JPanel {
52 private static final long serialVersionUID = 1L;
53 private FrameHelper helper;
54 private Map<String, GuiReaderGroup> books;
55 private GuiReaderGroup bookPane; // for more "All"
56 private JPanel pane;
57 private Color color;
58 private ProgressBar pgBar;
59 private JMenuBar bar;
60 private GuiReaderBook selectedBook;
61 private boolean words; // words or authors (secondary info on books)
62 private boolean currentType; // type/source or author mode (All and Listing)
63
64 /**
65 * An object that offers some helper methods to access the frame that host
66 * it and the Fanfix-related functions.
67 *
68 * @author niki
69 */
70 public interface FrameHelper {
71 /**
72 * Return the reader associated to this {@link FrameHelper}.
73 *
74 * @return the reader
75 */
76 public GuiReader getReader();
77
78 /**
79 * Create the main menu bar.
80 *
81 * @param libOk
82 * the library can be queried
83 *
84 * @return the bar
85 */
86 public void createMenu(boolean b);
87
88 /**
89 * Create a popup menu for a {@link GuiReaderBook} that represents a
90 * story.
91 *
92 * @return the popup menu to display
93 */
94 public JPopupMenu createBookPopup();
95
96 /**
97 * Create a popup menu for a {@link GuiReaderBook} that represents a
98 * source/type or an author.
99 *
100 * @return the popup menu to display
101 */
102 public JPopupMenu createSourceAuthorPopup();
103 }
104
105 /**
106 * A {@link Runnable} with a {@link Story} parameter.
107 *
108 * @author niki
109 */
110 public interface StoryRunnable {
111 /**
112 * Run the action.
113 *
114 * @param story
115 * the story
116 */
117 public void run(Story story);
118 }
119
120 /**
121 * Create a new {@link GuiReaderMainPanel}.
122 *
123 * @param reader
124 * the associated {@link GuiReader} to forward some commands and
125 * access its {@link LocalLibrary}
126 * @param type
127 * the type of {@link Story} to load, or NULL for all types
128 */
129 public GuiReaderMainPanel(FrameHelper parent, String type) {
130 super(new BorderLayout(), true);
131
132 this.helper = parent;
133
134 pane = new JPanel();
135 pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
136
137 Integer icolor = Instance.getUiConfig().getColor(
138 UiConfig.BACKGROUND_COLOR);
139 if (icolor != null) {
140 color = new Color(icolor);
141 setBackground(color);
142 pane.setBackground(color);
143 }
144
145 JScrollPane scroll = new JScrollPane(pane);
146 scroll.getVerticalScrollBar().setUnitIncrement(16);
147 add(scroll, BorderLayout.CENTER);
148
149 String message = parent.getReader().getLibrary().getLibraryName();
150 if (!message.isEmpty()) {
151 JLabel name = new JLabel(message, SwingConstants.CENTER);
152 add(name, BorderLayout.NORTH);
153 }
154
155 pgBar = new ProgressBar();
156 add(pgBar, BorderLayout.SOUTH);
157
158 pgBar.addActionListener(new ActionListener() {
159 @Override
160 public void actionPerformed(ActionEvent e) {
161 invalidate();
162 pgBar.setProgress(null);
163 validate();
164 setEnabled(true);
165 }
166 });
167
168 pgBar.addUpdateListener(new ActionListener() {
169 @Override
170 public void actionPerformed(ActionEvent e) {
171 invalidate();
172 validate();
173 repaint();
174 }
175 });
176
177 books = new TreeMap<String, GuiReaderGroup>();
178
179 pane.setVisible(false);
180 final Progress pg = new Progress();
181 final String typeF = type;
182 outOfUi(pg, new Runnable() {
183 @Override
184 public void run() {
185 BasicLibrary lib = helper.getReader().getLibrary();
186 Status status = lib.getStatus();
187
188 if (status == Status.READY) {
189 lib.refresh(pg);
190 invalidate();
191 helper.createMenu(true);
192 if (typeF == null) {
193 addBookPane(true, false);
194 } else {
195 addBookPane(typeF, true);
196 }
197 refreshBooks();
198 validate();
199 pane.setVisible(true);
200 } else {
201 invalidate();
202 helper.createMenu(false);
203 validate();
204
205 String err = lib.getLibraryName() + "\n";
206 switch (status) {
207 case INVALID:
208 err += "Library not valid";
209 break;
210
211 case UNAUTORIZED:
212 err += "You are not allowed to access this library";
213 break;
214
215 case UNAVAILABLE:
216 err += "Library currently unavailable";
217 break;
218
219 default:
220 err += "An error occured when contacting the library";
221 break;
222 }
223
224 error(err, "Library error", null);
225 }
226 }
227 });
228 }
229
230 public boolean getCurrentType() {
231 return currentType;
232 }
233
234 /**
235 * Add a new {@link GuiReaderGroup} on the frame to display all the
236 * sources/types or all the authors, or a listing of all the books sorted
237 * either by source or author.
238 * <p>
239 * A display of all the sources/types or all the authors will show one icon
240 * per source/type or author.
241 * <p>
242 * A listing of all the books sorted by source/type or author will display
243 * all the books.
244 *
245 * @param type
246 * TRUE for type/source, FALSE for author
247 * @param listMode
248 * TRUE to get a listing of all the sources or authors, FALSE to
249 * get one icon per source or author
250 */
251 public void addBookPane(boolean type, boolean listMode) {
252 this.currentType = type;
253 BasicLibrary lib = helper.getReader().getLibrary();
254 if (type) {
255 if (!listMode) {
256 addListPane("Sources", lib.getSources(), type);
257 } else {
258 for (String tt : lib.getSources()) {
259 if (tt != null) {
260 addBookPane(tt, type);
261 }
262 }
263 }
264 } else {
265 if (!listMode) {
266 addListPane("Authors", lib.getAuthors(), type);
267 } else {
268 for (String tt : lib.getAuthors()) {
269 if (tt != null) {
270 addBookPane(tt, type);
271 }
272 }
273 }
274 }
275 }
276
277 /**
278 * Add a new {@link GuiReaderGroup} on the frame to display the books of the
279 * selected type or author.
280 *
281 *
282 * @param value
283 * the author or the type, or NULL to get all the
284 * authors-or-types
285 * @param type
286 * TRUE for type/source, FALSE for author
287 */
288 public void addBookPane(String value, boolean type) {
289 this.currentType = type;
290
291 GuiReaderGroup bookPane = new GuiReaderGroup(helper.getReader(), value,
292 color);
293
294 books.put(value, bookPane);
295
296 this.invalidate();
297 pane.invalidate();
298 pane.add(bookPane);
299 pane.validate();
300 this.validate();
301
302 bookPane.setActionListener(new BookActionListener() {
303 @Override
304 public void select(GuiReaderBook book) {
305 selectedBook = book;
306 }
307
308 @Override
309 public void popupRequested(GuiReaderBook book, MouseEvent e) {
310 JPopupMenu popup = helper.createBookPopup();
311 popup.show(e.getComponent(), e.getX(), e.getY());
312 }
313
314 @Override
315 public void action(final GuiReaderBook book) {
316 openBook(book);
317 }
318 });
319 }
320
321 /**
322 * Clear the pane from any book that may be present, usually prior to adding
323 * new ones.
324 */
325 public void removeBookPanes() {
326 books.clear();
327 pane.invalidate();
328 this.invalidate();
329 pane.removeAll();
330 pane.validate();
331 this.validate();
332 }
333
334 /**
335 * Refresh the list of {@link GuiReaderBook}s from disk.
336 */
337 public void refreshBooks() {
338 BasicLibrary lib = helper.getReader().getLibrary();
339 for (String value : books.keySet()) {
340 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
341
342 List<MetaData> metas;
343 if (currentType) {
344 metas = lib.getListBySource(value);
345 } else {
346 metas = lib.getListByAuthor(value);
347 }
348 for (MetaData meta : metas) {
349 infos.add(GuiReaderBookInfo.fromMeta(meta));
350 }
351
352 books.get(value).refreshBooks(infos, words);
353 }
354
355 if (bookPane != null) {
356 bookPane.refreshBooks(words);
357 }
358
359 pane.repaint();
360 this.repaint();
361 }
362
363 /**
364 * Open a {@link GuiReaderBook} item.
365 *
366 * @param book
367 * the {@link GuiReaderBook} to open
368 */
369 public void openBook(final GuiReaderBook book) {
370 final Progress pg = new Progress();
371 outOfUi(pg, new Runnable() {
372 @Override
373 public void run() {
374 try {
375 helper.getReader().read(book.getInfo().getMeta().getLuid(),
376 false, pg);
377 SwingUtilities.invokeLater(new Runnable() {
378 @Override
379 public void run() {
380 book.setCached(true);
381 }
382 });
383 } catch (IOException e) {
384 Instance.getTraceHandler().error(e);
385 error("Cannot open the selected book", "Error", e);
386 }
387 }
388 });
389 }
390
391 /**
392 * Process the given action out of the Swing UI thread and link the given
393 * {@link ProgressBar} to the action.
394 * <p>
395 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
396 * to done when the action is done.
397 *
398 * @param progress
399 * the {@link ProgressBar} or NULL
400 * @param run
401 * the action to run
402 */
403 public void outOfUi(Progress progress, final Runnable run) {
404 final Progress pg = new Progress();
405 final Progress reload = new Progress("Reload books");
406 if (progress == null) {
407 progress = new Progress();
408 }
409
410 pg.addProgress(progress, 90);
411 pg.addProgress(reload, 10);
412
413 invalidate();
414 pgBar.setProgress(pg);
415 validate();
416 setEnabled(false);
417
418 new Thread(new Runnable() {
419 @Override
420 public void run() {
421 try {
422 run.run();
423 refreshBooks();
424 } finally {
425 reload.done();
426 if (!pg.isDone()) {
427 // will trigger pgBar ActionListener:
428 pg.done();
429 }
430 }
431 }
432 }, "outOfUi thread").start();
433 }
434
435 /**
436 * Import a {@link Story} into the main {@link LocalLibrary}.
437 * <p>
438 * Should be called inside the UI thread.
439 *
440 * @param askUrl
441 * TRUE for an {@link URL}, false for a {@link File}
442 */
443 public void imprt(boolean askUrl) {
444 JFileChooser fc = new JFileChooser();
445
446 Object url;
447 if (askUrl) {
448 String clipboard = "";
449 try {
450 clipboard = ("" + Toolkit.getDefaultToolkit()
451 .getSystemClipboard().getData(DataFlavor.stringFlavor))
452 .trim();
453 } catch (Exception e) {
454 // No data will be handled
455 }
456
457 if (clipboard == null || !clipboard.startsWith("http")) {
458 clipboard = "";
459 }
460
461 url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
462 "url of the story to import?", "Importing from URL",
463 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
464 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
465 url = fc.getSelectedFile().getAbsolutePath();
466 } else {
467 url = null;
468 }
469
470 if (url != null && !url.toString().isEmpty()) {
471 imprt(url.toString(), null, null);
472 }
473 }
474
475 /**
476 * Actually import the {@link Story} into the main {@link LocalLibrary}.
477 * <p>
478 * Should be called inside the UI thread.
479 *
480 * @param url
481 * the {@link Story} to import by {@link URL}
482 * @param onSuccess
483 * Action to execute on success
484 */
485 public void imprt(final String url, final StoryRunnable onSuccess,
486 String onSuccessPgName) {
487 final Progress pg = new Progress();
488 final Progress pgImprt = new Progress();
489 final Progress pgOnSuccess = new Progress(onSuccessPgName);
490 pg.addProgress(pgImprt, 95);
491 pg.addProgress(pgOnSuccess, 5);
492
493 outOfUi(pg, new Runnable() {
494 @Override
495 public void run() {
496 Exception ex = null;
497 Story story = null;
498 try {
499 story = helper.getReader().getLibrary()
500 .imprt(BasicReader.getUrl(url), pgImprt);
501 } catch (IOException e) {
502 ex = e;
503 }
504
505 final Exception e = ex;
506
507 final boolean ok = (e == null);
508
509 pgOnSuccess.setProgress(0);
510 if (!ok) {
511 if (e instanceof UnknownHostException) {
512 error("URL not supported: " + url, "Cannot import URL",
513 null);
514 } else {
515 error("Failed to import " + url + ": \n"
516 + e.getMessage(), "Cannot import URL", e);
517 }
518 } else {
519 if (onSuccess != null) {
520 onSuccess.run(story);
521 }
522 }
523 pgOnSuccess.done();
524 }
525 });
526 }
527
528 /**
529 * Enables or disables this component, depending on the value of the
530 * parameter <code>b</code>. An enabled component can respond to user input
531 * and generate events. Components are enabled initially by default.
532 * <p>
533 * Enabling or disabling <b>this</b> component will also affect its
534 * children.
535 *
536 * @param b
537 * If <code>true</code>, this component is enabled; otherwise
538 * this component is disabled
539 */
540 @Override
541 public void setEnabled(boolean b) {
542 if (bar != null) {
543 bar.setEnabled(b);
544 }
545
546 for (GuiReaderGroup group : books.values()) {
547 group.setEnabled(b);
548 }
549 super.setEnabled(b);
550 repaint();
551 }
552
553 public void setWords(boolean words) {
554 this.words = words;
555 }
556
557 public GuiReaderBook getSelectedBook() {
558 return selectedBook;
559 }
560
561 public void unsetSelectedBook() {
562 selectedBook = null;
563 }
564
565 private void addListPane(String name, List<String> values,
566 final boolean type) {
567 GuiReader reader = helper.getReader();
568 BasicLibrary lib = reader.getLibrary();
569
570 bookPane = new GuiReaderGroup(reader, name, color);
571
572 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
573 for (String value : values) {
574 if (type) {
575 infos.add(GuiReaderBookInfo.fromSource(lib, value));
576 } else {
577 infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
578 }
579 }
580
581 bookPane.refreshBooks(infos, words);
582
583 this.invalidate();
584 pane.invalidate();
585 pane.add(bookPane);
586 pane.validate();
587 this.validate();
588
589 bookPane.setActionListener(new BookActionListener() {
590 @Override
591 public void select(GuiReaderBook book) {
592 selectedBook = book;
593 }
594
595 @Override
596 public void popupRequested(GuiReaderBook book, MouseEvent e) {
597 JPopupMenu popup = helper.createSourceAuthorPopup();
598 popup.show(e.getComponent(), e.getX(), e.getY());
599 }
600
601 @Override
602 public void action(final GuiReaderBook book) {
603 removeBookPanes();
604 addBookPane(book.getInfo().getMainInfo(), type);
605 refreshBooks();
606 }
607 });
608 }
609
610 /**
611 * Display an error message and log the linked {@link Exception}.
612 *
613 * @param message
614 * the message
615 * @param title
616 * the title of the error message
617 * @param e
618 * the exception to log if any
619 */
620 private void error(final String message, final String title, Exception e) {
621 Instance.getTraceHandler().error(title + ": " + message);
622 if (e != null) {
623 Instance.getTraceHandler().error(e);
624 }
625
626 SwingUtilities.invokeLater(new Runnable() {
627 @Override
628 public void run() {
629 JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
630 title, JOptionPane.ERROR_MESSAGE);
631 }
632 });
633 }
634 }