GUI search, step 3
[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;
81acd363
NR
28import be.nikiroo.fanfix.searchable.BasicSearchable;
29import be.nikiroo.fanfix.searchable.SearchableTag;
91b82a5c 30import be.nikiroo.fanfix.supported.SupportType;
3b2b638f 31import be.nikiroo.utils.Progress;
b42117f1 32import be.nikiroo.utils.Version;
b0e88ebd 33import 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 46class 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 {
81acd363 226 GuiReaderSearch search = new GuiReaderSearch(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) {
b31a0db0 237 GuiReaderSearch search = new GuiReaderSearch(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
250 final GuiReaderSearch search = new GuiReaderSearch(GuiReader.this);
251 final BasicSearchable searchable = BasicSearchable
252 .getSearchable(searchOn);
253
254 Runnable action = new Runnable() {
255 @Override
256 public void run() {
257 SearchableTag tag = null;
258 try {
259 tag = searchable.getTag(tags);
260 } catch (IOException e) {
261 Instance.getTraceHandler().error(e);
262 }
263
264 search.searchTag(searchOn, page, item, tag);
265
266 if (sync) {
267 sync(search);
268 } else {
269 search.setVisible(true);
270 }
271 }
272 };
273
f76de465 274 if (sync) {
81acd363
NR
275 action.run();
276 } else {
277 new Thread(action).start();
f76de465 278 }
91b82a5c 279 }
16a81ef7 280
c3b229a1
NR
281 /**
282 * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
283 * from the main library.
284 * <p>
285 * The next time we try to retrieve the {@link Story}, it may be required to
286 * cache it again.
287 *
288 * @param luid
289 * the luid of the {@link Story}
290 */
754a5bc2 291 void clearLocalReaderCache(String luid) {
68e2c6d2 292 try {
ff05b828 293 cacheLib.clearFromCache(luid);
68e2c6d2 294 } catch (IOException e) {
62c63b07 295 Instance.getTraceHandler().error(e);
68e2c6d2 296 }
10d558d2
NR
297 }
298
c3b229a1
NR
299 /**
300 * Forward the delete operation to the main library.
301 * <p>
302 * The {@link Story} will be deleted from the main library as well as the
303 * cache if present.
304 *
305 * @param luid
306 * the {@link Story} to delete
307 */
10d558d2 308 void delete(String luid) {
68e2c6d2 309 try {
ff05b828 310 cacheLib.delete(luid);
68e2c6d2 311 } catch (IOException e) {
62c63b07 312 Instance.getTraceHandler().error(e);
68e2c6d2 313 }
10d558d2 314 }
edd46289 315
c3b229a1
NR
316 /**
317 * "Open" the given {@link Story}. It usually involves starting an external
318 * program adapted to the given file type.
350bc060
NR
319 * <p>
320 * Asynchronous method.
c3b229a1
NR
321 *
322 * @param luid
323 * the luid of the {@link Story} to open
350bc060
NR
324 * @param sync
325 * execute the process synchronously (wait until it is terminated
326 * before returning)
c3b229a1
NR
327 * @param pg
328 * the optional progress (we may need to prepare the
329 * {@link Story} for reading
330 *
331 * @throws IOException
332 * in case of I/O errors
333 */
350bc060 334 void read(String luid, boolean sync, Progress pg) throws IOException {
dd81a122 335 MetaData meta = cacheLib.getInfo(luid);
edd46289 336
dd81a122
NR
337 boolean textInternal = Instance.getUiConfig().getBoolean(
338 UiConfig.NON_IMAGES_DOCUMENT_USE_INTERNAL_READER, true);
339 boolean imageInternal = Instance.getUiConfig().getBoolean(
340 UiConfig.IMAGES_DOCUMENT_USE_INTERNAL_READER, true);
47b1710c 341
dd81a122
NR
342 boolean useInternalViewer = true;
343 if (meta.isImageDocument() && !imageInternal) {
344 useInternalViewer = false;
345 }
346 if (!meta.isImageDocument() && !textInternal) {
347 useInternalViewer = false;
348 }
349
350 if (useInternalViewer) {
351 GuiReaderViewer viewer = new GuiReaderViewer(cacheLib,
352 cacheLib.getStory(luid, null));
47b1710c
NR
353 if (sync) {
354 sync(viewer);
355 } else {
356 viewer.setVisible(true);
357 }
1ee67095 358 } else {
dd81a122
NR
359 File file = cacheLib.getFile(luid, pg);
360 openExternal(meta, file, sync);
47b1710c 361 }
edd46289 362 }
70c9b112 363
c3b229a1
NR
364 /**
365 * Change the source of the given {@link Story} (the source is the main
366 * information used to group the stories together).
367 * <p>
368 * In other words, <b>move</b> the {@link Story} into other source.
369 * <p>
370 * The source can be a new one, it needs not exist before hand.
371 *
372 * @param luid
373 * the luid of the {@link Story} to move
374 * @param newSource
375 * the new source
376 */
377 void changeSource(String luid, String newSource) {
68e2c6d2 378 try {
ff05b828 379 cacheLib.changeSource(luid, newSource, null);
68e2c6d2 380 } catch (IOException e) {
62c63b07 381 Instance.getTraceHandler().error(e);
68e2c6d2 382 }
70c9b112 383 }
c8d48938
NR
384
385 /**
386 * Change the title of the given {@link Story}.
387 *
388 * @param luid
389 * the luid of the {@link Story} to change
390 * @param newTitle
391 * the new title
392 */
393 void changeTitle(String luid, String newTitle) {
394 try {
395 cacheLib.changeTitle(luid, newTitle, null);
396 } catch (IOException e) {
397 Instance.getTraceHandler().error(e);
398 }
399 }
400
401 /**
402 * Change the author of the given {@link Story}.
403 * <p>
404 * The author can be a new one, it needs not exist before hand.
405 *
406 * @param luid
407 * the luid of the {@link Story} to change
408 * @param newAuthor
409 * the new author
410 */
411 void changeAuthor(String luid, String newAuthor) {
412 try {
413 cacheLib.changeAuthor(luid, newAuthor, null);
414 } catch (IOException e) {
415 Instance.getTraceHandler().error(e);
416 }
417 }
12b9873f 418
5bc9573b
NR
419 /**
420 * Simple shortcut method to call {link Instance#getTransGui()#getString()}.
421 *
422 * @param id
423 * the ID to translate
424 *
425 * @return the translated result
426 */
427 static String trans(StringIdGui id, Object... params) {
428 return Instance.getTransGui().getString(id, params);
429 }
430
12b9873f
NR
431 /**
432 * Start a frame and wait until it is closed before returning.
433 *
434 * @param frame
435 * the frame to start
436 */
437 static private void sync(final JFrame frame) {
7a3eb29f
NR
438 if (EventQueue.isDispatchThread()) {
439 throw new IllegalStateException(
440 "Cannot call a sync method in the dispatch thread");
441 }
12b9873f 442
7a3eb29f 443 final Boolean[] done = new Boolean[] { false };
12b9873f 444 try {
7a3eb29f 445 Runnable run = new Runnable() {
12b9873f
NR
446 @Override
447 public void run() {
448 try {
449 frame.addWindowListener(new WindowAdapter() {
450 @Override
451 public void windowClosing(WindowEvent e) {
452 super.windowClosing(e);
453 done[0] = true;
454 }
455 });
456
457 frame.setVisible(true);
458 } catch (Exception e) {
459 done[0] = true;
460 }
461 }
7a3eb29f
NR
462 };
463
464 if (EventQueue.isDispatchThread()) {
465 run.run();
466 } else {
467 EventQueue.invokeLater(run);
468 }
12b9873f
NR
469 } catch (Exception e) {
470 Instance.getTraceHandler().error(e);
471 done[0] = true;
472 }
473
474 // This action must be synchronous, so wait until the frame is closed
475 while (!done[0]) {
476 try {
477 Thread.sleep(100);
478 } catch (InterruptedException e) {
479 }
480 }
481 }
a6395bef 482}