Implement a text story viewer
[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
47b1710c
NR
271 GuiReaderTextViewer viewer = new GuiReaderTextViewer(cacheLib,
272 cacheLib.getStory(luid, null));
273
274 // TODO: testing text viewer:
275 if (false) {
276 if (sync) {
277 sync(viewer);
278 } else {
279 viewer.setVisible(true);
280 }
281 }
282
350bc060 283 openExternal(getLibrary().getInfo(luid), file, sync);
edd46289 284 }
70c9b112 285
c3b229a1
NR
286 /**
287 * Change the source of the given {@link Story} (the source is the main
288 * information used to group the stories together).
289 * <p>
290 * In other words, <b>move</b> the {@link Story} into other source.
291 * <p>
292 * The source can be a new one, it needs not exist before hand.
293 *
294 * @param luid
295 * the luid of the {@link Story} to move
296 * @param newSource
297 * the new source
298 */
299 void changeSource(String luid, String newSource) {
68e2c6d2 300 try {
ff05b828 301 cacheLib.changeSource(luid, newSource, null);
68e2c6d2 302 } catch (IOException e) {
62c63b07 303 Instance.getTraceHandler().error(e);
68e2c6d2 304 }
70c9b112 305 }
c8d48938
NR
306
307 /**
308 * Change the title of the given {@link Story}.
309 *
310 * @param luid
311 * the luid of the {@link Story} to change
312 * @param newTitle
313 * the new title
314 */
315 void changeTitle(String luid, String newTitle) {
316 try {
317 cacheLib.changeTitle(luid, newTitle, null);
318 } catch (IOException e) {
319 Instance.getTraceHandler().error(e);
320 }
321 }
322
323 /**
324 * Change the author of the given {@link Story}.
325 * <p>
326 * The author can be a new one, it needs not exist before hand.
327 *
328 * @param luid
329 * the luid of the {@link Story} to change
330 * @param newAuthor
331 * the new author
332 */
333 void changeAuthor(String luid, String newAuthor) {
334 try {
335 cacheLib.changeAuthor(luid, newAuthor, null);
336 } catch (IOException e) {
337 Instance.getTraceHandler().error(e);
338 }
339 }
12b9873f
NR
340
341 /**
342 * Start a frame and wait until it is closed before returning.
343 *
344 * @param frame
345 * the frame to start
346 */
347 static private void sync(final JFrame frame) {
7a3eb29f
NR
348 if (EventQueue.isDispatchThread()) {
349 throw new IllegalStateException(
350 "Cannot call a sync method in the dispatch thread");
351 }
12b9873f 352
7a3eb29f 353 final Boolean[] done = new Boolean[] { false };
12b9873f 354 try {
7a3eb29f 355 Runnable run = new Runnable() {
12b9873f
NR
356 @Override
357 public void run() {
358 try {
359 frame.addWindowListener(new WindowAdapter() {
360 @Override
361 public void windowClosing(WindowEvent e) {
362 super.windowClosing(e);
363 done[0] = true;
364 }
365 });
366
367 frame.setVisible(true);
368 } catch (Exception e) {
369 done[0] = true;
370 }
371 }
7a3eb29f
NR
372 };
373
374 if (EventQueue.isDispatchThread()) {
375 run.run();
376 } else {
377 EventQueue.invokeLater(run);
378 }
12b9873f
NR
379 } catch (Exception e) {
380 Instance.getTraceHandler().error(e);
381 done[0] = true;
382 }
383
384 // This action must be synchronous, so wait until the frame is closed
385 while (!done[0]) {
386 try {
387 Thread.sleep(100);
388 } catch (InterruptedException e) {
389 }
390 }
391 }
a6395bef 392}