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
.data
.MetaData
;
21 import be
.nikiroo
.fanfix
.data
.Story
;
22 import be
.nikiroo
.fanfix
.library
.BasicLibrary
;
23 import be
.nikiroo
.fanfix
.library
.CacheLibrary
;
24 import be
.nikiroo
.fanfix
.reader
.BasicReader
;
25 import be
.nikiroo
.fanfix
.reader
.Reader
;
26 import be
.nikiroo
.utils
.Progress
;
27 import be
.nikiroo
.utils
.Version
;
28 import be
.nikiroo
.utils
.ui
.UIUtils
;
31 * This class implements a graphical {@link Reader} using the Swing library from
34 * It can thus be themed to look native-like, or metal-like, or use any other
35 * theme you may want to try.
37 * We actually try to enable native look-alike mode on start.
41 class GuiReader
extends BasicReader
{
42 static private boolean nativeLookLoaded
;
44 private CacheLibrary cacheLib
;
46 private File cacheDir
;
49 * Create a new graphical {@link Reader}.
52 * in case of I/O errors
54 public GuiReader() throws IOException
{
55 // TODO: allow different themes?
56 if (!nativeLookLoaded
) {
57 UIUtils
.setLookAndFeel();
58 nativeLookLoaded
= true;
61 cacheDir
= Instance
.getReaderDir();
63 if (!cacheDir
.exists()) {
64 throw new IOException(
65 "Cannote create cache directory for local reader: "
71 public synchronized BasicLibrary
getLibrary() {
72 if (cacheLib
== null) {
73 BasicLibrary lib
= super.getLibrary();
74 if (lib
instanceof CacheLibrary
) {
75 cacheLib
= (CacheLibrary
) lib
;
77 cacheLib
= new CacheLibrary(cacheDir
, lib
);
85 public void read(boolean sync
) throws IOException
{
86 MetaData meta
= getMeta();
89 throw new IOException("No story to read");
92 read(meta
.getLuid(), sync
, null);
96 * Check if the {@link Story} denoted by this Library UID is present in the
97 * {@link GuiReader} cache.
102 * @return TRUE if it is
104 public boolean isCached(String luid
) {
105 return cacheLib
.isCached(luid
);
109 public void browse(String type
) {
110 final Boolean
[] done
= new Boolean
[] { false };
112 // TODO: improve presentation of update message
113 final VersionCheck updates
= VersionCheck
.check();
114 StringBuilder builder
= new StringBuilder();
116 final JEditorPane updateMessage
= new JEditorPane("text/html", "");
117 if (updates
.isNewVersionAvailable()) {
118 builder
.append("A new version of the program is available at <span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>");
119 builder
.append("<br>");
120 builder
.append("<br>");
121 for (Version v
: updates
.getNewer()) {
122 builder
.append("\t<b>Version " + v
+ "</b>");
123 builder
.append("<br>");
124 builder
.append("<ul>");
125 for (String item
: updates
.getChanges().get(v
)) {
126 builder
.append("<li>" + item
+ "</li>");
128 builder
.append("</ul>");
132 updateMessage
.setText("<html><body>" //
136 // handle link events
137 updateMessage
.addHyperlinkListener(new HyperlinkListener() {
139 public void hyperlinkUpdate(HyperlinkEvent e
) {
140 if (e
.getEventType().equals(
141 HyperlinkEvent
.EventType
.ACTIVATED
))
143 Desktop
.getDesktop().browse(e
.getURL().toURI());
144 } catch (IOException ee
) {
145 Instance
.getTraceHandler().error(ee
);
146 } catch (URISyntaxException ee
) {
147 Instance
.getTraceHandler().error(ee
);
151 updateMessage
.setEditable(false);
152 updateMessage
.setBackground(new JLabel().getBackground());
155 final String typeFinal
= type
;
156 EventQueue
.invokeLater(new Runnable() {
159 if (updates
.isNewVersionAvailable()) {
160 int rep
= JOptionPane
.showConfirmDialog(null,
161 updateMessage
, "Updates available",
162 JOptionPane
.OK_CANCEL_OPTION
);
163 if (rep
== JOptionPane
.OK_OPTION
) {
170 new Thread(new Runnable() {
174 GuiReaderFrame gui
= new GuiReaderFrame(
175 GuiReader
.this, typeFinal
);
177 } catch (Exception e
) {
178 Instance
.getTraceHandler().error(e
);
188 // This action must be synchronous, so wait until the frame is closed
192 } catch (InterruptedException e
) {
198 public void start(File target
, String program
, boolean sync
)
201 boolean handled
= false;
202 if (program
== null && !sync
) {
204 Desktop
.getDesktop().browse(target
.toURI());
206 } catch (UnsupportedOperationException e
) {
211 super.start(target
, program
, sync
);
216 * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
217 * from the main library.
219 * The next time we try to retrieve the {@link Story}, it may be required to
223 * the luid of the {@link Story}
225 void clearLocalReaderCache(String luid
) {
227 cacheLib
.clearFromCache(luid
);
228 } catch (IOException e
) {
229 Instance
.getTraceHandler().error(e
);
234 * Forward the delete operation to the main library.
236 * The {@link Story} will be deleted from the main library as well as the
240 * the {@link Story} to delete
242 void delete(String luid
) {
244 cacheLib
.delete(luid
);
245 } catch (IOException e
) {
246 Instance
.getTraceHandler().error(e
);
251 * "Open" the given {@link Story}. It usually involves starting an external
252 * program adapted to the given file type.
254 * Asynchronous method.
257 * the luid of the {@link Story} to open
259 * execute the process synchronously (wait until it is terminated
262 * the optional progress (we may need to prepare the
263 * {@link Story} for reading
265 * @throws IOException
266 * in case of I/O errors
268 void read(String luid
, boolean sync
, Progress pg
) throws IOException
{
269 File file
= cacheLib
.getFile(luid
, pg
);
271 // TODO: show a special page for the chapter?
272 // We could also implement an internal viewer, both for image and
273 // non-image documents
274 openExternal(getLibrary().getInfo(luid
), file
, sync
);
278 * Change the source of the given {@link Story} (the source is the main
279 * information used to group the stories together).
281 * In other words, <b>move</b> the {@link Story} into other source.
283 * The source can be a new one, it needs not exist before hand.
286 * the luid of the {@link Story} to move
290 void changeSource(String luid
, String newSource
) {
292 cacheLib
.changeSource(luid
, newSource
, null);
293 } catch (IOException e
) {
294 Instance
.getTraceHandler().error(e
);
299 * Change the title of the given {@link Story}.
302 * the luid of the {@link Story} to change
306 void changeTitle(String luid
, String newTitle
) {
308 cacheLib
.changeTitle(luid
, newTitle
, null);
309 } catch (IOException e
) {
310 Instance
.getTraceHandler().error(e
);
315 * Change the author of the given {@link Story}.
317 * The author can be a new one, it needs not exist before hand.
320 * the luid of the {@link Story} to change
324 void changeAuthor(String luid
, String newAuthor
) {
326 cacheLib
.changeAuthor(luid
, newAuthor
, null);
327 } catch (IOException e
) {
328 Instance
.getTraceHandler().error(e
);
333 * Start a frame and wait until it is closed before returning.
338 static private void sync(final JFrame frame
) {
339 if (EventQueue
.isDispatchThread()) {
340 throw new IllegalStateException(
341 "Cannot call a sync method in the dispatch thread");
344 final Boolean
[] done
= new Boolean
[] { false };
346 Runnable run
= new Runnable() {
350 frame
.addWindowListener(new WindowAdapter() {
352 public void windowClosing(WindowEvent e
) {
353 super.windowClosing(e
);
358 frame
.setVisible(true);
359 } catch (Exception e
) {
365 if (EventQueue
.isDispatchThread()) {
368 EventQueue
.invokeLater(run
);
370 } catch (Exception e
) {
371 Instance
.getTraceHandler().error(e
);
375 // This action must be synchronous, so wait until the frame is closed
379 } catch (InterruptedException e
) {