Fanfiction step2 + SearchableTags
[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.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.utils.Progress;
29 import be.nikiroo.utils.Version;
30 import be.nikiroo.utils.ui.UIUtils;
31
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 */
43 class GuiReader extends BasicReader {
44 static private boolean nativeLookLoaded;
45
46 private CacheLibrary cacheLib;
47
48 private File cacheDir;
49
50 /**
51 * Create a new graphical {@link Reader}.
52 *
53 * @throws IOException
54 * in case of I/O errors
55 */
56 public GuiReader() throws IOException {
57 // TODO: allow different themes?
58 if (!nativeLookLoaded) {
59 UIUtils.setLookAndFeel();
60 nativeLookLoaded = true;
61 }
62
63 cacheDir = Instance.getReaderDir();
64 cacheDir.mkdirs();
65 if (!cacheDir.exists()) {
66 throw new IOException(
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 }
81 }
82
83 return cacheLib;
84 }
85
86 @Override
87 public void read(boolean sync) throws IOException {
88 MetaData meta = getMeta();
89
90 if (meta == null) {
91 throw new IOException("No story to read");
92 }
93
94 read(meta.getLuid(), sync, null);
95 }
96
97 /**
98 * Check if the {@link Story} denoted by this Library UID is present in the
99 * {@link GuiReader} cache.
100 *
101 * @param luid
102 * the Library UID
103 *
104 * @return TRUE if it is
105 */
106 public boolean isCached(String luid) {
107 return cacheLib.isCached(luid);
108 }
109
110 @Override
111 public void browse(String type) {
112 final Boolean[] done = new Boolean[] { false };
113
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()) {
120 builder.append(trans(StringIdGui.NEW_VERSION_AVAILABLE,
121 "<span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>"));
122 builder.append("<br>");
123 builder.append("<br>");
124 for (Version v : updates.getNewer()) {
125 builder.append("\t<b>"
126 + trans(StringIdGui.NEW_VERSION_VERSION, v.toString())
127 + "</b>");
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() {
143 @Override
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) {
150 Instance.getTraceHandler().error(ee);
151 } catch (URISyntaxException ee) {
152 Instance.getTraceHandler().error(ee);
153 }
154 }
155 });
156 updateMessage.setEditable(false);
157 updateMessage.setBackground(new JLabel().getBackground());
158 }
159
160 final String typeFinal = type;
161 EventQueue.invokeLater(new Runnable() {
162 @Override
163 public void run() {
164 if (updates.isNewVersionAvailable()) {
165 int rep = JOptionPane.showConfirmDialog(null,
166 updateMessage,
167 trans(StringIdGui.NEW_VERSION_TITLE),
168 JOptionPane.OK_CANCEL_OPTION);
169 if (rep == JOptionPane.OK_OPTION) {
170 updates.ok();
171 } else {
172 updates.ignore();
173 }
174 }
175
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();
191 }
192 });
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 }
201 }
202
203 @Override
204 public void start(File target, String program, boolean sync)
205 throws IOException {
206
207 boolean handled = false;
208 if (program == null && !sync) {
209 try {
210 Desktop.getDesktop().browse(target.toURI());
211 handled = true;
212 } catch (UnsupportedOperationException e) {
213 }
214 }
215
216 if (!handled) {
217 super.start(target, program, sync);
218 }
219 }
220
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 */
231 void clearLocalReaderCache(String luid) {
232 try {
233 cacheLib.clearFromCache(luid);
234 } catch (IOException e) {
235 Instance.getTraceHandler().error(e);
236 }
237 }
238
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 */
248 void delete(String luid) {
249 try {
250 cacheLib.delete(luid);
251 } catch (IOException e) {
252 Instance.getTraceHandler().error(e);
253 }
254 }
255
256 /**
257 * "Open" the given {@link Story}. It usually involves starting an external
258 * program adapted to the given file type.
259 * <p>
260 * Asynchronous method.
261 *
262 * @param luid
263 * the luid of the {@link Story} to open
264 * @param sync
265 * execute the process synchronously (wait until it is terminated
266 * before returning)
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 */
274 void read(String luid, boolean sync, Progress pg) throws IOException {
275 MetaData meta = cacheLib.getInfo(luid);
276
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);
281
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));
293 if (sync) {
294 sync(viewer);
295 } else {
296 viewer.setVisible(true);
297 }
298 } else {
299 File file = cacheLib.getFile(luid, pg);
300 openExternal(meta, file, sync);
301 }
302 }
303
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) {
318 try {
319 cacheLib.changeSource(luid, newSource, null);
320 } catch (IOException e) {
321 Instance.getTraceHandler().error(e);
322 }
323 }
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 }
358
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
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) {
378 if (EventQueue.isDispatchThread()) {
379 throw new IllegalStateException(
380 "Cannot call a sync method in the dispatch thread");
381 }
382
383 final Boolean[] done = new Boolean[] { false };
384 try {
385 Runnable run = new Runnable() {
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 }
402 };
403
404 if (EventQueue.isDispatchThread()) {
405 run.run();
406 } else {
407 EventQueue.invokeLater(run);
408 }
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 }
422 }