Version 1.4.0
authorNiki Roo <niki@nikiroo.be>
Sun, 5 Mar 2017 12:18:30 +0000 (13:18 +0100)
committerNiki Roo <niki@nikiroo.be>
Sun, 5 Mar 2017 12:18:30 +0000 (13:18 +0100)
- Remember the word count and the date of creation of Fanfix stories
- UI: option to show the word count instead of the author below the book
  title
- CBZ: do not include the first page twice anymore for no-cover websites
- UI: update version check (we now check for new versions)

VERSION
changelog.md
libs/nikiroo-utils-1.3.4-sources.jar [moved from libs/nikiroo-utils-1.3.3-sources.jar with 74% similarity]
src/be/nikiroo/fanfix/Instance.java
src/be/nikiroo/fanfix/Main.java
src/be/nikiroo/fanfix/VersionCheck.java [new file with mode: 0644]
src/be/nikiroo/fanfix/bundles/Config.java
src/be/nikiroo/fanfix/bundles/config.properties
src/be/nikiroo/fanfix/reader/LocalReader.java

diff --git a/VERSION b/VERSION
index 3a3cd8cc8b079cb410a465d2925b9cbd703115cb..88c5fb891dcf1d1647d2b84bac0630cf9570d213 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.3.1
+1.4.0
index 20b58702b40f68eafe819746113b5f10dc580681..1e638e3b409d2925cde3bea95dd36590c6687811 100644 (file)
@@ -1,10 +1,11 @@
 # Fanfix
 
-## Version (in progress)
+## Version 1.4.0
 
 - Remember the word count and the date of creation of Fanfix stories
 - UI: option to show the word count instead of the author below the book title
 - CBZ: do not include the first page twice anymore for no-cover websites
+- UI: update version check (we now check for new versions)
 
 ## Version 1.3.1
 
similarity index 74%
rename from libs/nikiroo-utils-1.3.3-sources.jar
rename to libs/nikiroo-utils-1.3.4-sources.jar
index a7c9ff1ee16247db7dc91a0088ee1b5b11a15ca5..f4b5883d12bbb6a81149ad8861d192e62f89412d 100644 (file)
Binary files a/libs/nikiroo-utils-1.3.3-sources.jar and b/libs/nikiroo-utils-1.3.4-sources.jar differ
index fdd73b8d12cd306bd8727cf9fbbae153363139a3..1266ac7e437f700564165dadec15521383f4083b 100644 (file)
@@ -2,6 +2,7 @@ package be.nikiroo.fanfix;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Date;
 
 import be.nikiroo.fanfix.bundles.Config;
 import be.nikiroo.fanfix.bundles.ConfigBundle;
@@ -9,6 +10,7 @@ import be.nikiroo.fanfix.bundles.StringIdBundle;
 import be.nikiroo.fanfix.bundles.UiConfig;
 import be.nikiroo.fanfix.bundles.UiConfigBundle;
 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
