1 package be
.nikiroo
.fanfix
.reader
;
3 import java
.awt
.BorderLayout
;
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
.KeyEvent
;
11 import java
.awt
.event
.MouseEvent
;
12 import java
.awt
.event
.WindowEvent
;
14 import java
.io
.IOException
;
16 import java
.util
.HashMap
;
17 import java
.util
.List
;
19 import java
.util
.Map
.Entry
;
21 import javax
.swing
.BoxLayout
;
22 import javax
.swing
.JFileChooser
;
23 import javax
.swing
.JFrame
;
24 import javax
.swing
.JMenu
;
25 import javax
.swing
.JMenuBar
;
26 import javax
.swing
.JMenuItem
;
27 import javax
.swing
.JOptionPane
;
28 import javax
.swing
.JPanel
;
29 import javax
.swing
.JPopupMenu
;
30 import javax
.swing
.JScrollPane
;
31 import javax
.swing
.SwingUtilities
;
32 import javax
.swing
.filechooser
.FileFilter
;
33 import javax
.swing
.filechooser
.FileNameExtensionFilter
;
35 import be
.nikiroo
.fanfix
.Instance
;
36 import be
.nikiroo
.fanfix
.Library
;
37 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
38 import be
.nikiroo
.fanfix
.data
.MetaData
;
39 import be
.nikiroo
.fanfix
.data
.Story
;
40 import be
.nikiroo
.fanfix
.output
.BasicOutput
.OutputType
;
41 import be
.nikiroo
.fanfix
.reader
.LocalReaderBook
.BookActionListener
;
42 import be
.nikiroo
.utils
.Progress
;
43 import be
.nikiroo
.utils
.Version
;
44 import be
.nikiroo
.utils
.ui
.ProgressBar
;
47 * A {@link Frame} that will show a {@link LocalReaderBook} item for each
48 * {@link Story} in the main cache ({@link Instance#getCache()}), and offer a
49 * way to copy them to the {@link LocalReader} cache ({@link LocalReader#lib}),
50 * read them, delete them...
54 class LocalReaderFrame
extends JFrame
{
55 private static final long serialVersionUID
= 1L;
56 private LocalReader reader
;
57 private Map
<LocalReaderGroup
, String
> booksByType
;
58 private Map
<LocalReaderGroup
, String
> booksByAuthor
;
61 private ProgressBar pgBar
;
63 private LocalReaderBook selectedBook
;
64 private boolean words
; // words or authors (secondary info on books)
67 * Create a new {@link LocalReaderFrame}.
70 * the associated {@link LocalReader} to forward some commands
71 * and access its {@link Library}
73 * the type of {@link Story} to load, or NULL for all types
75 public LocalReaderFrame(LocalReader reader
, String type
) {
76 super(String
.format("Fanfix %s Library", Version
.getCurrentVersion()));
80 setDefaultCloseOperation(JFrame
.EXIT_ON_CLOSE
);
82 setLayout(new BorderLayout());
85 pane
.setLayout(new BoxLayout(pane
, BoxLayout
.PAGE_AXIS
));
87 color
= Instance
.getUiConfig().getColor(UiConfig
.BACKGROUND_COLOR
);
90 pane
.setBackground(color
);
93 JScrollPane scroll
= new JScrollPane(pane
);
94 scroll
.getVerticalScrollBar().setUnitIncrement(16);
95 add(scroll
, BorderLayout
.CENTER
);
97 pgBar
= new ProgressBar();
98 add(pgBar
, BorderLayout
.SOUTH
);
100 setJMenuBar(createMenu());
102 booksByType
= new HashMap
<LocalReaderGroup
, String
>();
103 booksByAuthor
= new HashMap
<LocalReaderGroup
, String
>();
105 addBookPane(type
, true);
112 * Add a new {@link LocalReaderGroup} on the frame to display the books of
113 * the selected type or author.
116 * the author or the type
118 * TRUE for type, FALSE for author
120 private void addBookPane(String value
, boolean type
) {
123 for (String tt
: Instance
.getLibrary().getTypes()) {
125 addBookPane(tt
, type
);
129 for (String tt
: Instance
.getLibrary().getAuthors()) {
131 addBookPane(tt
, type
);
139 LocalReaderGroup bookPane
= new LocalReaderGroup(reader
, value
, color
);
141 booksByType
.put(bookPane
, value
);
143 booksByAuthor
.put(bookPane
, value
);
152 bookPane
.setActionListener(new BookActionListener() {
153 public void select(LocalReaderBook book
) {
157 public void popupRequested(LocalReaderBook book
, MouseEvent e
) {
158 JPopupMenu popup
= new JPopupMenu();
159 popup
.add(createMenuItemOpenBook());
160 popup
.addSeparator();
161 popup
.add(createMenuItemExport());
162 popup
.add(createMenuItemClearCache());
163 popup
.add(createMenuItemRedownload());
164 popup
.addSeparator();
165 popup
.add(createMenuItemDelete());
166 popup
.show(e
.getComponent(), e
.getX(), e
.getY());
169 public void action(final LocalReaderBook book
) {
175 private void removeBookPanes() {
177 booksByAuthor
.clear();
186 * Refresh the list of {@link LocalReaderBook}s from disk.
189 * the type of {@link Story} to load, or NULL for all types
191 private void refreshBooks() {
192 for (LocalReaderGroup group
: booksByType
.keySet()) {
193 List
<MetaData
> stories
= Instance
.getLibrary().getListByType(
194 booksByType
.get(group
));
195 group
.refreshBooks(stories
, words
);
198 for (LocalReaderGroup group
: booksByAuthor
.keySet()) {
199 List
<MetaData
> stories
= Instance
.getLibrary().getListByAuthor(
200 booksByAuthor
.get(group
));
201 group
.refreshBooks(stories
, words
);
209 * Create the main menu bar.
213 private JMenuBar
createMenu() {
214 bar
= new JMenuBar();
216 JMenu file
= new JMenu("File");
217 file
.setMnemonic(KeyEvent
.VK_F
);
219 JMenuItem imprt
= new JMenuItem("Import URL...", KeyEvent
.VK_U
);
220 imprt
.addActionListener(new ActionListener() {
221 public void actionPerformed(ActionEvent e
) {
225 JMenuItem imprtF
= new JMenuItem("Import File...", KeyEvent
.VK_F
);
226 imprtF
.addActionListener(new ActionListener() {
227 public void actionPerformed(ActionEvent e
) {
231 JMenuItem exit
= new JMenuItem("Exit", KeyEvent
.VK_X
);
232 exit
.addActionListener(new ActionListener() {
233 public void actionPerformed(ActionEvent e
) {
234 LocalReaderFrame
.this.dispatchEvent(new WindowEvent(
235 LocalReaderFrame
.this, WindowEvent
.WINDOW_CLOSING
));
239 file
.add(createMenuItemOpenBook());
240 file
.add(createMenuItemExport());
249 JMenu edit
= new JMenu("Edit");
250 edit
.setMnemonic(KeyEvent
.VK_E
);
252 edit
.add(createMenuItemClearCache());
253 edit
.add(createMenuItemRedownload());
255 edit
.add(createMenuItemDelete());
259 JMenu view
= new JMenu("View");
260 view
.setMnemonic(KeyEvent
.VK_V
);
261 JMenuItem vauthors
= new JMenuItem("Author");
262 vauthors
.setMnemonic(KeyEvent
.VK_A
);
263 vauthors
.addActionListener(new ActionListener() {
264 public void actionPerformed(ActionEvent e
) {
270 JMenuItem vwords
= new JMenuItem("Word count");
271 vwords
.setMnemonic(KeyEvent
.VK_W
);
272 vwords
.addActionListener(new ActionListener() {
273 public void actionPerformed(ActionEvent e
) {
281 JMenu sources
= new JMenu("Sources");
282 sources
.setMnemonic(KeyEvent
.VK_S
);
284 List
<String
> tt
= Instance
.getLibrary().getTypes();
286 for (final String type
: tt
) {
287 JMenuItem item
= new JMenuItem(type
== null ?
"All" : type
);
288 item
.addActionListener(new ActionListener() {
289 public void actionPerformed(ActionEvent e
) {
291 addBookPane(type
, true);
298 sources
.addSeparator();
304 JMenu authors
= new JMenu("Authors");
305 authors
.setMnemonic(KeyEvent
.VK_A
);
307 List
<String
> aa
= Instance
.getLibrary().getAuthors();
309 for (final String author
: aa
) {
310 JMenuItem item
= new JMenuItem(author
== null ?
"All"
311 : author
.isEmpty() ?
"[unknown]" : author
);
312 item
.addActionListener(new ActionListener() {
313 public void actionPerformed(ActionEvent e
) {
315 addBookPane(author
, false);
321 if (author
== null || author
.isEmpty()) {
322 authors
.addSeparator();
332 * Create the export menu item.
336 private JMenuItem
createMenuItemExport() {
337 final JFileChooser fc
= new JFileChooser();
338 fc
.setAcceptAllFileFilterUsed(false);
340 final Map
<FileFilter
, OutputType
> filters
= new HashMap
<FileFilter
, OutputType
>();
341 for (OutputType type
: OutputType
.values()) {
342 String ext
= type
.getDefaultExtension(false);
343 String desc
= type
.getDesc(false);
345 if (ext
== null || ext
.isEmpty()) {
346 filters
.put(createAllFilter(desc
), type
);
348 filters
.put(new FileNameExtensionFilter(desc
, ext
), type
);
352 // First the "ALL" filters, then, the extension filters
353 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
354 if (!(entry
.getKey() instanceof FileNameExtensionFilter
)) {
355 fc
.addChoosableFileFilter(entry
.getKey());
358 for (Entry
<FileFilter
, OutputType
> entry
: filters
.entrySet()) {
359 if (entry
.getKey() instanceof FileNameExtensionFilter
) {
360 fc
.addChoosableFileFilter(entry
.getKey());
365 JMenuItem export
= new JMenuItem("Save as...", KeyEvent
.VK_S
);
366 export
.addActionListener(new ActionListener() {
367 public void actionPerformed(ActionEvent e
) {
368 if (selectedBook
!= null) {
369 fc
.showDialog(LocalReaderFrame
.this, "Save");
370 if (fc
.getSelectedFile() != null) {
371 final OutputType type
= filters
.get(fc
.getFileFilter());
372 final String path
= fc
.getSelectedFile()
374 + type
.getDefaultExtension(false);
375 final Progress pg
= new Progress();
376 outOfUi(pg
, new Runnable() {
379 Instance
.getLibrary().export(
380 selectedBook
.getMeta().getLuid(),
382 } catch (IOException e
) {
396 * Create a {@link FileFilter} that accepts all files and return the given
404 private FileFilter
createAllFilter(final String desc
) {
405 return new FileFilter() {
407 public String
getDescription() {
412 public boolean accept(File f
) {
419 * Create the refresh (delete cache) menu item.
423 private JMenuItem
createMenuItemClearCache() {
424 JMenuItem refresh
= new JMenuItem("Clear cache", KeyEvent
.VK_C
);
425 refresh
.addActionListener(new ActionListener() {
426 public void actionPerformed(ActionEvent e
) {
427 if (selectedBook
!= null) {
428 outOfUi(null, new Runnable() {
430 reader
.refresh(selectedBook
.getMeta().getLuid());
431 selectedBook
.setCached(false);
432 SwingUtilities
.invokeLater(new Runnable() {
434 selectedBook
.repaint();
447 * Create the redownload (then delete original) menu item.
451 private JMenuItem
createMenuItemRedownload() {
452 JMenuItem refresh
= new JMenuItem("Redownload", KeyEvent
.VK_R
);
453 refresh
.addActionListener(new ActionListener() {
454 public void actionPerformed(ActionEvent e
) {
455 if (selectedBook
!= null) {
456 imprt(selectedBook
.getMeta().getUrl(), new Runnable() {
458 reader
.delete(selectedBook
.getMeta().getLuid());
470 * Create the delete menu item.
474 private JMenuItem
createMenuItemDelete() {
475 JMenuItem delete
= new JMenuItem("Delete", KeyEvent
.VK_D
);
476 delete
.addActionListener(new ActionListener() {
477 public void actionPerformed(ActionEvent e
) {
478 if (selectedBook
!= null) {
479 outOfUi(null, new Runnable() {
481 reader
.delete(selectedBook
.getMeta().getLuid());
483 SwingUtilities
.invokeLater(new Runnable() {
498 * Create the open menu item.
502 private JMenuItem
createMenuItemOpenBook() {
503 JMenuItem open
= new JMenuItem("Open", KeyEvent
.VK_O
);
504 open
.addActionListener(new ActionListener() {
505 public void actionPerformed(ActionEvent e
) {
506 if (selectedBook
!= null) {
507 openBook(selectedBook
);
516 * Open a {@link LocalReaderBook} item.
519 * the {@link LocalReaderBook} to open
521 private void openBook(final LocalReaderBook book
) {
522 final Progress pg
= new Progress();
523 outOfUi(pg
, new Runnable() {
526 reader
.open(book
.getMeta().getLuid(), pg
);
527 SwingUtilities
.invokeLater(new Runnable() {
529 book
.setCached(true);
532 } catch (IOException e
) {
533 // TODO: error message?
541 * Process the given action out of the Swing UI thread and link the given
542 * {@link ProgressBar} to the action.
544 * The code will make sure that the {@link ProgressBar} (if not NULL) is set
545 * to done when the action is done.
548 * the {@link ProgressBar} or NULL
552 private void outOfUi(final Progress pg
, final Runnable run
) {
553 pgBar
.setProgress(pg
);
556 pgBar
.addActioListener(new ActionListener() {
557 public void actionPerformed(ActionEvent e
) {
558 pgBar
.setProgress(null);
563 new Thread(new Runnable() {
567 SwingUtilities
.invokeLater(new Runnable() {
572 } else if (!pg
.isDone()) {
573 pg
.setProgress(pg
.getMax());
580 * Import a {@link Story} into the main {@link Library}.
582 * Should be called inside the UI thread.
585 * TRUE for an {@link URL}, false for a {@link File}
587 private void imprt(boolean askUrl
) {
588 JFileChooser fc
= new JFileChooser();
592 String clipboard
= "";
594 clipboard
= ("" + Toolkit
.getDefaultToolkit()
595 .getSystemClipboard().getData(DataFlavor
.stringFlavor
))
597 } catch (Exception e
) {
598 // No data will be handled
601 if (clipboard
== null || !clipboard
.startsWith("http")) {
605 url
= JOptionPane
.showInputDialog(LocalReaderFrame
.this,
606 "url of the story to import?", "Importing from URL",
607 JOptionPane
.QUESTION_MESSAGE
, null, null, clipboard
);
608 } else if (fc
.showOpenDialog(this) != JFileChooser
.CANCEL_OPTION
) {
609 url
= fc
.getSelectedFile().getAbsolutePath();
614 if (url
!= null && !url
.toString().isEmpty()) {
615 imprt(url
.toString(), null);
620 * Actually import the {@link Story} into the main {@link Library}.
622 * Should be called inside the UI thread.
625 * the {@link Story} to import by {@link URL}
627 * Action to execute on success
629 private void imprt(final String url
, final Runnable onSuccess
) {
630 final Progress pg
= new Progress("Importing " + url
);
631 outOfUi(pg
, new Runnable() {
635 Instance
.getLibrary().imprt(BasicReader
.getUrl(url
), pg
);
636 } catch (IOException e
) {
640 final Exception e
= ex
;
642 final boolean ok
= (e
== null);
643 SwingUtilities
.invokeLater(new Runnable() {
647 JOptionPane
.showMessageDialog(
648 LocalReaderFrame
.this, "Cannot import: "
649 + url
, e
.getMessage(),
650 JOptionPane
.ERROR_MESSAGE
);
655 if (onSuccess
!= null) {
667 * Enables or disables this component, depending on the value of the
668 * parameter <code>b</code>. An enabled component can respond to user input
669 * and generate events. Components are enabled initially by default.
671 * Disabling this component will also affect its children.
674 * If <code>true</code>, this component is enabled; otherwise
675 * this component is disabled
678 public void setEnabled(boolean b
) {
680 for (LocalReaderGroup group
: booksByType
.keySet()) {
683 for (LocalReaderGroup group
: booksByAuthor
.keySet()) {