fix see word count on source/author, step 1
[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<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
338 for (MetaData meta : lib.getListBySource(booksByType.get(group))) {
339 infos.add(GuiReaderBookInfo.fromMeta(meta));
340 }
341 group.refreshBooks(infos, words);
342 }
343
344 for (GuiReaderGroup group : booksByAuthor.keySet()) {
345 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
346 for (MetaData meta : lib.getListByAuthor(booksByAuthor.get(group))) {
347 infos.add(GuiReaderBookInfo.fromMeta(meta));
348 }
349 group.refreshBooks(infos, words);
350 }
351
352 pane.repaint();
353 this.repaint();
354 }
355
356 /**
357 * Open a {@link GuiReaderBook} item.
358 *
359 * @param book
360 * the {@link GuiReaderBook} to open
361 */
362 public void openBook(final GuiReaderBook book) {
363 final Progress pg = new Progress();
364 outOfUi(pg, new Runnable() {
365 @Override
366 public void run() {
367 try {
368 helper.getReader().read(book.getInfo().getMeta().getLuid(),
369 false, pg);
370 SwingUtilities.invokeLater(new Runnable() {
371 @Override
372 public void run() {
373 book.setCached(true);
374 }
375 });
376 } catch (IOException e) {
377 Instance.getTraceHandler().error(e);
378 error("Cannot open the selected book", "Error", e);
379 }
380 }
381 });
382 }
383
384 /**
385 * Process the given action out of the Swing UI thread and link the given
386 * {@link ProgressBar} to the action.
387 * <p>
388 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
389 * to done when the action is done.
390 *
391 * @param progress
392 * the {@link ProgressBar} or NULL
393 * @param run
394 * the action to run
395 */
396 public void outOfUi(Progress progress, final Runnable run) {
397 final Progress pg = new Progress();
398 final Progress reload = new Progress("Reload books");
399 if (progress == null) {
400 progress = new Progress();
401 }
402
403 pg.addProgress(progress, 90);
404 pg.addProgress(reload, 10);
405
406 invalidate();
407 pgBar.setProgress(pg);
408 validate();
409 setEnabled(false);
410
411 new Thread(new Runnable() {
412 @Override
413 public void run() {
414 try {
415 run.run();
416 refreshBooks();
417 } finally {
418 reload.done();
419 if (!pg.isDone()) {
420 // will trigger pgBar ActionListener:
421 pg.done();
422 }
423 }
424 }
425 }, "outOfUi thread").start();
426 }
427
428 /**
429 * Import a {@link Story} into the main {@link LocalLibrary}.
430 * <p>
431 * Should be called inside the UI thread.
432 *
433 * @param askUrl
434 * TRUE for an {@link URL}, false for a {@link File}
435 */
436 public void imprt(boolean askUrl) {
437 JFileChooser fc = new JFileChooser();
438
439 Object url;
440 if (askUrl) {
441 String clipboard = "";
442 try {
443 clipboard = ("" + Toolkit.getDefaultToolkit()
444 .getSystemClipboard().getData(DataFlavor.stringFlavor))
445 .trim();
446 } catch (Exception e) {
447 // No data will be handled
448 }
449
450 if (clipboard == null || !clipboard.startsWith("http")) {
451 clipboard = "";
452 }
453
454 url = JOptionPane.showInputDialog(GuiReaderMainPanel.this,
455 "url of the story to import?", "Importing from URL",
456 JOptionPane.QUESTION_MESSAGE, null, null, clipboard);
457 } else if (fc.showOpenDialog(this) != JFileChooser.CANCEL_OPTION) {
458 url = fc.getSelectedFile().getAbsolutePath();
459 } else {
460 url = null;
461 }
462
463 if (url != null && !url.toString().isEmpty()) {
464 imprt(url.toString(), null, null);
465 }
466 }
467
468 /**
469 * Actually import the {@link Story} into the main {@link LocalLibrary}.
470 * <p>
471 * Should be called inside the UI thread.
472 *
473 * @param url
474 * the {@link Story} to import by {@link URL}
475 * @param onSuccess
476 * Action to execute on success
477 */
478 public void imprt(final String url, final StoryRunnable onSuccess,
479 String onSuccessPgName) {
480 final Progress pg = new Progress();
481 final Progress pgImprt = new Progress();
482 final Progress pgOnSuccess = new Progress(onSuccessPgName);
483 pg.addProgress(pgImprt, 95);
484 pg.addProgress(pgOnSuccess, 5);
485
486 outOfUi(pg, new Runnable() {
487 @Override
488 public void run() {
489 Exception ex = null;
490 Story story = null;
491 try {
492 story = helper.getReader().getLibrary()
493 .imprt(BasicReader.getUrl(url), pgImprt);
494 } catch (IOException e) {
495 ex = e;
496 }
497
498 final Exception e = ex;
499
500 final boolean ok = (e == null);
501
502 pgOnSuccess.setProgress(0);
503 if (!ok) {
504 if (e instanceof UnknownHostException) {
505 error("URL not supported: " + url, "Cannot import URL",
506 null);
507 } else {
508 error("Failed to import " + url + ": \n"
509 + e.getMessage(), "Cannot import URL", e);
510 }
511 } else {
512 if (onSuccess != null) {
513 onSuccess.run(story);
514 }
515 }
516 pgOnSuccess.done();
517 }
518 });
519 }
520
521 /**
522 * Enables or disables this component, depending on the value of the
523 * parameter <code>b</code>. An enabled component can respond to user input
524 * and generate events. Components are enabled initially by default.
525 * <p>
526 * Enabling or disabling <b>this</b> component will also affect its
527 * children.
528 *
529 * @param b
530 * If <code>true</code>, this component is enabled; otherwise
531 * this component is disabled
532 */
533 @Override
534 public void setEnabled(boolean b) {
535 if (bar != null) {
536 bar.setEnabled(b);
537 }
538
539 for (GuiReaderGroup group : booksByType.keySet()) {
540 group.setEnabled(b);
541 }
542 for (GuiReaderGroup group : booksByAuthor.keySet()) {
543 group.setEnabled(b);
544 }
545 super.setEnabled(b);
546 repaint();
547 }
548
549 public void setWords(boolean words) {
550 this.words = words;
551 }
552
553 public GuiReaderBook getSelectedBook() {
554 return selectedBook;
555 }
556
557 public void unsetSelectedBook() {
558 selectedBook = null;
559 }
560
561 private void addListPane(String name, List<String> values,
562 final boolean type) {
563 GuiReader reader = helper.getReader();
564 BasicLibrary lib = reader.getLibrary();
565
566 GuiReaderGroup bookPane = new GuiReaderGroup(reader, name, color);
567
568 List<GuiReaderBookInfo> infos = new ArrayList<GuiReaderBookInfo>();
569 for (String value : values) {
570 if (type) {
571 infos.add(GuiReaderBookInfo.fromSource(lib, value));
572 } else {
573 infos.add(GuiReaderBookInfo.fromAuthor(lib, value));
574 }
575 }
576
577 bookPane.refreshBooks(infos, words);
578
579 this.invalidate();
580 pane.invalidate();
581 pane.add(bookPane);
582 pane.validate();
583 this.validate();
584
585 bookPane.setActionListener(new BookActionListener() {
586 @Override
587 public void select(GuiReaderBook book) {
588 selectedBook = book;
589 }
590
591 @Override
592 public void popupRequested(GuiReaderBook book, MouseEvent e) {
593 JPopupMenu popup = helper.createSourcePopup();
594 popup.show(e.getComponent(), e.getX(), e.getY());
595 }
596
597 @Override
598 public void action(final GuiReaderBook book) {
599 removeBookPanes();
600 addBookPane(book.getInfo().getMainInfo(), type);
601 refreshBooks();
602 }
603 });
604 }
605
606 /**
607 * Display an error message and log the linked {@link Exception}.
608 *
609 * @param message
610 * the message
611 * @param title
612 * the title of the error message
613 * @param e
614 * the exception to log if any
615 */
616 private void error(final String message, final String title, Exception e) {
617 Instance.getTraceHandler().error(title + ": " + message);
618 if (e != null) {
619 Instance.getTraceHandler().error(e);
620 }
621
622 SwingUtilities.invokeLater(new Runnable() {
623 @Override
624 public void run() {
625 JOptionPane.showMessageDialog(GuiReaderMainPanel.this, message,
626 title, JOptionPane.ERROR_MESSAGE);
627 }
628 });
629 }
630 }