+import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.resources.Bundles;
 
 /**
@@ -25,49 +27,49 @@ public class Instance {
        private static boolean debug;
        private static File coverDir;
        private static File readerTmp;
+       private static String configDir;
 
        static {
                // Most of the rest is dependent upon this:
                config = new ConfigBundle();
 
-               String configDir = System.getProperty("CONFIG_DIR");
+               configDir = System.getProperty("CONFIG_DIR");
                if (configDir == null) {
                        configDir = System.getenv("CONFIG_DIR");
                }
+
                if (configDir == null) {
                        configDir = new File(System.getProperty("user.home"), ".fanfix")
                                        .getPath();
                }
 
-               if (configDir != null) {
-                       if (!new File(configDir).exists()) {
-                               new File(configDir).mkdirs();
-                       } else {
-                               Bundles.setDirectory(configDir);
-                       }
-
-                       try {
-                               config = new ConfigBundle();
-                               config.updateFile(configDir);
-                       } catch (IOException e) {
-                               syserr(e);
-                       }
-                       try {
-                               uiconfig = new UiConfigBundle();
-                               uiconfig.updateFile(configDir);
-                       } catch (IOException e) {
-                               syserr(e);
-                       }
-                       try {
-                               trans = new StringIdBundle(getLang());
-                               trans.updateFile(configDir);
-                       } catch (IOException e) {
-                               syserr(e);
-                       }
-
+               if (!new File(configDir).exists()) {
+                       new File(configDir).mkdirs();
+               } else {
                        Bundles.setDirectory(configDir);
                }
 
+               try {
+                       config = new ConfigBundle();
+                       config.updateFile(configDir);
+               } catch (IOException e) {
+                       syserr(e);
+               }
+               try {
+                       uiconfig = new UiConfigBundle();
+                       uiconfig.updateFile(configDir);
+               } catch (IOException e) {
+                       syserr(e);
+               }
+               try {
+                       trans = new StringIdBundle(getLang());
+                       trans.updateFile(configDir);
+               } catch (IOException e) {
+                       syserr(e);
+               }
+
+               Bundles.setDirectory(configDir);
+
                uiconfig = new UiConfigBundle();
                trans = new StringIdBundle(getLang());
                try {
@@ -183,6 +185,45 @@ public class Instance {
                return readerTmp;
        }
 
+       /**
+        * Check if we need to check that a new version of Fanfix is available.
+        * 
+        * @return TRUE if we need to
+        */
+       public static boolean isVersionCheckNeeded() {
+               try {
+                       long wait = config.getInteger(Config.UPDATE_INTERVAL, 1) * 24 * 60 * 60;
+                       if (wait >= 0) {
+                               String lastUpString = IOUtils.readSmallFile(new File(configDir,
+                                               "LAST_UPDATE"));
+                               long delay = new Date().getTime()
+                                               - Long.parseLong(lastUpString);
+                               if (delay > wait) {
+                                       return true;
+                               }
+                       } else {
+                               return false;
+                       }
+               } catch (Exception e) {
+                       // No file or bad file:
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Notify that we checked for a new version of Fanfix.
+        */
+       public static void setVersionChecked() {
+               try {
+                       IOUtils.writeSmallFile(new File(configDir), "LAST_UPDATE",
+                                       Long.toString(new Date().getTime()));
+               } catch (IOException e) {
+                       syserr(e);
+               }
+       }
+
        /**
         * Report an error to the user
         * 
index 06287f1236457860b833399274933202db9e7a55..85d74928458e1589926bd0e0ab0f8d34be725c78 100644 (file)
@@ -189,17 +189,38 @@ public class Main {
                Progress pg = new Progress();
                mainProgress.addProgress(pg, mainProgress.getMax());
 
+               VersionCheck updates = VersionCheck.check();
+               if (updates.isNewVersionAvailable()) {
+                       // Sent to syserr so not to cause problem if one tries to capture a
+                       // story content in text mode
+                       System.err
+                                       .println("A new version of the program is available at https://github.com/nikiroo/fanfix/releases");
+                       System.err.println("");
+                       for (Version v : updates.getNewer()) {
+                               System.err.println("\tVersion " + v);
+                               System.err.println("\t-------------");
+                               System.err.println("");
+                               for (String item : updates.getChanges().get(v)) {
+                                       System.err.println("\t- " + item);
+                               }
+                               System.err.println("");
+                       }
+               }
+
                if (exitCode != 255) {
                        switch (action) {
                        case IMPORT:
                                exitCode = imprt(urlString, pg);
+                               updates.ok(); // we consider it read
                                break;
                        case EXPORT:
                                exitCode = export(luid, typeString, target, pg);
+                               updates.ok(); // we consider it read
                                break;
                        case CONVERT:
                                exitCode = convert(urlString, typeString, target,
                                                plusInfo == null ? false : plusInfo, pg);
+                               updates.ok(); // we consider it read
                                break;
                        case LIST:
                                exitCode = list(typeString);
@@ -222,6 +243,7 @@ public class Main {
                                                                + "\nhttps://github.com/nikiroo/fanfix/"
                                                                + "\n\tWritten by Nikiroo",
                                                                Version.getCurrentVersion()));
+                               updates.ok(); // we consider it read
                                break;
                        case START:
                                UIUtils.setLookAndFeel();
diff --git a/src/be/nikiroo/fanfix/VersionCheck.java b/src/be/nikiroo/fanfix/VersionCheck.java
new file mode 100644 (file)
index 0000000..3359bac
--- /dev/null
@@ -0,0 +1,140 @@
+package be.nikiroo.fanfix;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import be.nikiroo.utils.Version;
+
+public class VersionCheck {
+       private static final String url = "https://github.com/nikiroo/fanfix/raw/master/changelog.md";
+
+       private Version current;
+       private List<Version> newer;
+       private Map<Version, List<String>> changes;
+
+       /**
+        * Create a new {@link VersionCheck}.
+        * 
+        * @param current
+        *            the current version of the program
+        * @param newer
+        *            the list of available {@link Version}s newer the current one
+        * @param changes
+        *            the list of changes
+        */
+       private VersionCheck(Version current, List<Version> newer,
+                       Map<Version, List<String>> changes) {
+               this.current = current;
+               this.newer = newer;
+               this.changes = changes;
+       }
+
+       /**
+        * Check if there are more recent {@link Version}s of this program
+        * available.
+        * 
+        * @return TRUE if there is at least one
+        */
+       public boolean isNewVersionAvailable() {
+               return !newer.isEmpty();
+       }
+
+       /**
+        * The current {@link Version} of the program.
+        * 
+        * @return the current {@link Version}
+        */
+       public Version getCurrentVersion() {
+               return current;
+       }
+
+       /**
+        * The list of available {@link Version}s newer than the current one.
+        * 
+        * @return the newer {@link Version}s
+        */
+       public List<Version> getNewer() {
+               return newer;
+       }
+
+       /**
+        * The list of changes for each available {@link Version} newer than the
+        * current one.
+        * 
+        * @return the list of changes
+        */
+       public Map<Version, List<String>> getChanges() {
+               return changes;
+       }
+
+       /**
+        * Ignore the check result.
+        */
+       public void ignore() {
+
+       }
+
+       /**
+        * Accept the information, and do not check again until the minimum wait
+        * time has elapsed.
+        */
+       public void ok() {
+               Instance.setVersionChecked();
+       }
+
+       /**
+        * Check if there are available {@link Version}s of this program more recent
+        * than the current one.
+        * 
+        * @return a {@link VersionCheck}
+        */
+       public static VersionCheck check() {
+               Version current = Version.getCurrentVersion();
+               List<Version> newer = new ArrayList<Version>();
+               Map<Version, List<String>> changes = new HashMap<Version, List<String>>();
+
+               if (Instance.isVersionCheckNeeded()) {
+                       try {
+                               InputStream in = Instance.getCache().openNoCache(new URL(url),
+                                               null);
+                               BufferedReader reader = new BufferedReader(
+                                               new InputStreamReader(in, "UTF-8"));
+                               try {
+                                       for (String line = reader.readLine(); line != null; line = reader
+                                                       .readLine()) {
+                                               if (line.startsWith("## Version ")) {
+                                                       String v = line.substring("## Version ".length());
+                                                       Version version = new Version(v);
+                                                       if (version.isNewerThan(current)) {
+                                                               newer.add(version);
+                                                               changes.put(version, new ArrayList<String>());
+                                                       }
+                                               } else if (!newer.isEmpty() && !line.isEmpty()) {
+                                                       Version version = newer.get(newer.size() - 1);
+                                                       List<String> ch = changes.get(version);
+                                                       if (!ch.isEmpty() && !line.startsWith("- ")) {
+                                                               int i = ch.size() - 1;
+                                                               ch.set(i, ch.get(i) + " " + line.trim());
+                                                       } else {
+                                                               ch.add(line.substring("- ".length()).trim());
+                                                       }
+                                               }
+                                       }
+                               } finally {
+                                       reader.close();
+                               }
+                       } catch (IOException e) {
+                               Instance.syserr(e);
+                       }
+               }
+
+               return new VersionCheck(current, newer, changes);
+       }
+}
index b68265e04865870aff23234dfe47a8eb5f1e4e3c..6a728c4e52a0432425adc66a01899819a2dacf6a 100644 (file)
@@ -8,44 +8,46 @@ import be.nikiroo.utils.resources.Meta;
  * @author niki
  */
 public enum Config {
-       @Meta(what = "language", where = "", format = "language (example: en-GB) or nothing for default system language", info = "Force the language (can be overwritten again with the env variable $LANG)")
+       @Meta(what = "language (example: en-GB, fr-BE...) or nothing for default system language", where = "", format = "Locale|''", info = "Force the language (can be overwritten again with the env variable $LANG)")
        LANG, //
-       @Meta(what = "reader type", where = "", format = "CLI or LOCAL", info = "Select the default reader to use to read stories (CLI = simple output to console, LOCAL = use local system file handler)")
+       @Meta(what = "reader type (CLI = simple output to console, LOCAL = use local system file handler)", where = "", format = "'CLI'|'LOCAL'", info = "Select the default reader to use to read stories")
        READER_TYPE, //
-       @Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)")
+       @Meta(what = "absolute path, $HOME variable supported, / is always accepted as dir separator", where = "", format = "Directory", info = "The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)")
        CACHE_DIR, //
-       @Meta(what = "delay in hours", where = "", format = "integer | 0: no cache | -1: infinite time cache which is default", info = "The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh")
+       @Meta(what = "delay in hours, or 0 for no cache, or -1 for infinite time (default)", where = "", format = "int", info = "The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh")
        CACHE_MAX_TIME_CHANGING, //
-       @Meta(what = "delay in hours", where = "", format = "integer | 0: no cache | -1: infinite time cache which is default", info = "The delay after which a cached resource that is thought to change rarely is considered too old and triggers a refresh")
+       @Meta(what = "delay in hours, or 0 for no cache, or -1 for infinite time (default)", where = "", format = "int", info = "The delay after which a cached resource that is thought to change rarely is considered too old and triggers a refresh")
        CACHE_MAX_TIME_STABLE, //
-       @Meta(what = "string", where = "", format = "", info = "The user-agent to use to download files")
+       @Meta(what = "string", where = "", format = "String", info = "The user-agent to use to download files")
        USER_AGENT, //
-       @Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to get the default story covers")
+       @Meta(what = "absolute path, $HOME variable supported, / is always accepted as dir separator", where = "", format = "Directory", info = "The directory where to get the default story covers")
        DEFAULT_COVERS_DIR, //
-       @Meta(what = "directory", where = "", format = "absolute path, $HOME variable supported, / is always accepted as dir separator", info = "The directory where to store the library")
+       @Meta(what = "absolute path, $HOME variable supported, / is always accepted as dir separator", where = "", format = "Directory", info = "The directory where to store the library")
        LIBRARY_DIR, //
-       @Meta(what = "boolean", where = "", format = "'true' or 'false'", info = "Show debug information on errors")
+       @Meta(what = "boolean", where = "", format = "'true'|'false'", info = "Show debug information on errors")
        DEBUG_ERR, //
-       @Meta(what = "image format", where = "", format = "PNG, JPG, BMP...", info = "Image format to use for cover images")
+       @Meta(what = "image format", where = "", format = "'PNG'|JPG'|'BMP'", info = "Image format to use for cover images")
        IMAGE_FORMAT_COVER, //
-       @Meta(what = "image format", where = "", format = "PNG, JPG, BMP...", info = "Image format to use for content images")
+       @Meta(what = "image format", where = "", format = "'PNG'|JPG'|'BMP'", info = "Image format to use for content images")
        IMAGE_FORMAT_CONTENT, //
-       @Meta(what = "", where = "", format = "not used", info = "This key is only present to allow access to suffixes")
+       // This key is only present to allow access to suffixes, so no Meta
        LATEX_LANG, //
-       @Meta(what = "LaTeX output language", where = "LaTeX", format = "", info = "LaTeX full name for English")
+       @Meta(what = "LaTeX output language", where = "LaTeX", format = "String", info = "LaTeX full name for English")
        LATEX_LANG_EN, //
-       @Meta(what = "LaTeX output language", where = "LaTeX", format = "", info = "LaTeX full name for French")
+       @Meta(what = "LaTeX output language", where = "LaTeX", format = "String", info = "LaTeX full name for French")
        LATEX_LANG_FR, //
-       @Meta(what = "other 'by' prefixes before author name", where = "", format = "comma-separated list", info = "used to identify the author")
+       @Meta(what = "other 'by' prefixes before author name", where = "", format = "comma-separated list|String", info = "used to identify the author")
        BYS, //
-       @Meta(what = "Chapter identification languages", where = "", format = "comma-separated list", info = "used to identify a starting chapter in text mode")
+       @Meta(what = "Chapter identification languages", where = "", format = "comma-separated list|String", info = "used to identify a starting chapter in text mode")
        CHAPTER, //
-       @Meta(what = "Chapter identification string", where = "", format = "", info = "used to identify a starting chapter in text mode")
+       @Meta(what = "Chapter identification string", where = "String", format = "", info = "used to identify a starting chapter in text mode")
        CHAPTER_EN, //
-       @Meta(what = "Chapter identification string", where = "", format = "", info = "used to identify a starting chapter in text mode")
+       @Meta(what = "Chapter identification string", where = "String", format = "", info = "used to identify a starting chapter in text mode")
        CHAPTER_FR, //
-       @Meta(what = "Login information", where = "", format = "", info = "used to login on YiffStar to have access to all the stories (should not be necessary anymore)")
+       @Meta(what = "Login information", where = "", format = "String", info = "used to login on YiffStar to have access to all the stories (should not be necessary anymore)")
        LOGIN_YIFFSTAR_USER, //
-       @Meta(what = "Login information", where = "", format = "", info = "used to login on YiffStar to have access to all the stories (should not be necessary anymore)")
+       @Meta(what = "Login information", where = "", format = "Password", info = "used to login on YiffStar to have access to all the stories (should not be necessary anymore)")
        LOGIN_YIFFSTAR_PASS, //
+       @Meta(what = "Minimum time between version update checks in days, or -1 for 'no checks' -- default is 1 day", where = "VersionCheck", format = "int", info = "If the last update check was done at least that many days, check for updates at startup")
+       UPDATE_INTERVAL,
 }
