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