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