index 3f8345f4f569e2c3a6c3abc2fb6deeea79774698..2fa3d32a054ffe846e5eac1b44ad898ec757bf38 100644 (file)
@@ -2,63 +2,63 @@
 #
 
 
-# (WHAT: language, FORMAT: language (example: en-GB) or nothing for default system language)
+# (WHAT: language (example: en-GB, fr-BE...) or nothing for default system language, FORMAT: Locale|'')
 # Force the language (can be overwritten again with the env variable $LANG)
 LANG = 
-# (WHAT: reader type, FORMAT: CLI or LOCAL)
-# Select the default reader to use to read stories (CLI = simple output to console, LOCAL = use local system file handler)
+# (WHAT: reader type (CLI = simple output to console, LOCAL = use local system file handler), FORMAT: 'CLI'|'LOCAL')
+# Select the default reader to use to read stories
 READER_TYPE = 
-# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
+# (WHAT: absolute path, $HOME variable supported, / is always accepted as dir separator, FORMAT: Directory)
 # The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)
 CACHE_DIR = 
-# (WHAT: delay in hours, FORMAT: integer | 0: no cache | -1: infinite time cache which is default)
+# (WHAT: delay in hours, or 0 for no cache, or -1 for infinite time (default), FORMAT: int)
 # The delay after which a cached resource that is thought to change ~often is considered too old and triggers a refresh
 CACHE_MAX_TIME_CHANGING = 24
