55701b1ad7b727d94470c449c368569e2aac3c5f
[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 // 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() {
136 @Override
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) {
143 Instance.getTraceHandler().error(ee);
144 } catch (URISyntaxException ee) {
145 Instance.getTraceHandler().error(ee);
146 }
147 }
148 });
149 updateMessage.setEditable(false);
150 updateMessage.setBackground(new JLabel().getBackground());
151 }
152
153 final String typeFinal = type;
154 EventQueue.invokeLater(new Runnable() {
155 @Override
156 public void run() {
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
168 try {
169 GuiReaderFrame gui = new GuiReaderFrame(GuiReader.this,
170 typeFinal);
171 sync(gui);
172 } catch (Exception e) {
173 Instance.getTraceHandler().error(e);
174 }
175 }
176 });
177 }
178
179 @Override
180 public void start(File target, String program, boolean sync)
181 throws IOException {
182
183 boolean handled = false;
184 if (program == null && !sync) {
185 try {
186 Desktop.getDesktop().browse(target.toURI());
187 handled = true;
188 } catch (UnsupportedOperationException e) {
189 }
190 }
191
192 if (!handled) {
193 super.start(target, program, sync);
194 }
195 }
196
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 */
207 void clearLocalReaderCache(String luid) {
208 try {
209 cacheLib.clearFromCache(luid);
210 } catch (IOException e) {
211 Instance.getTraceHandler().error(e);
212 }
213 }
214
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 */
224 void delete(String luid) {
225 try {
226 cacheLib.delete(luid);
227 } catch (IOException e) {
228 Instance.getTraceHandler().error(e);
229 }
230 }
231
232 /**
233 * "Open" the given {@link Story}. It usually involves starting an external
234 * program adapted to the given file type.
235 * <p>
236 * Asynchronous method.
237 *
238 * @param luid
239 * the luid of the {@link Story} to open
240 * @param sync
241 * execute the process synchronously (wait until it is terminated
242 * before returning)
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 */
250 void read(String luid, boolean sync, Progress pg) throws IOException {
251 File file = cacheLib.getFile(luid, pg);
252
253 // TODO: show a special page for the chapter?
254 // We could also implement an internal viewer, both for image and
255 // non-image documents
256 openExternal(getLibrary().getInfo(luid), file, sync);
257 }
258
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) {
273 try {
274 cacheLib.changeSource(luid, newSource, null);
275 } catch (IOException e) {
276 Instance.getTraceHandler().error(e);
277 }
278 }
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 }
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 }
356 }