9f2fd3dad1056c6ba4635a70856e2983f330fad0
[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.JLabel;
13 import javax.swing.JOptionPane;
14 import javax.swing.event.HyperlinkEvent;
15 import javax.swing.event.HyperlinkListener;
16
17 import be.nikiroo.fanfix.Instance;
18 import be.nikiroo.fanfix.VersionCheck;
19 import be.nikiroo.fanfix.data.MetaData;
20 import be.nikiroo.fanfix.data.Story;
21 import be.nikiroo.fanfix.library.BasicLibrary;
22 import be.nikiroo.fanfix.library.CacheLibrary;
23 import be.nikiroo.fanfix.reader.BasicReader;
24 import be.nikiroo.fanfix.reader.Reader;
25 import be.nikiroo.utils.Progress;
26 import be.nikiroo.utils.Version;
27 import be.nikiroo.utils.ui.UIUtils;
28
29 /**
30 * This class implements a graphical {@link Reader} using the Swing library from
31 * Java.
32 * <p>
33 * It can thus be themed to look native-like, or metal-like, or use any other
34 * theme you may want to try.
35 * <p>
36 * We actually try to enable native look-alike mode on start.
37 *
38 * @author niki
39 */
40 class GuiReader extends BasicReader {
41 static private boolean nativeLookLoaded;
42
43 private CacheLibrary cacheLib;
44
45 private File cacheDir;
46
47 /**
48 * Create a new graphical {@link Reader}.
49 *
50 * @throws IOException
51 * in case of I/O errors
52 */
53 public GuiReader() throws IOException {
54 // TODO: allow different themes?
55 if (!nativeLookLoaded) {
56 UIUtils.setLookAndFeel();
57 nativeLookLoaded = true;
58 }
59
60 cacheDir = Instance.getReaderDir();
61 cacheDir.mkdirs();
62 if (!cacheDir.exists()) {
63 throw new IOException(
64 "Cannote create cache directory for local reader: "
65 + cacheDir);
66 }
67 }
68
69 @Override
70 public synchronized BasicLibrary getLibrary() {
71 if (cacheLib == null) {
72 BasicLibrary lib = super.getLibrary();
73 if (lib instanceof CacheLibrary) {
74 cacheLib = (CacheLibrary) lib;
75 } else {
76 cacheLib = new CacheLibrary(cacheDir, lib);
77 }
78 }
79
80 return cacheLib;
81 }
82
83 @Override
84 public void read(boolean sync) throws IOException {
85 MetaData meta = getMeta();
86
87 if (meta == null) {
88 throw new IOException("No story to read");
89 }
90
91 read(meta.getLuid(), sync, null);
92 }
93
94 /**
95 * Check if the {@link Story} denoted by this Library UID is present in the
96 * {@link GuiReader} cache.
97 *
98 * @param luid
99 * the Library UID
100 *
101 * @return TRUE if it is
102 */
103 public boolean isCached(String luid) {
104 return cacheLib.isCached(luid);
105 }
106
107 @Override
108 public void browse(String type) {
109 // TODO: improve presentation of update message
110 final VersionCheck updates = VersionCheck.check();
111 StringBuilder builder = new StringBuilder();
112 final Boolean[] done = new Boolean[] { false };
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 gui.addWindowListener(new WindowAdapter() {
172 @Override
173 public void windowClosing(WindowEvent e) {
174 super.windowClosing(e);
175 done[0] = true;
176 }
177 });
178
179 gui.setVisible(true);
180 } catch (Exception e) {
181 Instance.getTraceHandler().error(e);
182 done[0] = true;
183 }
184 }
185 });
186
187 // This action must be synchronous, so wait until the frame is closed
188 while (!done[0]) {
189 try {
190 Thread.sleep(100);
191 } catch (InterruptedException e) {
192 }
193 }
194 }
195
196 @Override
197 public void start(File target, String program, boolean sync)
198 throws IOException {
199
200 boolean handled = false;
201 if (program == null && !sync) {
202 try {
203 Desktop.getDesktop().browse(target.toURI());
204 handled = true;
205 } catch (UnsupportedOperationException e) {
206 }
207 }
208
209 if (!handled) {
210 super.start(target, program, sync);
211 }
212 }
213
214 /**
215 * Delete the {@link Story} from the cache if it is present, but <b>NOT</b>
216 * from the main library.
217 * <p>
218 * The next time we try to retrieve the {@link Story}, it may be required to
219 * cache it again.
220 *
221 * @param luid
222 * the luid of the {@link Story}
223 */
224 void clearLocalReaderCache(String luid) {
225 try {
226 cacheLib.clearFromCache(luid);
227 } catch (IOException e) {
228 Instance.getTraceHandler().error(e);
229 }
230 }
231
232 /**
233 * Forward the delete operation to the main library.
234 * <p>
235 * The {@link Story} will be deleted from the main library as well as the
236 * cache if present.
237 *
238 * @param luid
239 * the {@link Story} to delete
240 */
241 void delete(String luid) {
242 try {
243 cacheLib.delete(luid);
244 } catch (IOException e) {
245 Instance.getTraceHandler().error(e);
246 }
247 }
248
249 /**
250 * "Open" the given {@link Story}. It usually involves starting an external
251 * program adapted to the given file type.
252 * <p>
253 * Asynchronous method.
254 *
255 * @param luid
256 * the luid of the {@link Story} to open
257 * @param sync
258 * execute the process synchronously (wait until it is terminated
259 * before returning)
260 * @param pg
261 * the optional progress (we may need to prepare the
262 * {@link Story} for reading
263 *
264 * @throws IOException
265 * in case of I/O errors
266 */
267 void read(String luid, boolean sync, Progress pg) throws IOException {
268 File file = cacheLib.getFile(luid, pg);
269
270 // TODO: show a special page for the chapter?
271 // We could also implement an internal viewer, both for image and
272 // non-image documents
273 openExternal(getLibrary().getInfo(luid), file, sync);
274 }
275
276 /**
277 * Change the source of the given {@link Story} (the source is the main
278 * information used to group the stories together).
279 * <p>
280 * In other words, <b>move</b> the {@link Story} into other source.
281 * <p>
282 * The source can be a new one, it needs not exist before hand.
283 *
284 * @param luid
285 * the luid of the {@link Story} to move
286 * @param newSource
287 * the new source
288 */
289 void changeSource(String luid, String newSource) {
290 try {
291 cacheLib.changeSource(luid, newSource, null);
292 } catch (IOException e) {
293 Instance.getTraceHandler().error(e);
294 }
295 }
296
297 /**
298 * Change the title of the given {@link Story}.
299 *
300 * @param luid
301 * the luid of the {@link Story} to change
302 * @param newTitle
303 * the new title
304 */
305 void changeTitle(String luid, String newTitle) {
306 try {
307 cacheLib.changeTitle(luid, newTitle, null);
308 } catch (IOException e) {
309 Instance.getTraceHandler().error(e);
310 }
311 }
312
313 /**
314 * Change the author of the given {@link Story}.
315 * <p>
316 * The author can be a new one, it needs not exist before hand.
317 *
318 * @param luid
319 * the luid of the {@link Story} to change
320 * @param newAuthor
321 * the new author
322 */
323 void changeAuthor(String luid, String newAuthor) {
324 try {
325 cacheLib.changeAuthor(luid, newAuthor, null);
326 } catch (IOException e) {
327 Instance.getTraceHandler().error(e);
328 }
329 }
330 }