Commit | Line | Data |
---|---|---|
16a81ef7 | 1 | package be.nikiroo.fanfix.reader.ui; |
a6395bef | 2 | |
edd46289 | 3 | import java.awt.Desktop; |
a6395bef | 4 | import java.awt.EventQueue; |
350bc060 NR |
5 | import java.awt.event.WindowAdapter; |
6 | import java.awt.event.WindowEvent; | |
a6395bef NR |
7 | import java.io.File; |
8 | import java.io.IOException; | |
b42117f1 NR |
9 | import java.net.URISyntaxException; |
10 | ||
11 | import javax.swing.JEditorPane; | |
12b9873f | 12 | import javax.swing.JFrame; |
b42117f1 NR |
13 | import javax.swing.JLabel; |
14 | import javax.swing.JOptionPane; | |
15 | import javax.swing.event.HyperlinkEvent; | |
16 | import javax.swing.event.HyperlinkListener; | |
a6395bef NR |
17 | |
18 | import be.nikiroo.fanfix.Instance; | |
b42117f1 | 19 | import be.nikiroo.fanfix.VersionCheck; |
bc2ea776 | 20 | import be.nikiroo.fanfix.data.MetaData; |
a6395bef | 21 | import be.nikiroo.fanfix.data.Story; |
ff05b828 NR |
22 | import be.nikiroo.fanfix.library.BasicLibrary; |
23 | import be.nikiroo.fanfix.library.CacheLibrary; | |
16a81ef7 | 24 | import be.nikiroo.fanfix.reader.BasicReader; |
c3b229a1 | 25 | import be.nikiroo.fanfix.reader.Reader; |
3b2b638f | 26 | import be.nikiroo.utils.Progress; |
b42117f1 | 27 | import be.nikiroo.utils.Version; |
b0e88ebd | 28 | import 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 | 41 | class 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) { |
b42117f1 NR |
110 | // TODO: improve presentation of update message |
111 | final VersionCheck updates = VersionCheck.check(); | |
112 | StringBuilder builder = new StringBuilder(); | |
113 | ||
114 | final JEditorPane updateMessage = new JEditorPane("text/html", ""); | |
115 | if (updates.isNewVersionAvailable()) { | |
116 | builder.append("A new version of the program is available at <span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>"); | |
117 | builder.append("<br>"); | |
118 | builder.append("<br>"); | |
119 | for (Version v : updates.getNewer()) { | |
120 | builder.append("\t<b>Version " + v + "</b>"); | |
121 | builder.append("<br>"); | |
122 | builder.append("<ul>"); | |
123 | for (String item : updates.getChanges().get(v)) { | |
124 | builder.append("<li>" + item + "</li>"); | |
125 | } | |
126 | builder.append("</ul>"); | |
127 | } | |
128 | ||
129 | // html content | |
130 | updateMessage.setText("<html><body>" // | |
131 | + builder// | |
132 | + "</body></html>"); | |
133 | ||
134 | // handle link events | |
135 | updateMessage.addHyperlinkListener(new HyperlinkListener() { | |
211f7ddb | 136 | @Override |
b42117f1 NR |
137 | public void hyperlinkUpdate(HyperlinkEvent e) { |
138 | if (e.getEventType().equals( | |
139 | HyperlinkEvent.EventType.ACTIVATED)) | |
140 | try { | |
141 | Desktop.getDesktop().browse(e.getURL().toURI()); | |
142 | } catch (IOException ee) { | |
62c63b07 | 143 | Instance.getTraceHandler().error(ee); |
b42117f1 | 144 | } catch (URISyntaxException ee) { |
62c63b07 | 145 | Instance.getTraceHandler().error(ee); |
b42117f1 NR |
146 | } |
147 | } | |
148 | }); | |
149 | updateMessage.setEditable(false); | |
150 | updateMessage.setBackground(new JLabel().getBackground()); | |
151 | } | |
152 | ||
333f0e7b | 153 | final String typeFinal = type; |
a6395bef | 154 | EventQueue.invokeLater(new Runnable() { |
211f7ddb | 155 | @Override |
a6395bef | 156 | public void run() { |
b42117f1 NR |
157 | if (updates.isNewVersionAvailable()) { |
158 | int rep = JOptionPane.showConfirmDialog(null, | |
159 | updateMessage, "Updates available", | |
160 | JOptionPane.OK_CANCEL_OPTION); | |
161 | if (rep == JOptionPane.OK_OPTION) { | |
162 | updates.ok(); | |
163 | } else { | |
164 | updates.ignore(); | |
165 | } | |
166 | } | |
167 | ||
350bc060 NR |
168 | try { |
169 | GuiReaderFrame gui = new GuiReaderFrame(GuiReader.this, | |
170 | typeFinal); | |
12b9873f | 171 | sync(gui); |
350bc060 NR |
172 | } catch (Exception e) { |
173 | Instance.getTraceHandler().error(e); | |
350bc060 | 174 | } |
a6395bef NR |
175 | } |
176 | }); | |
177 | } | |
10d558d2 | 178 | |
16a81ef7 | 179 | @Override |
350bc060 NR |
180 | public void start(File target, String program, boolean sync) |
181 | throws IOException { | |
182 | ||
183 | boolean handled = false; | |
184 | if (program == null && !sync) { | |
16a81ef7 NR |
185 | try { |
186 | Desktop.getDesktop().browse(target.toURI()); | |
350bc060 | 187 | handled = true; |
16a81ef7 | 188 | } catch (UnsupportedOperationException e) { |
16a81ef7 | 189 | } |
350bc060 NR |
190 | } |
191 | ||
192 | if (!handled) { | |
193 | super.start(target, program, sync); | |
16a81ef7 NR |
194 | } |
195 | } | |
196 | ||
c3b229a1 NR |
197 | /** |
198 | * Delete the {@link Story} from the cache if it is present, but <b>NOT</b> | |
199 | * from the main library. | |
200 | * <p> | |
201 | * The next time we try to retrieve the {@link Story}, it may be required to | |
202 | * cache it again. | |
203 | * | |
204 | * @param luid | |
205 | * the luid of the {@link Story} | |
206 | */ | |
754a5bc2 | 207 | void clearLocalReaderCache(String luid) { |
68e2c6d2 | 208 | try { |
ff05b828 | 209 | cacheLib.clearFromCache(luid); |
68e2c6d2 | 210 | } catch (IOException e) { |
62c63b07 | 211 | Instance.getTraceHandler().error(e); |
68e2c6d2 | 212 | } |
10d558d2 NR |
213 | } |
214 | ||
c3b229a1 NR |
215 | /** |
216 | * Forward the delete operation to the main library. | |
217 | * <p> | |
218 | * The {@link Story} will be deleted from the main library as well as the | |
219 | * cache if present. | |
220 | * | |
221 | * @param luid | |
222 | * the {@link Story} to delete | |
223 | */ | |
10d558d2 | 224 | void delete(String luid) { |
68e2c6d2 | 225 | try { |
ff05b828 | 226 | cacheLib.delete(luid); |
68e2c6d2 | 227 | } catch (IOException e) { |
62c63b07 | 228 | Instance.getTraceHandler().error(e); |
68e2c6d2 | 229 | } |
10d558d2 | 230 | } |
edd46289 | 231 | |
c3b229a1 NR |
232 | /** |
233 | * "Open" the given {@link Story}. It usually involves starting an external | |
234 | * program adapted to the given file type. | |
350bc060 NR |
235 | * <p> |
236 | * Asynchronous method. | |
c3b229a1 NR |
237 | * |
238 | * @param luid | |
239 | * the luid of the {@link Story} to open | |
350bc060 NR |
240 | * @param sync |
241 | * execute the process synchronously (wait until it is terminated | |
242 | * before returning) | |
c3b229a1 NR |
243 | * @param pg |
244 | * the optional progress (we may need to prepare the | |
245 | * {@link Story} for reading | |
246 | * | |
247 | * @throws IOException | |
248 | * in case of I/O errors | |
249 | */ | |
350bc060 | 250 | void read(String luid, boolean sync, Progress pg) throws IOException { |
ff05b828 | 251 | File file = cacheLib.getFile(luid, pg); |
edd46289 | 252 | |
bc2ea776 | 253 | // TODO: show a special page for the chapter? |
c3b229a1 NR |
254 | // We could also implement an internal viewer, both for image and |
255 | // non-image documents | |
350bc060 | 256 | openExternal(getLibrary().getInfo(luid), file, sync); |
edd46289 | 257 | } |
70c9b112 | 258 | |
c3b229a1 NR |
259 | /** |
260 | * Change the source of the given {@link Story} (the source is the main | |
261 | * information used to group the stories together). | |
262 | * <p> | |
263 | * In other words, <b>move</b> the {@link Story} into other source. | |
264 | * <p> | |
265 | * The source can be a new one, it needs not exist before hand. | |
266 | * | |
267 | * @param luid | |
268 | * the luid of the {@link Story} to move | |
269 | * @param newSource | |
270 | * the new source | |
271 | */ | |
272 | void changeSource(String luid, String newSource) { | |
68e2c6d2 | 273 | try { |
ff05b828 | 274 | cacheLib.changeSource(luid, newSource, null); |
68e2c6d2 | 275 | } catch (IOException e) { |
62c63b07 | 276 | Instance.getTraceHandler().error(e); |
68e2c6d2 | 277 | } |
70c9b112 | 278 | } |
c8d48938 NR |
279 | |
280 | /** | |
281 | * Change the title of the given {@link Story}. | |
282 | * | |
283 | * @param luid | |
284 | * the luid of the {@link Story} to change | |
285 | * @param newTitle | |
286 | * the new title | |
287 | */ | |
288 | void changeTitle(String luid, String newTitle) { | |
289 | try { | |
290 | cacheLib.changeTitle(luid, newTitle, null); | |
291 | } catch (IOException e) { | |
292 | Instance.getTraceHandler().error(e); | |
293 | } | |
294 | } | |
295 | ||
296 | /** | |
297 | * Change the author of the given {@link Story}. | |
298 | * <p> | |
299 | * The author can be a new one, it needs not exist before hand. | |
300 | * | |
301 | * @param luid | |
302 | * the luid of the {@link Story} to change | |
303 | * @param newAuthor | |
304 | * the new author | |
305 | */ | |
306 | void changeAuthor(String luid, String newAuthor) { | |
307 | try { | |
308 | cacheLib.changeAuthor(luid, newAuthor, null); | |
309 | } catch (IOException e) { | |
310 | Instance.getTraceHandler().error(e); | |
311 | } | |
312 | } | |
12b9873f NR |
313 | |
314 | /** | |
315 | * Start a frame and wait until it is closed before returning. | |
316 | * | |
317 | * @param frame | |
318 | * the frame to start | |
319 | */ | |
320 | static private void sync(final JFrame frame) { | |
321 | final Boolean[] done = new Boolean[1]; | |
322 | done[0] = false; | |
323 | ||
324 | try { | |
325 | EventQueue.invokeLater(new Runnable() { | |
326 | @Override | |
327 | public void run() { | |
328 | try { | |
329 | frame.addWindowListener(new WindowAdapter() { | |
330 | @Override | |
331 | public void windowClosing(WindowEvent e) { | |
332 | super.windowClosing(e); | |
333 | done[0] = true; | |
334 | } | |
335 | }); | |
336 | ||
337 | frame.setVisible(true); | |
338 | } catch (Exception e) { | |
339 | done[0] = true; | |
340 | } | |
341 | } | |
342 | }); | |
343 | } catch (Exception e) { | |
344 | Instance.getTraceHandler().error(e); | |
345 | done[0] = true; | |
346 | } | |
347 | ||
348 | // This action must be synchronous, so wait until the frame is closed | |
349 | while (!done[0]) { | |
350 | try { | |
351 | Thread.sleep(100); | |
352 | } catch (InterruptedException e) { | |
353 | } | |
354 | } | |
355 | } | |
a6395bef | 356 | } |