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