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