Commit | Line | Data |
---|---|---|
16a81ef7 | 1 | package be.nikiroo.fanfix.reader.ui; |
a6395bef | 2 | |
edd46289 | 3 | import java.awt.Desktop; |
a6395bef | 4 | import java.awt.EventQueue; |
350bc060 NR |
5 | import java.awt.event.WindowAdapter; |
6 | import java.awt.event.WindowEvent; | |
a6395bef NR |
7 | import java.io.File; |
8 | import java.io.IOException; | |
b42117f1 NR |
9 | import java.net.URISyntaxException; |
10 | ||
11 | import javax.swing.JEditorPane; | |
12b9873f | 12 | import javax.swing.JFrame; |
b42117f1 NR |
13 | import javax.swing.JLabel; |
14 | import javax.swing.JOptionPane; | |
15 | import javax.swing.event.HyperlinkEvent; | |
16 | import javax.swing.event.HyperlinkListener; | |
a6395bef NR |
17 | |
18 | import be.nikiroo.fanfix.Instance; | |
b42117f1 | 19 | import be.nikiroo.fanfix.VersionCheck; |
5bc9573b | 20 | import be.nikiroo.fanfix.bundles.StringIdGui; |
dd81a122 | 21 | import be.nikiroo.fanfix.bundles.UiConfig; |
bc2ea776 | 22 | import be.nikiroo.fanfix.data.MetaData; |
a6395bef | 23 | import be.nikiroo.fanfix.data.Story; |
ff05b828 NR |
24 | import be.nikiroo.fanfix.library.BasicLibrary; |
25 | import be.nikiroo.fanfix.library.CacheLibrary; | |
16a81ef7 | 26 | import be.nikiroo.fanfix.reader.BasicReader; |
c3b229a1 | 27 | import be.nikiroo.fanfix.reader.Reader; |
81acd363 NR |
28 | import be.nikiroo.fanfix.searchable.BasicSearchable; |
29 | import be.nikiroo.fanfix.searchable.SearchableTag; | |
91b82a5c | 30 | import be.nikiroo.fanfix.supported.SupportType; |
3b2b638f | 31 | import be.nikiroo.utils.Progress; |
b42117f1 | 32 | import be.nikiroo.utils.Version; |
b0e88ebd | 33 | import be.nikiroo.utils.ui.UIUtils; |
a6395bef | 34 | |
c3b229a1 NR |
35 | /** |
36 | * This class implements a graphical {@link Reader} using the Swing library from | |
37 | * Java. | |
38 | * <p> | |
39 | * It can thus be themed to look native-like, or metal-like, or use any other | |
40 | * theme you may want to try. | |
41 | * <p> | |
42 | * We actually try to enable native look-alike mode on start. | |
43 | * | |
44 | * @author niki | |
45 | */ | |
5dd985cf | 46 | class GuiReader extends BasicReader { |
b0e88ebd NR |
47 | static private boolean nativeLookLoaded; |
48 | ||
ff05b828 NR |
49 | private CacheLibrary cacheLib; |
50 | ||
51 | private File cacheDir; | |
a6395bef | 52 | |
c3b229a1 NR |
53 | /** |
54 | * Create a new graphical {@link Reader}. | |
55 | * | |
56 | * @throws IOException | |
57 | * in case of I/O errors | |
58 | */ | |
5dd985cf | 59 | public GuiReader() throws IOException { |
c3b229a1 | 60 | // TODO: allow different themes? |
b0e88ebd NR |
61 | if (!nativeLookLoaded) { |
62 | UIUtils.setLookAndFeel(); | |
63 | nativeLookLoaded = true; | |
64 | } | |
65 | ||
ff05b828 NR |
66 | cacheDir = Instance.getReaderDir(); |
67 | cacheDir.mkdirs(); | |
68 | if (!cacheDir.exists()) { | |
a6395bef | 69 | throw new IOException( |
ff05b828 NR |
70 | "Cannote create cache directory for local reader: " |
71 | + cacheDir); | |
72 | } | |
73 | } | |
74 | ||
75 | @Override | |
76 | public synchronized BasicLibrary getLibrary() { | |
77 | if (cacheLib == null) { | |
78 | BasicLibrary lib = super.getLibrary(); | |
79 | if (lib instanceof CacheLibrary) { | |
80 | cacheLib = (CacheLibrary) lib; | |
81 | } else { | |
82 | cacheLib = new CacheLibrary(cacheDir, lib); | |
83 | } | |
a6395bef NR |
84 | } |
85 | ||
ff05b828 | 86 | return cacheLib; |
a6395bef NR |
87 | } |
88 | ||
211f7ddb | 89 | @Override |
350bc060 | 90 | public void read(boolean sync) throws IOException { |
bc2ea776 NR |
91 | MetaData meta = getMeta(); |
92 | ||
93 | if (meta == null) { | |
edd46289 NR |
94 | throw new IOException("No story to read"); |
95 | } | |
96 | ||
350bc060 | 97 | read(meta.getLuid(), sync, null); |
a6395bef NR |
98 | } |
99 | ||
9843a5e5 NR |
100 | /** |
101 | * Check if the {@link Story} denoted by this Library UID is present in the | |
5dd985cf | 102 | * {@link GuiReader} cache. |
9843a5e5 NR |
103 | * |
104 | * @param luid | |
105 | * the Library UID | |
106 | * | |
107 | * @return TRUE if it is | |
108 | */ | |
10d558d2 | 109 | public boolean isCached(String luid) { |
ff05b828 | 110 | return cacheLib.isCached(luid); |
10d558d2 NR |
111 | } |
112 | ||
211f7ddb | 113 | @Override |
b0e88ebd | 114 | public void browse(String type) { |
7a3eb29f NR |
115 | final Boolean[] done = new Boolean[] { false }; |
116 | ||
b42117f1 NR |
117 | // TODO: improve presentation of update message |
118 | final VersionCheck updates = VersionCheck.check(); | |
119 | StringBuilder builder = new StringBuilder(); | |
120 | ||
121 | final JEditorPane updateMessage = new JEditorPane("text/html", ""); | |
122 | if (updates.isNewVersionAvailable()) { | |
5bc9573b NR |
123 | builder.append(trans(StringIdGui.NEW_VERSION_AVAILABLE, |
124 | "<span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>")); | |
b42117f1 NR |
125 | builder.append("<br>"); |
126 | builder.append("<br>"); | |
127 | for (Version v : updates.getNewer()) { | |
5bc9573b NR |
128 | builder.append("\t<b>" |
129 | + trans(StringIdGui.NEW_VERSION_VERSION, v.toString()) | |
130 | + "</b>"); | |
b42117f1 NR |
131 | builder.append("<br>"); |
132 | builder.append("<ul>"); | |
133 | for (String item : updates.getChanges().get(v)) { | |
134 | builder.append("<li>" + item + "</li>"); | |
135 | } | |
136 | builder.append("</ul>"); | |
137 | } | |
138 | ||
139 | // html content | |
140 | updateMessage.setText("<html><body>" // | |
141 | + builder// | |
142 | + "</body></html>"); | |
143 | ||
144 | // handle link events | |
145 | updateMessage.addHyperlinkListener(new HyperlinkListener() { | |
211f7ddb | 146 | @Override |
b42117f1 NR |
147 | public void hyperlinkUpdate(HyperlinkEvent e) { |
148 | if (e.getEventType().equals( | |
149 | HyperlinkEvent.EventType.ACTIVATED)) | |
150 | try { | |
151 | Desktop.getDesktop().browse(e.getURL().toURI()); | |
152 | } catch (IOException ee) { | |
62c63b07 | 153 | Instance.getTraceHandler().error(ee); |
b42117f1 | 154 | } catch (URISyntaxException ee) { |
62c63b07 | 155 | Instance.getTraceHandler().error(ee); |
b42117f1 NR |
156 | } |
157 | } | |
158 | }); | |
159 | updateMessage.setEditable(false); | |
160 | updateMessage.setBackground(new JLabel().getBackground()); | |
161 | } | |
162 | ||
333f0e7b | 163 | final String typeFinal = type; |
a6395bef | 164 | EventQueue.invokeLater(new Runnable() { |
211f7ddb | 165 | @Override |
a6395bef | 166 | public void run() { |
b42117f1 NR |
167 | if (updates.isNewVersionAvailable()) { |
168 | int rep = JOptionPane.showConfirmDialog(null, | |
5bc9573b NR |
169 | updateMessage, |
170 | trans(StringIdGui.NEW_VERSION_TITLE), | |
b42117f1 NR |
171 | JOptionPane.OK_CANCEL_OPTION); |
172 | if (rep == JOptionPane.OK_OPTION) { | |
173 | updates.ok(); | |
174 | } else { | |
175 | updates.ignore(); | |
176 | } | |
177 | } | |
178 | ||
7a3eb29f NR |
179 | new Thread(new Runnable() { |
180 | @Override | |
181 | public void run() { | |
182 | try { | |
183 | GuiReaderFrame gui = new GuiReaderFrame( | |
184 | GuiReader.this, typeFinal); | |
185 | sync(gui); | |
186 | } catch (Exception e) { | |
187 | Instance.getTraceHandler().error(e); | |
188 | } finally { | |
189 | done[0] = true; | |
190 | } | |
191 | ||
192 | } | |
193 | }).start(); | |
a6395bef NR |
194 | } |
195 | }); | |
7a3eb29f NR |
196 | |
197 | // This action must be synchronous, so wait until the frame is closed | |
198 | while (!done[0]) { | |
199 | try { | |
200 | Thread.sleep(100); | |
201 | } catch (InterruptedException e) { | |
202 | } | |
203 | } | |
a6395bef | 204 | } |
10d558d2 | 205 | |
16a81ef7 | 206 | @Override |
350bc060 NR |
207 | public void start(File target, String program, boolean sync) |
208 | throws IOException { | |
209 | ||
210 | boolean handled = false; | |
211 | if (program == null && !sync) { | |
16a81ef7 NR |
212 | try { |
213 | Desktop.getDesktop().browse(target.toURI()); | |
350bc060 | 214 | handled = true; |
16a81ef7 | 215 | } catch (UnsupportedOperationException e) { |
16a81ef7 | 216 | } |
350bc060 NR |
217 | } |
218 | ||
219 | if (!handled) { | |
220 | super.start(target, program, sync); | |
16a81ef7 NR |
221 | } |
222 | } | |
0b39fb9f | 223 | |
91b82a5c | 224 | @Override |
b31a0db0 | 225 | public void search(boolean sync) throws IOException { |
741e8467 | 226 | GuiReaderSearchFrame search = new GuiReaderSearchFrame(this); |
f76de465 | 227 | if (sync) { |
81acd363 NR |
228 | sync(search); |
229 | } else { | |
230 | search.setVisible(true); | |
f76de465 | 231 | } |
91b82a5c | 232 | } |
0b39fb9f | 233 | |
b31a0db0 NR |
234 | @Override |
235 | public void search(SupportType searchOn, String keywords, int page, | |
236 | int item, boolean sync) { | |
9c598207 | 237 | GuiReaderSearchFrame search = new GuiReaderSearchFrame(this); |
81acd363 | 238 | search.search(searchOn, keywords, page, item); |
b31a0db0 NR |
239 | if (sync) { |
240 | sync(search); | |
241 | } else { | |
242 | search.setVisible(true); | |
243 | } | |
244 | } | |
245 | ||
91b82a5c | 246 | @Override |
81acd363 NR |
247 | public void searchTag(final SupportType searchOn, final int page, |
248 | final int item, final boolean sync, final Integer... tags) { | |
249 | ||
9c598207 | 250 | final GuiReaderSearchFrame search = new GuiReaderSearchFrame(this); |
7cc1e743 | 251 | |
81acd363 NR |
252 | final BasicSearchable searchable = BasicSearchable |
253 | .getSearchable(searchOn); | |
254 | ||
255 | Runnable action = new Runnable() { | |
256 | @Override | |
257 | public void run() { | |
258 | SearchableTag tag = null; | |
259 | try { | |
260 | tag = searchable.getTag(tags); | |
261 | } catch (IOException e) { | |
262 | Instance.getTraceHandler().error(e); | |
263 | } | |
264 | ||
265 | search.searchTag(searchOn, page, item, tag); | |
266 | ||
267 | if (sync) { | |
268 | sync(search); | |
269 | } else { | |
270 | search.setVisible(true); | |
271 | } | |
272 | } | |
273 | }; | |
274 | ||
f76de465 | 275 | if (sync) { |
81acd363 NR |
276 | action.run(); |
277 | } else { | |
278 | new Thread(action).start(); | |
f76de465 | 279 | } |
91b82a5c | 280 | } |
16a81ef7 | 281 | |
c3b229a1 NR |
282 | /** |
283 | * Delete the {@link Story} from the cache if it is present, but <b>NOT</b> | |
284 | * from the main library. | |
285 | * <p> | |
286 | * The next time we try to retrieve the {@link Story}, it may be required to | |
287 | * cache it again. | |
288 | * | |
289 | * @param luid | |
290 | * the luid of the {@link Story} | |
291 | */ | |
754a5bc2 | 292 | void clearLocalReaderCache(String luid) { |
68e2c6d2 | 293 | try { |
ff05b828 | 294 | cacheLib.clearFromCache(luid); |
68e2c6d2 | 295 | } catch (IOException e) { |
62c63b07 | 296 | Instance.getTraceHandler().error(e); |
68e2c6d2 | 297 | } |
10d558d2 NR |
298 | } |
299 | ||
c3b229a1 NR |
300 | /** |
301 | * Forward the delete operation to the main library. | |
302 | * <p> | |
303 | * The {@link Story} will be deleted from the main library as well as the | |
304 | * cache if present. | |
305 | * | |
306 | * @param luid | |
307 | * the {@link Story} to delete | |
308 | */ | |
10d558d2 | 309 | void delete(String luid) { |
68e2c6d2 | 310 | try { |
ff05b828 | 311 | cacheLib.delete(luid); |
68e2c6d2 | 312 | } catch (IOException e) { |
62c63b07 | 313 | Instance.getTraceHandler().error(e); |
68e2c6d2 | 314 | } |
10d558d2 | 315 | } |
edd46289 | 316 | |
c3b229a1 NR |
317 | /** |
318 | * "Open" the given {@link Story}. It usually involves starting an external | |
319 | * program adapted to the given file type. | |
320 | * | |
321 | * @param luid | |
322 | * the luid of the {@link Story} to open | |
350bc060 NR |
323 | * @param sync |
324 | * execute the process synchronously (wait until it is terminated | |
325 | * before returning) | |
c3b229a1 NR |
326 | * @param pg |
327 | * the optional progress (we may need to prepare the | |
328 | * {@link Story} for reading | |
329 | * | |
330 | * @throws IOException | |
331 | * in case of I/O errors | |
332 | */ | |
350bc060 | 333 | void read(String luid, boolean sync, Progress pg) throws IOException { |
dd81a122 | 334 | MetaData meta = cacheLib.getInfo(luid); |
edd46289 | 335 | |
dd81a122 NR |
336 | boolean textInternal = Instance.getUiConfig().getBoolean( |
337 | UiConfig.NON_IMAGES_DOCUMENT_USE_INTERNAL_READER, true); | |
338 | boolean imageInternal = Instance.getUiConfig().getBoolean( | |
339 | UiConfig.IMAGES_DOCUMENT_USE_INTERNAL_READER, true); | |
47b1710c | 340 | |
dd81a122 NR |
341 | boolean useInternalViewer = true; |
342 | if (meta.isImageDocument() && !imageInternal) { | |
343 | useInternalViewer = false; | |
344 | } | |
345 | if (!meta.isImageDocument() && !textInternal) { | |
346 | useInternalViewer = false; | |
347 | } | |
348 | ||
349 | if (useInternalViewer) { | |
350 | GuiReaderViewer viewer = new GuiReaderViewer(cacheLib, | |
351 | cacheLib.getStory(luid, null)); | |
47b1710c NR |
352 | if (sync) { |
353 | sync(viewer); | |
354 | } else { | |
355 | viewer.setVisible(true); | |
356 | } | |
1ee67095 | 357 | } else { |
dd81a122 NR |
358 | File file = cacheLib.getFile(luid, pg); |
359 | openExternal(meta, file, sync); | |
47b1710c | 360 | } |
edd46289 | 361 | } |
70c9b112 | 362 | |
ca7d4e2f NR |
363 | |
364 | /** | |
365 | * "Prefetch" the given {@link Story}. | |
366 | * <p> | |
367 | * Synchronous method. | |
368 | * | |
369 | * @param luid | |
370 | * the luid of the {@link Story} to prefetch | |
371 | * @param pg | |
372 | * the optional progress (we may need to prepare the | |
373 | * {@link Story} for reading | |
374 | * | |
375 | * @throws IOException | |
376 | * in case of I/O errors | |
377 | */ | |
378 | void prefetch(String luid, Progress pg) throws IOException { | |
379 | cacheLib.getFile(luid, pg); | |
380 | } | |
c3b229a1 NR |
381 | /** |
382 | * Change the source of the given {@link Story} (the source is the main | |
383 | * information used to group the stories together). | |
384 | * <p> | |
385 | * In other words, <b>move</b> the {@link Story} into other source. | |
386 | * <p> | |
387 | * The source can be a new one, it needs not exist before hand. | |
388 | * | |
389 | * @param luid | |
390 | * the luid of the {@link Story} to move | |
391 | * @param newSource | |
392 | * the new source | |
393 | */ | |
394 | void changeSource(String luid, String newSource) { | |
68e2c6d2 | 395 | try { |
ff05b828 | 396 | cacheLib.changeSource(luid, newSource, null); |
68e2c6d2 | 397 | } catch (IOException e) { |
62c63b07 | 398 | Instance.getTraceHandler().error(e); |
68e2c6d2 | 399 | } |
70c9b112 | 400 | } |
c8d48938 NR |
401 | |
402 | /** | |
403 | * Change the title of the given {@link Story}. | |
404 | * | |
405 | * @param luid | |
406 | * the luid of the {@link Story} to change | |
407 | * @param newTitle | |
408 | * the new title | |
409 | */ | |
410 | void changeTitle(String luid, String newTitle) { | |
411 | try { | |
412 | cacheLib.changeTitle(luid, newTitle, null); | |
413 | } catch (IOException e) { | |
414 | Instance.getTraceHandler().error(e); | |
415 | } | |
416 | } | |
417 | ||
418 | /** | |
419 | * Change the author of the given {@link Story}. | |
420 | * <p> | |
421 | * The author can be a new one, it needs not exist before hand. | |
422 | * | |
423 | * @param luid | |
424 | * the luid of the {@link Story} to change | |
425 | * @param newAuthor | |
426 | * the new author | |
427 | */ | |
428 | void changeAuthor(String luid, String newAuthor) { | |
429 | try { | |
430 | cacheLib.changeAuthor(luid, newAuthor, null); | |
431 | } catch (IOException e) { | |
432 | Instance.getTraceHandler().error(e); | |
433 | } | |
434 | } | |
12b9873f | 435 | |
5bc9573b NR |
436 | /** |
437 | * Simple shortcut method to call {link Instance#getTransGui()#getString()}. | |
438 | * | |
439 | * @param id | |
440 | * the ID to translate | |
441 | * | |
442 | * @return the translated result | |
443 | */ | |
444 | static String trans(StringIdGui id, Object... params) { | |
445 | return Instance.getTransGui().getString(id, params); | |
446 | } | |
447 | ||
12b9873f NR |
448 | /** |
449 | * Start a frame and wait until it is closed before returning. | |
450 | * | |
451 | * @param frame | |
452 | * the frame to start | |
453 | */ | |
454 | static private void sync(final JFrame frame) { | |
7a3eb29f NR |
455 | if (EventQueue.isDispatchThread()) { |
456 | throw new IllegalStateException( | |
457 | "Cannot call a sync method in the dispatch thread"); | |
458 | } | |
12b9873f | 459 | |
7a3eb29f | 460 | final Boolean[] done = new Boolean[] { false }; |
12b9873f | 461 | try { |
7a3eb29f | 462 | Runnable run = new Runnable() { |
12b9873f NR |
463 | @Override |
464 | public void run() { | |
465 | try { | |
466 | frame.addWindowListener(new WindowAdapter() { | |
467 | @Override | |
468 | public void windowClosing(WindowEvent e) { | |
469 | super.windowClosing(e); | |
470 | done[0] = true; | |
471 | } | |
472 | }); | |
473 | ||
474 | frame.setVisible(true); | |
475 | } catch (Exception e) { | |
476 | done[0] = true; | |
477 | } | |
478 | } | |
7a3eb29f NR |
479 | }; |
480 | ||
481 | if (EventQueue.isDispatchThread()) { | |
482 | run.run(); | |
483 | } else { | |
484 | EventQueue.invokeLater(run); | |
485 | } | |
12b9873f NR |
486 | } catch (Exception e) { |
487 | Instance.getTraceHandler().error(e); | |
488 | done[0] = true; | |
489 | } | |
490 | ||
491 | // This action must be synchronous, so wait until the frame is closed | |
492 | while (!done[0]) { | |
493 | try { | |
494 | Thread.sleep(100); | |
495 | } catch (InterruptedException e) { | |
496 | } | |
497 | } | |
498 | } | |
a6395bef | 499 | } |