# 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
import java.io.File;
import java.io.IOException;
+import java.util.Date;
import be.nikiroo.fanfix.bundles.Config;
import be.nikiroo.fanfix.bundles.ConfigBundle;
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;
/**
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 {
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
*
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);
+ "\nhttps://github.com/nikiroo/fanfix/"
+ "\n\tWritten by Nikiroo",
Version.getCurrentVersion()));
+ updates.ok(); // we consider it read
break;
case START:
UIUtils.setLookAndFeel();
--- /dev/null
+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);
+ }
+}
* @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,
}
#
-# (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 =
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;
@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);
}