-# (WHAT: delay in hours, FORMAT: integer | 0: no cache | -1: infinite time cache which is default)
+# (WHAT: delay in hours, or 0 for no cache, or -1 for infinite time (default), FORMAT: int)
 # The delay after which a cached resource that is thought to change rarely is considered too old and triggers a refresh
 CACHE_MAX_TIME_STABLE = 
-# (WHAT: string)
+# (WHAT: string, FORMAT: String)
 # The user-agent to use to download files
 USER_AGENT = Mozilla/5.0 (X11; Linux x86_64; rv:44.0) Gecko/20100101 Firefox/44.0 -- ELinks/0.9.3 (Linux 2.6.11 i686; 80x24)
-# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
+# (WHAT: absolute path, $HOME variable supported, / is always accepted as dir separator, FORMAT: Directory)
 # The directory where to get the default story covers
 DEFAULT_COVERS_DIR = $HOME/bin/epub/
-# (WHAT: directory, FORMAT: absolute path, $HOME variable supported, / is always accepted as dir separator)
+# (WHAT: absolute path, $HOME variable supported, / is always accepted as dir separator, FORMAT: Directory)
 # The directory where to store the library
 LIBRARY_DIR = $HOME/Books
-# (WHAT: boolean, FORMAT: 'true' or 'false')
+# (WHAT: boolean, FORMAT: 'true'|'false')
 # Show debug information on errors
 DEBUG_ERR = false
