GUI search: reorg mostly OK
[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.fanfix.searchable.BasicSearchable;
29 import be.nikiroo.fanfix.searchable.SearchableTag;
30 import be.nikiroo.fanfix.supported.SupportType;
31 import be.nikiroo.utils.Progress;
32 import be.nikiroo.utils.Version;
33 import be.nikiroo.utils.ui.UIUtils;
34
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 */
46 class GuiReader extends BasicReader {
47 static private boolean nativeLookLoaded;
48
49 private CacheLibrary cacheLib;
50
51 private File cacheDir;
52
53 /**
54 * Create a new graphical {@link Reader}.
55 *
56 * @throws IOException
57 * in case of I/O errors
58 */
59 public GuiReader() throws IOException {
60 // TODO: allow different themes?
61 if (!nativeLookLoaded) {
62 UIUtils.setLookAndFeel();
63 nativeLookLoaded = true;
64 }
65
66 cacheDir = Instance.getReaderDir();
67 cacheDir.mkdirs();
68 if (!cacheDir.exists()) {
69 throw new IOException(
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 }
84 }
85
86 return cacheLib;
87 }
88
89 @Override
90 public void read(boolean sync) throws IOException {
91 MetaData meta = getMeta();
92
93 if (meta == null) {
94 throw new IOException("No story to read");
95 }
96
97 read(meta.getLuid(), sync, null);
98 }
99
100 /**
101 * Check if the {@link Story} denoted by this Library UID is present in the
102 * {@link GuiReader} cache.
103 *
104 * @param luid
105 * the Library UID
106 *
107 * @return TRUE if it is
108 */
109 public boolean isCached(String luid) {
110 return cacheLib.isCached(luid);
111 }
112
113 @Override
114 public void browse(String type) {
115 final Boolean[] done = new Boolean[] { false };
116
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()) {
123 builder.append(trans(StringIdGui.NEW_VERSION_AVAILABLE,
124 "<span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>"));
125 builder.append("<br>");
126 builder.append("<br>");
127 for (Version v : updates.getNewer()) {
128 builder.append("\t<b>"
129 + trans(StringIdGui.NEW_VERSION_VERSION, v.toString())
130 + "</b>");
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() {
146 @Override
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) {
153 Instance.getTraceHandler().error(ee);
154 } catch (URISyntaxException ee) {
155 Instance.getTraceHandler().error(ee);
156 }
157 }
158 });
159 updateMessage.setEditable(false);
160 updateMessage.setBackground(new JLabel().getBackground());
161 }
162
163 final String typeFinal = type;
164 EventQueue.invokeLater(new Runnable() {
165 @Override
166 public void run() {
167 if (updates.isNewVersionAvailable()) {
168 int rep = JOptionPane.showConfirmDialog(null,
169 updateMessage,
170 trans(StringIdGui.NEW_VERSION_TITLE),
171 JOptionPane.OK_CANCEL_OPTION);
172 if (rep == JOptionPane.OK_OPTION) {
173 updates.ok();
174 } else {
175 updates.ignore();
176 }
177 }
178
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();
194 }
195 });
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 }
204 }
205
206 @Override
207 public void start(File target, String program, boolean sync)
208 throws IOException {
209
210 boolean handled = false;
211 if (program == null && !sync) {
212 try {
213 Desktop.getDesktop().browse(target.toURI());
214 handled = true;
215 } catch (UnsupportedOperationException e) {
216 }
217 }
218
219 if (!handled) {
220 super.start(target, program, sync);
221 }
222 }
223
224 @Override
225 public void search(boolean sync) throws IOException {
226 GuiReaderSearchFrame search = new GuiReaderSearchFrame(this);
227 if (sync) {
228 sync(search);
229 } else {
230 search.setVisible(true);
231 }
232 }
233
234 @Override
235 public void search(SupportType searchOn, String keywords, int page,
236 int item, boolean sync) {
237 final GuiReaderSearchFrame search = new GuiReaderSearchFrame(
238 GuiReader.this);
239 while (!search.isEnabled()) {
240 try {
241 Thread.sleep(10);
242 } catch (InterruptedException e) {
243 Instance.getTraceHandler().error(e);
244 }
245 }
246
247 search.search(searchOn, keywords, page, item);
248 if (sync) {
249 sync(search);
250 } else {
251 search.setVisible(true);
252 }
253 }
254
255 @Override
256 public void searchTag(final SupportType searchOn, final int page,
257 final int item, final boolean sync, final Integer... tags) {
258
259 final GuiReaderSearchFrame search = new GuiReaderSearchFrame(
260 GuiReader.this);
261 while (!search.isEnabled()) {
262 try {
263 Thread.sleep(10);
264 } catch (InterruptedException e) {
265 Instance.getTraceHandler().error(e);
266 }
267 }
268
269 final BasicSearchable searchable = BasicSearchable
270 .getSearchable(searchOn);
271
272 Runnable action = new Runnable() {
273 @Override
274 public void run() {
275 SearchableTag tag = null;
276 try {
277 tag = searchable.getTag(tags);
278 } catch (IOException e) {
279 Instance.getTraceHandler().error(e);
280 }
281
282 search.searchTag(searchOn, page, item, tag);
283
284 if (sync) {
285 sync(search);
286 } else {
287 search.setVisible(true);
288 }
289 }
290 };
291
292 if (sync) {
293 action.run();
294 } else {
295 new Thread(action).start();
296 }
297 }
298
299 /**
300 * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
301 * from the main library.
302 * <p>
303 * The next time we try to retrieve the {@link Story}, it may be required to
304 * cache it again.
305 *
306 * @param luid
307 * the luid of the {@link Story}
308 */
309 void clearLocalReaderCache(String luid) {
310 try {
311 cacheLib.clearFromCache(luid);
312 } catch (IOException e) {
313 Instance.getTraceHandler().error(e);
314 }
315 }
316
317 /**
318 * Forward the delete operation to the main library.
319 * <p>
320 * The {@link Story} will be deleted from the main library as well as the
321 * cache if present.
322 *
323 * @param luid
324 * the {@link Story} to delete
325 */
326 void delete(String luid) {
327 try {
328 cacheLib.delete(luid);
329 } catch (IOException e) {
330 Instance.getTraceHandler().error(e);
331 }
332 }
333
334 /**
335 * "Open" the given {@link Story}. It usually involves starting an external
336 * program adapted to the given file type.
337 * <p>
338 * Asynchronous method.
339 *
340 * @param luid
341 * the luid of the {@link Story} to open
342 * @param sync
343 * execute the process synchronously (wait until it is terminated
344 * before returning)
345 * @param pg
346 * the optional progress (we may need to prepare the
347 * {@link Story} for reading
348 *
349 * @throws IOException
350 * in case of I/O errors
351 */
352 void read(String luid, boolean sync, Progress pg) throws IOException {
353 MetaData meta = cacheLib.getInfo(luid);
354
355 boolean textInternal = Instance.getUiConfig().getBoolean(
356 UiConfig.NON_IMAGES_DOCUMENT_USE_INTERNAL_READER, true);
357 boolean imageInternal = Instance.getUiConfig().getBoolean(
358 UiConfig.IMAGES_DOCUMENT_USE_INTERNAL_READER, true);
359
360 boolean useInternalViewer = true;
361 if (meta.isImageDocument() && !imageInternal) {
362 useInternalViewer = false;
363 }
364 if (!meta.isImageDocument() && !textInternal) {
365 useInternalViewer = false;
366 }
367
368 if (useInternalViewer) {
369 GuiReaderViewer viewer = new GuiReaderViewer(cacheLib,
370 cacheLib.getStory(luid, null));
371 if (sync) {
372 sync(viewer);
373 } else {
374 viewer.setVisible(true);
375 }
376 } else {
377 File file = cacheLib.getFile(luid, pg);
378 openExternal(meta, file, sync);
379 }
380 }
381
382 /**
383 * Change the source of the given {@link Story} (the source is the main
384 * information used to group the stories together).
385 * <p>
386 * In other words, <b>move</b> the {@link Story} into other source.
387 * <p>
388 * The source can be a new one, it needs not exist before hand.
389 *
390 * @param luid
391 * the luid of the {@link Story} to move
392 * @param newSource
393 * the new source
394 */
395 void changeSource(String luid, String newSource) {
396 try {
397 cacheLib.changeSource(luid, newSource, null);
398 } catch (IOException e) {
399 Instance.getTraceHandler().error(e);
400 }
401 }
402
403 /**
404 * Change the title of the given {@link Story}.
405 *
406 * @param luid
407 * the luid of the {@link Story} to change
408 * @param newTitle
409 * the new title
410 */
411 void changeTitle(String luid, String newTitle) {
412 try {
413 cacheLib.changeTitle(luid, newTitle, null);
414 } catch (IOException e) {
415 Instance.getTraceHandler().error(e);
416 }
417 }
418
419 /**
420 * Change the author of the given {@link Story}.
421 * <p>
422 * The author can be a new one, it needs not exist before hand.
423 *
424 * @param luid
425 * the luid of the {@link Story} to change
426 * @param newAuthor
427 * the new author
428 */
429 void changeAuthor(String luid, String newAuthor) {
430 try {
431 cacheLib.changeAuthor(luid, newAuthor, null);
432 } catch (IOException e) {
433 Instance.getTraceHandler().error(e);
434 }
435 }
436
437 /**
438 * Simple shortcut method to call {link Instance#getTransGui()#getString()}.
439 *
440 * @param id
441 * the ID to translate
442 *
443 * @return the translated result
444 */
445 static String trans(StringIdGui id, Object... params) {
446 return Instance.getTransGui().getString(id, params);
447 }
448
449 /**
450 * Start a frame and wait until it is closed before returning.
451 *
452 * @param frame
453 * the frame to start
454 */
455 static private void sync(final JFrame frame) {
456 if (EventQueue.isDispatchThread()) {
457 throw new IllegalStateException(
458 "Cannot call a sync method in the dispatch thread");
459 }
460
461 final Boolean[] done = new Boolean[] { false };
462 try {
463 Runnable run = new Runnable() {
464 @Override
465 public void run() {
466 try {
467 frame.addWindowListener(new WindowAdapter() {
468 @Override
469 public void windowClosing(WindowEvent e) {
470 super.windowClosing(e);
471 done[0] = true;
472 }
473 });
474
475 frame.setVisible(true);
476 } catch (Exception e) {
477 done[0] = true;
478 }
479 }
480 };
481
482 if (EventQueue.isDispatchThread()) {
483 run.run();
484 } else {
485 EventQueue.invokeLater(run);
486 }
487 } catch (Exception e) {
488 Instance.getTraceHandler().error(e);
489 done[0] = true;
490 }
491
492 // This action must be synchronous, so wait until the frame is closed
493 while (!done[0]) {
494 try {
495 Thread.sleep(100);
496 } catch (InterruptedException e) {
497 }
498 }
499 }
500 }