GUI: Update internal viewer:
[fanfix.git] / src / be / nikiroo / fanfix / reader / ui / GuiReader.java
1 package be.nikiroo.fanfix.reader.ui;
2
3 import java.awt.Desktop;
4 import java.awt.EventQueue;
5 import java.awt.event.WindowAdapter;
6 import java.awt.event.WindowEvent;
7 import java.io.File;
8 import java.io.IOException;
9 import java.net.URISyntaxException;
10
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;
17
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;
29
30 /**
31 * This class implements a graphical {@link Reader} using the Swing library from
32 * Java.
33 * <p>
34 * It can thus be themed to look native-like, or metal-like, or use any other
35 * theme you may want to try.
36 * <p>
37 * We actually try to enable native look-alike mode on start.
38 *
39 * @author niki
40 */
41 class GuiReader extends BasicReader {
42 static private boolean nativeLookLoaded;
43
44 private CacheLibrary cacheLib;
45
46 private File cacheDir;
47
48 /**
49 * Create a new graphical {@link Reader}.
50 *
51 * @throws IOException
52 * in case of I/O errors
53 */
54 public GuiReader() throws IOException {
55 // TODO: allow different themes?
56 if (!nativeLookLoaded) {
57 UIUtils.setLookAndFeel();
58 nativeLookLoaded = true;
59 }
60
61 cacheDir = Instance.getReaderDir();
62 cacheDir.mkdirs();
63 if (!cacheDir.exists()) {
64 throw new IOException(
65 "Cannote create cache directory for local reader: "
66 + cacheDir);
67 }
68 }
69
70 @Override
71 public synchronized BasicLibrary getLibrary() {
72 if (cacheLib == null) {
73 BasicLibrary lib = super.getLibrary();
74 if (lib instanceof CacheLibrary) {
75 cacheLib = (CacheLibrary) lib;
76 } else {
77 cacheLib = new CacheLibrary(cacheDir, lib);
78 }
79 }
80
81 return cacheLib;
82 }
83
84 @Override
85 public void read(boolean sync) throws IOException {
86 MetaData meta = getMeta();
87
88 if (meta == null) {
89 throw new IOException("No story to read");
90 }
91
92 read(meta.getLuid(), sync, null);
93 }
94
95 /**
96 * Check if the {@link Story} denoted by this Library UID is present in the
97 * {@link GuiReader} cache.
98 *
99 * @param luid
100 * the Library UID
101 *
102 * @return TRUE if it is
103 */
104 public boolean isCached(String luid) {
105 return cacheLib.isCached(luid);
106 }
107
108 @Override
109 public void browse(String type) {
110 final Boolean[] done = new Boolean[] { false };
111
112 // TODO: improve presentation of update message
113 final VersionCheck updates = VersionCheck.check();
114 StringBuilder builder = new StringBuilder();
115
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>");
127 }
128 builder.append("</ul>");
129 }
130
131 // html content
132 updateMessage.setText("<html><body>" //
133 + builder//
134 + "</body></html>");
135
136 // handle link events
137 updateMessage.addHyperlinkListener(new HyperlinkListener() {
138 @Override
139 public void hyperlinkUpdate(HyperlinkEvent e) {
140 if (e.getEventType().equals(
141 HyperlinkEvent.EventType.ACTIVATED))
142 try {
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);
148 }
149 }
150 });
151 updateMessage.setEditable(false);
152 updateMessage.setBackground(new JLabel().getBackground());
153 }
154
155 final String typeFinal = type;
156 EventQueue.invokeLater(new Runnable() {
157 @Override
158 public void run() {
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) {
164 updates.ok();
165 } else {
166 updates.ignore();
167 }
168 }
169
170 new Thread(new Runnable() {
171 @Override
172 public void run() {
173 try {
174 GuiReaderFrame gui = new GuiReaderFrame(
175 GuiReader.this, typeFinal);
176 sync(gui);
177 } catch (Exception e) {
178 Instance.getTraceHandler().error(e);
179 } finally {
180 done[0] = true;
181 }
182
183 }
184 }).start();
185 }
186 });
187
188 // This action must be synchronous, so wait until the frame is closed
189 while (!done[0]) {
190 try {
191 Thread.sleep(100);
192 } catch (InterruptedException e) {
193 }
194 }
195 }
196
197 @Override
198 public void start(File target, String program, boolean sync)
199 throws IOException {
200
201 boolean handled = false;
202 if (program == null && !sync) {
203 try {
204 Desktop.getDesktop().browse(target.toURI());
205 handled = true;
206 } catch (UnsupportedOperationException e) {
207 }
208 }
209
210 if (!handled) {
211 super.start(target, program, sync);
212 }
213 }
214
215 /**
216 * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
217 * from the main library.
218 * <p>
219 * The next time we try to retrieve the {@link Story}, it may be required to
220 * cache it again.
221 *
222 * @param luid
223 * the luid of the {@link Story}
224 */
225 void clearLocalReaderCache(String luid) {
226 try {
227 cacheLib.clearFromCache(luid);
228 } catch (IOException e) {
229 Instance.getTraceHandler().error(e);
230 }
231 }
232
233 /**
234 * Forward the delete operation to the main library.
235 * <p>
236 * The {@link Story} will be deleted from the main library as well as the
237 * cache if present.
238 *
239 * @param luid
240 * the {@link Story} to delete
241 */
242 void delete(String luid) {
243 try {
244 cacheLib.delete(luid);
245 } catch (IOException e) {
246 Instance.getTraceHandler().error(e);
247 }
248 }
249
250 /**
251 * "Open" the given {@link Story}. It usually involves starting an external
252 * program adapted to the given file type.
253 * <p>
254 * Asynchronous method.
255 *
256 * @param luid
257 * the luid of the {@link Story} to open
258 * @param sync
259 * execute the process synchronously (wait until it is terminated
260 * before returning)
261 * @param pg
262 * the optional progress (we may need to prepare the
263 * {@link Story} for reading
264 *
265 * @throws IOException
266 * in case of I/O errors
267 */
268 void read(String luid, boolean sync, Progress pg) throws IOException {
269 File file = cacheLib.getFile(luid, pg);
270
271 GuiReaderViewer viewer = new GuiReaderViewer(cacheLib,
272 cacheLib.getStory(luid, null));
273
274 // TODO: testing internal story viewer:
275 if (false) {
276 if (sync) {
277 sync(viewer);
278 } else {
279 viewer.setVisible(true);
280 }
281 } else {
282 openExternal(getLibrary().getInfo(luid), file, sync);
283 }
284 }
285
286 /**
287 * Change the source of the given {@link Story} (the source is the main
288 * information used to group the stories together).
289 * <p>
290 * In other words, <b>move</b> the {@link Story} into other source.
291 * <p>
292 * The source can be a new one, it needs not exist before hand.
293 *
294 * @param luid
295 * the luid of the {@link Story} to move
296 * @param newSource
297 * the new source
298 */
299 void changeSource(String luid, String newSource) {
300 try {
301 cacheLib.changeSource(luid, newSource, null);
302 } catch (IOException e) {
303 Instance.getTraceHandler().error(e);
304 }
305 }
306
307 /**
308 * Change the title of the given {@link Story}.
309 *
310 * @param luid
311 * the luid of the {@link Story} to change
312 * @param newTitle
313 * the new title
314 */
315 void changeTitle(String luid, String newTitle) {
316 try {
317 cacheLib.changeTitle(luid, newTitle, null);
318 } catch (IOException e) {
319 Instance.getTraceHandler().error(e);
320 }
321 }
322
323 /**
324 * Change the author of the given {@link Story}.
325 * <p>
326 * The author can be a new one, it needs not exist before hand.
327 *
328 * @param luid
329 * the luid of the {@link Story} to change
330 * @param newAuthor
331 * the new author
332 */
333 void changeAuthor(String luid, String newAuthor) {
334 try {
335 cacheLib.changeAuthor(luid, newAuthor, null);
336 } catch (IOException e) {
337 Instance.getTraceHandler().error(e);
338 }
339 }
340
341 /**
342 * Start a frame and wait until it is closed before returning.
343 *
344 * @param frame
345 * the frame to start
346 */
347 static private void sync(final JFrame frame) {
348 if (EventQueue.isDispatchThread()) {
349 throw new IllegalStateException(
350 "Cannot call a sync method in the dispatch thread");
351 }
352
353 final Boolean[] done = new Boolean[] { false };
354 try {
355 Runnable run = new Runnable() {
356 @Override
357 public void run() {
358 try {
359 frame.addWindowListener(new WindowAdapter() {
360 @Override
361 public void windowClosing(WindowEvent e) {
362 super.windowClosing(e);
363 done[0] = true;
364 }
365 });
366
367 frame.setVisible(true);
368 } catch (Exception e) {
369 done[0] = true;
370 }
371 }
372 };
373
374 if (EventQueue.isDispatchThread()) {
375 run.run();
376 } else {
377 EventQueue.invokeLater(run);
378 }
379 } catch (Exception e) {
380 Instance.getTraceHandler().error(e);
381 done[0] = true;
382 }
383
384 // This action must be synchronous, so wait until the frame is closed
385 while (!done[0]) {
386 try {
387 Thread.sleep(100);
388 } catch (InterruptedException e) {
389 }
390 }
391 }
392 }