1 package be
.nikiroo
.fanfix
.reader
.ui
;
3 import java
.awt
.Desktop
;
4 import java
.awt
.EventQueue
;
5 import java
.awt
.event
.WindowAdapter
;
6 import java
.awt
.event
.WindowEvent
;
8 import java
.io
.IOException
;
9 import java
.net
.URISyntaxException
;
11 import javax
.swing
.JEditorPane
;
12 import javax
.swing
.JFrame
;
13 import javax
.swing
.JLabel
;
14 import javax
.swing
.JOptionPane
;
15 import javax
.swing
.event
.HyperlinkEvent
;
16 import javax
.swing
.event
.HyperlinkListener
;
18 import be
.nikiroo
.fanfix
.Instance
;
19 import be
.nikiroo
.fanfix
.VersionCheck
;
20 import be
.nikiroo
.fanfix
.bundles
.StringIdGui
;
21 import be
.nikiroo
.fanfix
.bundles
.UiConfig
;
22 import be
.nikiroo
.fanfix
.data
.MetaData
;
23 import be
.nikiroo
.fanfix
.data
.Story
;
24 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
25 import be
.nikiroo
.fanfix
.library
.CacheLibrary
;
26 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
27 import be
.nikiroo
.fanfix
.reader
.Reader
;
28 import be
.nikiroo
.fanfix
.searchable
.BasicSearchable
;
29 import be
.nikiroo
.fanfix
.searchable
.SearchableTag
;
30 import be
.nikiroo
.fanfix
.supported
.SupportType
;
31 import be
.nikiroo
.utils
.Progress
;
32 import be
.nikiroo
.utils
.Version
;
33 import be
.nikiroo
.utils
.ui
.UIUtils
;
36 * This class implements a graphical {@link Reader} using the Swing library from
39 * It can thus be themed to look native-like, or metal-like, or use any other
40 * theme you may want to try.
42 * We actually try to enable native look-alike mode on start.
46 class GuiReader
extends BasicReader
{
47 static private boolean nativeLookLoaded
;
49 private CacheLibrary cacheLib
;
51 private File cacheDir
;
54 * Create a new graphical {@link Reader}.
57 * in case of I/O errors
59 public GuiReader() throws IOException
{
60 // TODO: allow different themes?
61 if (!nativeLookLoaded
) {
62 UIUtils
.setLookAndFeel();
63 nativeLookLoaded
= true;
66 cacheDir
= Instance
.getReaderDir();
68 if (!cacheDir
.exists()) {
69 throw new IOException(
70 "Cannote create cache directory for local reader: "
76 public synchronized BasicLibrary
getLibrary() {
77 if (cacheLib
== null) {
78 BasicLibrary lib
= super.getLibrary();
79 if (lib
instanceof CacheLibrary
) {
80 cacheLib
= (CacheLibrary
) lib
;
82 cacheLib
= new CacheLibrary(cacheDir
, lib
);
90 public void read(boolean sync
) throws IOException
{
91 MetaData meta
= getMeta();
94 throw new IOException("No story to read");
97 read(meta
.getLuid(), sync
, null);
101 * Check if the {@link Story} denoted by this Library UID is present in the
102 * {@link GuiReader} cache.
107 * @return TRUE if it is
109 public boolean isCached(String luid
) {
110 return cacheLib
.isCached(luid
);
114 public void browse(String type
) {
115 final Boolean
[] done
= new Boolean
[] { false };
117 // TODO: improve presentation of update message
118 final VersionCheck updates
= VersionCheck
.check();
119 StringBuilder builder
= new StringBuilder();
121 final JEditorPane updateMessage
= new JEditorPane("text/html", "");
122 if (updates
.isNewVersionAvailable()) {
123 builder
.append(trans(StringIdGui
.NEW_VERSION_AVAILABLE
,
124 "<span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>"));
125 builder
.append("<br>");
126 builder
.append("<br>");
127 for (Version v
: updates
.getNewer()) {
128 builder
.append("\t<b>"
129 + trans(StringIdGui
.NEW_VERSION_VERSION
, v
.toString())
131 builder
.append("<br>");
132 builder
.append("<ul>");
133 for (String item
: updates
.getChanges().get(v
)) {
134 builder
.append("<li>" + item
+ "</li>");
136 builder
.append("</ul>");
140 updateMessage
.setText("<html><body>" //
144 // handle link events
145 updateMessage
.addHyperlinkListener(new HyperlinkListener() {
147 public void hyperlinkUpdate(HyperlinkEvent e
) {
148 if (e
.getEventType().equals(
149 HyperlinkEvent
.EventType
.ACTIVATED
))
151 Desktop
.getDesktop().browse(e
.getURL().toURI());
152 } catch (IOException ee
) {
153 Instance
.getTraceHandler().error(ee
);
154 } catch (URISyntaxException ee
) {
155 Instance
.getTraceHandler().error(ee
);
159 updateMessage
.setEditable(false);
160 updateMessage
.setBackground(new JLabel().getBackground());
163 final String typeFinal
= type
;
164 EventQueue
.invokeLater(new Runnable() {
167 if (updates
.isNewVersionAvailable()) {
168 int rep
= JOptionPane
.showConfirmDialog(null,
170 trans(StringIdGui
.NEW_VERSION_TITLE
),
171 JOptionPane
.OK_CANCEL_OPTION
);
172 if (rep
== JOptionPane
.OK_OPTION
) {
179 new Thread(new Runnable() {
183 GuiReaderFrame gui
= new GuiReaderFrame(
184 GuiReader
.this, typeFinal
);
186 } catch (Exception e
) {
187 Instance
.getTraceHandler().error(e
);
197 // This action must be synchronous, so wait until the frame is closed
201 } catch (InterruptedException e
) {
207 public void start(File target
, String program
, boolean sync
)
210 boolean handled
= false;
211 if (program
== null && !sync
) {
213 Desktop
.getDesktop().browse(target
.toURI());
215 } catch (UnsupportedOperationException e
) {
220 super.start(target
, program
, sync
);
225 public void search(boolean sync
) throws IOException
{
226 GuiReaderSearchFrame search
= new GuiReaderSearchFrame(this);
230 search
.setVisible(true);
235 public void search(SupportType searchOn
, String keywords
, int page
,
236 int item
, boolean sync
) {
237 GuiReaderSearchFrame search
= new GuiReaderSearchFrame(this);
238 search
.search(searchOn
, keywords
, page
, item
);
242 search
.setVisible(true);
247 public void searchTag(final SupportType searchOn
, final int page
,
248 final int item
, final boolean sync
, final Integer
... tags
) {
250 final GuiReaderSearchFrame search
= new GuiReaderSearchFrame(this);
252 final BasicSearchable searchable
= BasicSearchable
253 .getSearchable(searchOn
);
255 Runnable action
= new Runnable() {
258 SearchableTag tag
= null;
260 tag
= searchable
.getTag(tags
);
261 } catch (IOException e
) {
262 Instance
.getTraceHandler().error(e
);
265 search
.searchTag(searchOn
, page
, item
, tag
);
270 search
.setVisible(true);
278 new Thread(action
).start();
283 * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
284 * from the main library.
286 * The next time we try to retrieve the {@link Story}, it may be required to
290 * the luid of the {@link Story}
292 void clearLocalReaderCache(String luid
) {
294 cacheLib
.clearFromCache(luid
);
295 } catch (IOException e
) {
296 Instance
.getTraceHandler().error(e
);
301 * Forward the delete operation to the main library.
303 * The {@link Story} will be deleted from the main library as well as the
307 * the {@link Story} to delete
309 void delete(String luid
) {
311 cacheLib
.delete(luid
);
312 } catch (IOException e
) {
313 Instance
.getTraceHandler().error(e
);
318 * "Open" the given {@link Story}. It usually involves starting an external
319 * program adapted to the given file type.
321 * Asynchronous method.
324 * the luid of the {@link Story} to open
326 * execute the process synchronously (wait until it is terminated
329 * the optional progress (we may need to prepare the
330 * {@link Story} for reading
332 * @throws IOException
333 * in case of I/O errors
335 void read(String luid
, boolean sync
, Progress pg
) throws IOException
{
336 MetaData meta
= cacheLib
.getInfo(luid
);
338 boolean textInternal
= Instance
.getUiConfig().getBoolean(
339 UiConfig
.NON_IMAGES_DOCUMENT_USE_INTERNAL_READER
, true);
340 boolean imageInternal
= Instance
.getUiConfig().getBoolean(
341 UiConfig
.IMAGES_DOCUMENT_USE_INTERNAL_READER
, true);
343 boolean useInternalViewer
= true;
344 if (meta
.isImageDocument() && !imageInternal
) {
345 useInternalViewer
= false;
347 if (!meta
.isImageDocument() && !textInternal
) {
348 useInternalViewer
= false;
351 if (useInternalViewer
) {
352 GuiReaderViewer viewer
= new GuiReaderViewer(cacheLib
,
353 cacheLib
.getStory(luid
, null));
357 viewer
.setVisible(true);
360 File file
= cacheLib
.getFile(luid
, pg
);
361 openExternal(meta
, file
, sync
);
366 * Change the source of the given {@link Story} (the source is the main
367 * information used to group the stories together).
369 * In other words, <b>move</b> the {@link Story} into other source.
371 * The source can be a new one, it needs not exist before hand.
374 * the luid of the {@link Story} to move
378 void changeSource(String luid
, String newSource
) {
380 cacheLib
.changeSource(luid
, newSource
, null);
381 } catch (IOException e
) {
382 Instance
.getTraceHandler().error(e
);
387 * Change the title of the given {@link Story}.
390 * the luid of the {@link Story} to change
394 void changeTitle(String luid
, String newTitle
) {
396 cacheLib
.changeTitle(luid
, newTitle
, null);
397 } catch (IOException e
) {
398 Instance
.getTraceHandler().error(e
);
403 * Change the author of the given {@link Story}.
405 * The author can be a new one, it needs not exist before hand.
408 * the luid of the {@link Story} to change
412 void changeAuthor(String luid
, String newAuthor
) {
414 cacheLib
.changeAuthor(luid
, newAuthor
, null);
415 } catch (IOException e
) {
416 Instance
.getTraceHandler().error(e
);
421 * Simple shortcut method to call {link Instance#getTransGui()#getString()}.
424 * the ID to translate
426 * @return the translated result
428 static String
trans(StringIdGui id
, Object
... params
) {
429 return Instance
.getTransGui().getString(id
, params
);
433 * Start a frame and wait until it is closed before returning.
438 static private void sync(final JFrame frame
) {
439 if (EventQueue
.isDispatchThread()) {
440 throw new IllegalStateException(
441 "Cannot call a sync method in the dispatch thread");
444 final Boolean
[] done
= new Boolean
[] { false };
446 Runnable run
= new Runnable() {
450 frame
.addWindowListener(new WindowAdapter() {
452 public void windowClosing(WindowEvent e
) {
453 super.windowClosing(e
);
458 frame
.setVisible(true);
459 } catch (Exception e
) {
465 if (EventQueue
.isDispatchThread()) {
468 EventQueue
.invokeLater(run
);
470 } catch (Exception e
) {
471 Instance
.getTraceHandler().error(e
);
475 // This action must be synchronous, so wait until the frame is closed
479 } catch (InterruptedException e
) {