c6c8413632e07bff43f99cb9dd0a8b575a59a055
[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.UiConfig;
21 import be.nikiroo.fanfix.data.MetaData;
22 import be.nikiroo.fanfix.data.Story;
23 import be.nikiroo.fanfix.library.BasicLibrary;
24 import be.nikiroo.fanfix.library.CacheLibrary;
25 import be.nikiroo.fanfix.reader.BasicReader;
26 import be.nikiroo.fanfix.reader.Reader;
27 import be.nikiroo.utils.Progress;
28 import be.nikiroo.utils.Version;
29 import be.nikiroo.utils.ui.UIUtils;
30
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 */
42 class GuiReader extends BasicReader {
43 static private boolean nativeLookLoaded;
44
45 private CacheLibrary cacheLib;
46
47 private File cacheDir;
48
49 /**
50 * Create a new graphical {@link Reader}.
51 *
52 * @throws IOException
53 * in case of I/O errors
54 */
55 public GuiReader() throws IOException {
56 // TODO: allow different themes?
57 if (!nativeLookLoaded) {
58 UIUtils.setLookAndFeel();
59 nativeLookLoaded = true;
60 }
61
62 cacheDir = Instance.getReaderDir();
63 cacheDir.mkdirs();
64 if (!cacheDir.exists()) {
65 throw new IOException(
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 }
80 }
81
82 return cacheLib;
83 }
84
85 @Override
86 public void read(boolean sync) throws IOException {
87 MetaData meta = getMeta();
88
89 if (meta == null) {
90 throw new IOException("No story to read");
91 }
92
93 read(meta.getLuid(), sync, null);
94 }
95
96 /**
97 * Check if the {@link Story} denoted by this Library UID is present in the
98 * {@link GuiReader} cache.
99 *
100 * @param luid
101 * the Library UID
102 *
103 * @return TRUE if it is
104 */
105 public boolean isCached(String luid) {
106 return cacheLib.isCached(luid);
107 }
108
109 @Override
110 public void browse(String type) {
111 final Boolean[] done = new Boolean[] { false };
112
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() {
139 @Override
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) {
146 Instance.getTraceHandler().error(ee);
147 } catch (URISyntaxException ee) {
148 Instance.getTraceHandler().error(ee);
149 }
150 }
151 });
152 updateMessage.setEditable(false);
153 updateMessage.setBackground(new JLabel().getBackground());
154 }
155
156 final String typeFinal = type;
157 EventQueue.invokeLater(new Runnable() {
158 @Override
159 public void run() {
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
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();
186 }
187 });
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 }
196 }
197
198 @Override
199 public void start(File target, String program, boolean sync)
200 throws IOException {
201
202 boolean handled = false;
203 if (program == null && !sync) {
204 try {
205 Desktop.getDesktop().browse(target.toURI());
206 handled = true;
207 } catch (UnsupportedOperationException e) {
208 }
209 }
210
211 if (!handled) {
212 super.start(target, program, sync);
213 }
214 }
215
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 */
226 void clearLocalReaderCache(String luid) {
227 try {
228 cacheLib.clearFromCache(luid);
229 } catch (IOException e) {
230 Instance.getTraceHandler().error(e);
231 }
232 }
233
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 */
243 void delete(String luid) {
244 try {
245 cacheLib.delete(luid);
246 } catch (IOException e) {
247 Instance.getTraceHandler().error(e);
248 }
249 }
250
251 /**
252 * "Open" the given {@link Story}. It usually involves starting an external
253 * program adapted to the given file type.
254 * <p>
255 * Asynchronous method.
256 *
257 * @param luid
258 * the luid of the {@link Story} to open
259 * @param sync
260 * execute the process synchronously (wait until it is terminated
261 * before returning)
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 */
269 void read(String luid, boolean sync, Progress pg) throws IOException {
270 MetaData meta = cacheLib.getInfo(luid);
271
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);
276
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));
288 if (sync) {
289 sync(viewer);
290 } else {
291 viewer.setVisible(true);
292 }
293 } else {
294 File file = cacheLib.getFile(luid, pg);
295 openExternal(meta, file, sync);
296 }
297 }
298
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) {
313 try {
314 cacheLib.changeSource(luid, newSource, null);
315 } catch (IOException e) {
316 Instance.getTraceHandler().error(e);
317 }
318 }
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 }
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) {
361 if (EventQueue.isDispatchThread()) {
362 throw new IllegalStateException(
363 "Cannot call a sync method in the dispatch thread");
364 }
365
366 final Boolean[] done = new Boolean[] { false };
367 try {
368 Runnable run = new Runnable() {
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 }
385 };
386
387 if (EventQueue.isDispatchThread()) {
388 run.run();
389 } else {
390 EventQueue.invokeLater(run);
391 }
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 }
405 }