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