-# (WHAT: image format, FORMAT: PNG, JPG, BMP...)
+# (WHAT: image format, FORMAT: 'PNG'|JPG'|'BMP')
 # Image format to use for cover images
 IMAGE_FORMAT_COVER = png
-# (WHAT: image format, FORMAT: PNG, JPG, BMP...)
+# (WHAT: image format, FORMAT: 'PNG'|JPG'|'BMP')
 # Image format to use for content images
 IMAGE_FORMAT_CONTENT = png
-# (FORMAT: not used)
-# This key is only present to allow access to suffixes
-LATEX_LANG = 
-# (WHAT: LaTeX output language, WHERE: LaTeX)
+# (WHAT: LaTeX output language, WHERE: LaTeX, FORMAT: String)
 # LaTeX full name for English
 LATEX_LANG_EN = english
-# (WHAT: LaTeX output language, WHERE: LaTeX)
+# (WHAT: LaTeX output language, WHERE: LaTeX, FORMAT: String)
 # LaTeX full name for French
 LATEX_LANG_FR = french
-# (WHAT: other 'by' prefixes before author name, FORMAT: comma-separated list)
+# (WHAT: other 'by' prefixes before author name, FORMAT: comma-separated list|String)
 # used to identify the author
 BYS = by,par,de,©,(c)
