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