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