-# (WHAT: Chapter identification languages, FORMAT: comma-separated list)
+# (WHAT: Chapter identification languages, FORMAT: comma-separated list|String)
 # used to identify a starting chapter in text mode
 CHAPTER = EN,FR
-# (WHAT: Chapter identification string)
+# (WHAT: Chapter identification string, WHERE: String)
 # used to identify a starting chapter in text mode
 CHAPTER_EN = Chapter
-# (WHAT: Chapter identification string)
+# (WHAT: Chapter identification string, WHERE: String)
 # used to identify a starting chapter in text mode
 CHAPTER_FR = Chapitre
-# (WHAT: Login information)
+# (WHAT: Login information, FORMAT: String)
 # used to login on YiffStar to have access to all the stories (should not be necessary anymore)
 LOGIN_YIFFSTAR_USER = 
-# (WHAT: Login information)
+# (WHAT: Login information, FORMAT: Password)
 # used to login on YiffStar to have access to all the stories (should not be necessary anymore)
 LOGIN_YIFFSTAR_PASS = 
+# (WHAT: Minimum time between version update checks in days, or -1 for 'no checks' -- default is 1 day, WHERE: VersionCheck, FORMAT: int)
+# If the last update check was done at least that many days, check for updates at startup
+UPDATE_INTERVAL = 
index 45fca8c10a41feb43e2fb9d13bf406241de4e88b..b031de84e372342710a0c420f4c22304b5a497d7 100644 (file)
@@ -4,14 +4,23 @@ import java.awt.Desktop;
 import java.awt.EventQueue;
 import java.io.File;
 import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
 
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.Library;
+import be.nikiroo.fanfix.VersionCheck;
 import be.nikiroo.fanfix.bundles.UiConfig;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
 import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.Version;
 
 class LocalReader extends BasicReader {
        private Library lib;
@@ -140,9 +149,62 @@ class LocalReader extends BasicReader {
 
        @Override
        public void start(String type) {
+               // TODO: improve presentation of update message
+               final VersionCheck updates = VersionCheck.check();
+               StringBuilder builder = new StringBuilder();
+
+               final JEditorPane updateMessage = new JEditorPane("text/html", "");
+               if (updates.isNewVersionAvailable()) {
+                       builder.append("A new version of the program is available at <span style='color: blue;'>https://github.com/nikiroo/fanfix/releases</span>");
+                       builder.append("<br>");
+                       builder.append("<br>");
+                       for (Version v : updates.getNewer()) {
+                               builder.append("\t<b>Version " + v + "</b>");
+                               builder.append("<br>");
+                               builder.append("<ul>");
+                               for (String item : updates.getChanges().get(v)) {
+                                       builder.append("<li>" + item + "</li>");
+                               }
+                               builder.append("</ul>");
+                       }
+
+                       // html content
+                       updateMessage.setText("<html><body>" //
+                                       + builder//
+                                       + "</body></html>");
+
+                       // handle link events
+                       updateMessage.addHyperlinkListener(new HyperlinkListener() {
+                               public void hyperlinkUpdate(HyperlinkEvent e) {
+                                       if (e.getEventType().equals(
+                                                       HyperlinkEvent.EventType.ACTIVATED))
+                                               try {
+                                                       Desktop.getDesktop().browse(e.getURL().toURI());
+                                               } catch (IOException ee) {
+                                                       Instance.syserr(ee);
+                                               } catch (URISyntaxException ee) {
+                                                       Instance.syserr(ee);
+                                               }
+                               }
+                       });
+                       updateMessage.setEditable(false);
+                       updateMessage.setBackground(new JLabel().getBackground());
+               }
+
                final String typeFinal = type;
                EventQueue.invokeLater(new Runnable() {
                        public void run() {
+                               if (updates.isNewVersionAvailable()) {
+                                       int rep = JOptionPane.showConfirmDialog(null,
+                                                       updateMessage, "Updates available",
+                                                       JOptionPane.OK_CANCEL_OPTION);
+                                       if (rep == JOptionPane.OK_OPTION) {
+                                               updates.ok();
+                                       } else {
+                                               updates.ignore();
+                                       }
+                               }
+
                                new LocalReaderFrame(LocalReader.this, typeFinal)
                                                .setVisible(true);
                        }