From: David ‘Bombe’ Roden Date: Sun, 9 Jan 2011 20:52:44 +0000 (+0100) Subject: Merge branch 'show-versions' into next X-Git-Tag: 0.3.7^2~8 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=a474ae28d3820854e9a8d8ea5b9f3a44b06fa737;hp=47ccfd42e03cb8b6d982ccf60c3478b1d52e81ea Merge branch 'show-versions' into next --- diff --git a/pom.xml b/pom.xml index e1fe047..e9ff09c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ net.pterodactylus utils - 0.7.6 + 0.7.7 junit diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 493221b..78df9ef 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -45,6 +45,7 @@ import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.number.Numbers; +import net.pterodactylus.util.version.Version; import freenet.keys.FreenetURI; /** @@ -52,7 +53,7 @@ import freenet.keys.FreenetURI; * * @author David ‘Bombe’ Roden */ -public class Core implements IdentityListener { +public class Core implements IdentityListener, UpdateListener { /** * Enumeration for the possible states of a {@link Sone}. @@ -98,6 +99,9 @@ public class Core implements IdentityListener { /** The Sone downloader. */ private final SoneDownloader soneDownloader; + /** The update checker. */ + private final UpdateChecker updateChecker; + /** Whether the core has been stopped. */ private volatile boolean stopped; @@ -162,6 +166,7 @@ public class Core implements IdentityListener { this.freenetInterface = freenetInterface; this.identityManager = identityManager; this.soneDownloader = new SoneDownloader(this, freenetInterface); + this.updateChecker = new UpdateChecker(freenetInterface); } // @@ -233,6 +238,15 @@ public class Core implements IdentityListener { } /** + * Returns the update checker. + * + * @return The update checker + */ + public UpdateChecker getUpdateChecker() { + return updateChecker; + } + + /** * Returns the status of the given Sone. * * @param sone @@ -1400,6 +1414,8 @@ public class Core implements IdentityListener { */ public void start() { loadConfiguration(); + updateChecker.addUpdateListener(this); + updateChecker.start(); } /** @@ -1411,6 +1427,8 @@ public class Core implements IdentityListener { soneInserter.stop(); } } + updateChecker.stop(); + updateChecker.removeUpdateListener(this); soneDownloader.stop(); saveConfiguration(); stopped = true; @@ -1622,4 +1640,16 @@ public class Core implements IdentityListener { /* TODO */ } + // + // INTERFACE UpdateListener + // + + /** + * {@inheritDoc} + */ + @Override + public void updateFound(Version version, long releaseTime) { + coreListenerManager.fireUpdateFound(version, releaseTime); + } + } diff --git a/src/main/java/net/pterodactylus/sone/core/CoreListener.java b/src/main/java/net/pterodactylus/sone/core/CoreListener.java index 5d5e715..5fbb333 100644 --- a/src/main/java/net/pterodactylus/sone/core/CoreListener.java +++ b/src/main/java/net/pterodactylus/sone/core/CoreListener.java @@ -22,6 +22,7 @@ import java.util.EventListener; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.util.version.Version; /** * Listener interface for objects that want to be notified on certain @@ -127,4 +128,14 @@ public interface CoreListener extends EventListener { */ public void soneUnlocked(Sone sone); + /** + * Notifies a listener that a new version has been found. + * + * @param version + * The version that was found + * @param releaseTime + * The release time of the new version + */ + public void updateFound(Version version, long releaseTime); + } diff --git a/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java b/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java index 464342c..4fc9532 100644 --- a/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java +++ b/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java @@ -21,6 +21,7 @@ import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.util.event.AbstractListenerManager; +import net.pterodactylus.util.version.Version; /** * Manager for {@link CoreListener}s. @@ -197,4 +198,19 @@ public class CoreListenerManager extends AbstractListenerManager soneUskCallbacks = new HashMap(); + /** The not-Sone-related USK callbacks. */ + private final Map uriUskCallbacks = Collections.synchronizedMap(new HashMap()); + /** * Creates a new Freenet interface. * @@ -197,4 +201,83 @@ public class FreenetInterface { } } + /** + * Registers an arbitrary URI and calls the given callback if a new edition + * is found. + * + * @param uri + * The URI to watch + * @param callback + * The callback to call + */ + public void registerUsk(FreenetURI uri, final Callback callback) { + USKCallback uskCallback = new USKCallback() { + + @Override + public void onFoundEdition(long edition, USK key, ObjectContainer objectContainer, ClientContext clientContext, boolean metadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) { + callback.editionFound(key.getURI(), edition, newKnownGood, newSlotToo); + } + + @Override + public short getPollingPriorityNormal() { + return RequestStarter.PREFETCH_PRIORITY_CLASS; + } + + @Override + public short getPollingPriorityProgress() { + return RequestStarter.INTERACTIVE_PRIORITY_CLASS; + } + + }; + try { + node.clientCore.uskManager.subscribe(USK.create(uri), uskCallback, true, (HighLevelSimpleClientImpl) client); + uriUskCallbacks.put(uri, uskCallback); + } catch (MalformedURLException mue1) { + logger.log(Level.WARNING, "Could not subscribe to USK: " + uri, uri); + } + } + + /** + * Unregisters the USK watcher for the given URI. + * + * @param uri + * The URI to unregister the USK watcher for + */ + public void unregisterUsk(FreenetURI uri) { + USKCallback uskCallback = uriUskCallbacks.remove(uri); + if (uskCallback == null) { + logger.log(Level.INFO, "Could not unregister unknown USK: " + uri); + return; + } + try { + node.clientCore.uskManager.unsubscribe(USK.create(uri), uskCallback); + } catch (MalformedURLException mue1) { + logger.log(Level.INFO, "Could not unregister invalid USK: " + uri); + } + } + + /** + * Callback for USK watcher events. + * + * @author David ‘Bombe’ Roden + */ + public static interface Callback { + + /** + * Notifies a listener that a new edition was found for a URI. + * + * @param uri + * The URI that a new edition was found for + * @param edition + * The found edition + * @param newKnownGood + * Whether the found edition was actually fetched + * @param newSlot + * Whether the found edition is higher than all previously + * found editions + */ + public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot); + + } + } diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java new file mode 100644 index 0000000..0839cb9 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/UpdateChecker.java @@ -0,0 +1,233 @@ +/* + * Sone - UpdateChecker.java - Copyright © 2011 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.util.Date; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.main.SonePlugin; +import net.pterodactylus.util.collection.Pair; +import net.pterodactylus.util.io.Closer; +import net.pterodactylus.util.logging.Logging; +import net.pterodactylus.util.version.Version; +import freenet.client.FetchResult; +import freenet.keys.FreenetURI; +import freenet.support.api.Bucket; + +/** + * Watches the official Sone homepage for new releases. + * + * @author David ‘Bombe’ Roden + */ +public class UpdateChecker { + + /** The logger. */ + private static final Logger logger = Logging.getLogger(UpdateChecker.class); + + /** The key of the Sone homepage. */ + private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/"; + + /** The current latest known edition. */ + private static final int LATEST_EDITION = 23; + + /** The Freenet interface. */ + private final FreenetInterface freenetInterface; + + /** The update listener manager. */ + private final UpdateListenerManager updateListenerManager = new UpdateListenerManager(); + + /** The current URI of the homepage. */ + private FreenetURI currentUri; + + /** The current latest known version. */ + private Version currentLatestVersion = SonePlugin.VERSION; + + /** The release date of the latest version. */ + private long latestVersionDate; + + /** + * Creates a new update checker. + * + * @param freenetInterface + * The freenet interface to use + */ + public UpdateChecker(FreenetInterface freenetInterface) { + this.freenetInterface = freenetInterface; + } + + // + // EVENT LISTENER MANAGEMENT + // + + /** + * Adds the given listener to the list of registered listeners. + * + * @param updateListener + * The listener to add + */ + public void addUpdateListener(UpdateListener updateListener) { + updateListenerManager.addListener(updateListener); + } + + /** + * Removes the given listener from the list of registered listeners. + * + * @param updateListener + * The listener to remove + */ + public void removeUpdateListener(UpdateListener updateListener) { + updateListenerManager.removeListener(updateListener); + } + + // + // ACCESSORS + // + + /** + * Returns whether a version that is later than the currently running + * version has been found. + * + * @return {@code true} if a new version was found + */ + public boolean hasLatestVersion() { + return currentLatestVersion.compareTo(SonePlugin.VERSION) > 0; + } + + /** + * Returns the latest version. If no new latest version has been found, the + * current version is returned. + * + * @return The latest known version + */ + public Version getLatestVersion() { + return currentLatestVersion; + } + + /** + * Returns the release time of the latest version. If no new latest version + * has been found, the returned value is undefined. + * + * @return The release time of the latest version, if a new version was + * found + */ + public long getLatestVersionDate() { + return latestVersionDate; + } + + // + // ACTIONS + // + + /** + * Starts the update checker. + */ + public void start() { + try { + currentUri = new FreenetURI(SONE_HOMEPAGE + LATEST_EDITION); + } catch (MalformedURLException mue1) { + /* this can not really happen unless I screw up. */ + logger.log(Level.SEVERE, "Sone Homepage URI invalid!", mue1); + } + freenetInterface.registerUsk(currentUri, new FreenetInterface.Callback() { + + @Override + @SuppressWarnings("synthetic-access") + public void editionFound(FreenetURI uri, long edition, boolean newKnownGood, boolean newSlot) { + logger.log(Level.FINEST, "Found update for %s: %d, %s, %s", new Object[] { uri, edition, newKnownGood, newSlot }); + if (newKnownGood || newSlot) { + Pair uriResult = freenetInterface.fetchUri(uri.setMetaString(new String[] { "sone.properties" })); + if (uriResult == null) { + logger.log(Level.WARNING, "Could not fetch properties of latest homepage: %s", uri); + return; + } + Bucket resultBucket = uriResult.getRight().asBucket(); + try { + parseProperties(resultBucket.getInputStream()); + } catch (IOException ioe1) { + logger.log(Level.WARNING, "Could not parse sone.properties of " + uri, ioe1); + } finally { + resultBucket.free(); + } + } + } + }); + } + + /** + * Stops the update checker. + */ + public void stop() { + freenetInterface.unregisterUsk(currentUri); + } + + // + // PRIVATE ACTIONS + // + + /** + * Parses the properties of the latest version and fires events, if + * necessary. + * + * @see UpdateListener#updateFound(Version, long) + * @see UpdateListenerManager#fireUpdateFound(Version, long) + * @param propertiesInputStream + * The input stream to parse + * @throws IOException + * if an I/O error occured + */ + private void parseProperties(InputStream propertiesInputStream) throws IOException { + Properties properties = new Properties(); + InputStreamReader inputStreamReader = null; + try { + inputStreamReader = new InputStreamReader(propertiesInputStream, "UTF-8"); + properties.load(inputStreamReader); + } finally { + Closer.close(inputStreamReader); + } + String versionString = properties.getProperty("CurrentVersion/Version"); + String releaseTimeString = properties.getProperty("CurrentVersion/ReleaseTime"); + if ((versionString == null) || (releaseTimeString == null)) { + logger.log(Level.INFO, "Invalid data parsed from properties."); + return; + } + Version version = Version.parse(versionString); + long releaseTime = 0; + try { + releaseTime = Long.parseLong(releaseTimeString); + } catch (NumberFormatException nfe1) { + /* ignore. */ + } + if ((version == null) || (releaseTime == 0)) { + logger.log(Level.INFO, "Could not parse data from properties."); + return; + } + if (version.compareTo(currentLatestVersion) > 0) { + currentLatestVersion = version; + latestVersionDate = releaseTime; + logger.log(Level.INFO, "Found new version: %s (%tc)", new Object[] { version, new Date(releaseTime) }); + updateListenerManager.fireUpdateFound(version, releaseTime); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateListener.java b/src/main/java/net/pterodactylus/sone/core/UpdateListener.java new file mode 100644 index 0000000..1469afa --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/UpdateListener.java @@ -0,0 +1,42 @@ +/* + * Sone - UpdateListener.java - Copyright © 2011 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.core; + +import java.util.EventListener; + +import net.pterodactylus.util.version.Version; + +/** + * Listener interface for {@link UpdateChecker} events. + * + * @author David ‘Bombe’ Roden + */ +public interface UpdateListener extends EventListener { + + /** + * Notifies a listener that a newer version than the current version was + * found. + * + * @param version + * The version that was found + * @param releaseTime + * The release time of the version + */ + public void updateFound(Version version, long releaseTime); + +} diff --git a/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java b/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java new file mode 100644 index 0000000..3f3b21f --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/UpdateListenerManager.java @@ -0,0 +1,55 @@ +/* + * Sone - UpdateListenerManager.java - Copyright © 2011 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sone.core; + +import net.pterodactylus.util.event.AbstractListenerManager; +import net.pterodactylus.util.version.Version; + +/** + * Listener manager for {@link UpdateListener} events. + * + * @author David ‘Bombe’ Roden + */ +public class UpdateListenerManager extends AbstractListenerManager { + + /** + * Creates a new update listener manager. + */ + public UpdateListenerManager() { + super(null); + } + + // + // ACTIONS + // + + /** + * Notifies all listeners that a new version has been found. + * + * @param version + * The new version + * @param releaseTime + * The release time of the new version + */ + void fireUpdateFound(Version version, long releaseTime) { + for (UpdateListener updateListener : getListeners()) { + updateListener.updateFound(version, releaseTime); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index 68c9d90..219f989 100644 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@ -166,28 +166,28 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10 } } - /* create freenet interface. */ - FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode()); + boolean startupFailed = true; + try { + /* create freenet interface. */ + FreenetInterface freenetInterface = new FreenetInterface(pluginRespirator.getNode()); - /* create web of trust connector. */ - PluginConnector pluginConnector = new PluginConnector(pluginRespirator); - WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector); - identityManager = new IdentityManager(webOfTrustConnector); - identityManager.setContext("Sone"); + /* create web of trust connector. */ + PluginConnector pluginConnector = new PluginConnector(pluginRespirator); + WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector); + identityManager = new IdentityManager(webOfTrustConnector); + identityManager.setContext("Sone"); - /* create core. */ - core = new Core(oldConfiguration, freenetInterface, identityManager); + /* create core. */ + core = new Core(oldConfiguration, freenetInterface, identityManager); - /* create the web interface. */ - webInterface = new WebInterface(this); - core.addCoreListener(webInterface); + /* create the web interface. */ + webInterface = new WebInterface(this); + core.addCoreListener(webInterface); - /* create the identity manager. */ - identityManager.addIdentityListener(core); + /* create the identity manager. */ + identityManager.addIdentityListener(core); - /* start core! */ - boolean startupFailed = true; - try { + /* start core! */ core.start(); if ((newConfiguration != null) && (oldConfiguration != newConfiguration)) { logger.log(Level.INFO, "Setting configuration to file-based configuration."); diff --git a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java index 23ca247..3d4cb80 100644 --- a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collection; import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.main.SonePlugin; import net.pterodactylus.sone.web.page.Page; import net.pterodactylus.sone.web.page.TemplatePage; import net.pterodactylus.util.template.Template; @@ -188,6 +189,10 @@ public class SoneTemplatePage extends TemplatePage { super.processTemplate(request, template); template.set("currentSone", getCurrentSone(request.getToadletContext(), false)); template.set("request", request); + template.set("currentVersion", SonePlugin.VERSION); + template.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion()); + template.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion()); + template.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate()); } /** diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index d3ec1ee..6085ff8 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -88,6 +88,7 @@ import net.pterodactylus.util.template.TemplateFactory; import net.pterodactylus.util.template.TemplateProvider; import net.pterodactylus.util.template.XmlFilter; import net.pterodactylus.util.thread.Ticker; +import net.pterodactylus.util.version.Version; import freenet.clients.http.SessionManager; import freenet.clients.http.SessionManager.Session; import freenet.clients.http.ToadletContainer; @@ -141,6 +142,9 @@ public class WebInterface implements CoreListener { /** The “Sone locked” notification. */ private final ListNotification lockedSonesNotification; + /** The “new version” notification. */ + private final TemplateNotification newVersionNotification; + /** * Creates a new web interface. * @@ -189,6 +193,9 @@ public class WebInterface implements CoreListener { Template lockedSonesTemplate = templateFactory.createTemplate(createReader("/templates/notify/lockedSonesNotification.html")); lockedSonesNotification = new ListNotification("sones-locked-notification", "sones", lockedSonesTemplate); + + Template newVersionTemplate = templateFactory.createTemplate(createReader("/templates/notify/newVersionNotification.html")); + newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate); } // @@ -701,6 +708,16 @@ public class WebInterface implements CoreListener { } /** + * {@inheritDoc} + */ + @Override + public void updateFound(Version version, long releaseTime) { + newVersionNotification.set("version", version); + newVersionNotification.set("releaseTime", releaseTime); + notificationManager.addNotification(newVersionNotification); + } + + /** * Template provider implementation that uses * {@link WebInterface#createReader(String)} to load templates for * inclusion. diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index 533a01b..740cf45 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -175,6 +175,8 @@ WebInterface.SelectBox.Choose=Choose… WebInterface.SelectBox.Yes=Yes WebInterface.SelectBox.No=No WebInterface.ClickToShow.Replies=Click here to show hidden replies. +WebInterface.VersionInformation.CurrentVersion=Current Version: +WebInterface.VersionInformation.LatestVersion=Latest Version: Notification.ClickHereToRead=Click here to read the full text of the notification. Notification.FirstStart.Text=This seems to be the first time you start Sone. To start, create a new Sone from a web of trust identity and start following other Sones. @@ -191,3 +193,4 @@ Notification.SoneIsBeingRescued.Text=The following Sones are currently being res Notification.SoneRescued.Text=The following Sones have been rescued: Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones! Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked: +Notification.NewVersion.Text=A new version of the Sone plugin was found: Version {version}. diff --git a/src/main/resources/static/css/sone.css b/src/main/resources/static/css/sone.css index 89d9854..dfd6b9a 100644 --- a/src/main/resources/static/css/sone.css +++ b/src/main/resources/static/css/sone.css @@ -444,6 +444,10 @@ textarea { color: #888; } +#sone #tail #version-information { + margin-top: 1em; +} + #sone #add-sone textarea, #sone #create-sone textarea, #sone #load-sone textarea, #sone #edit-profile textarea { height: 1.5em; } diff --git a/src/main/resources/templates/include/tail.html b/src/main/resources/templates/include/tail.html index 180bcf2..dd2e25e 100644 --- a/src/main/resources/templates/include/tail.html +++ b/src/main/resources/templates/include/tail.html @@ -7,6 +7,13 @@ Flattr Sone + +
+
<%= WebInterface.VersionInformation.CurrentVersion|l10n|html> <% currentVersion|html>
+ <%if hasLatestVersion> +
<%= WebInterface.VersionInformation.LatestVersion|l10n|html> <% latestVersion|html>
+ <%/if> +
diff --git a/src/main/resources/templates/notify/newVersionNotification.html b/src/main/resources/templates/notify/newVersionNotification.html new file mode 100644 index 0000000..23a5855 --- /dev/null +++ b/src/main/resources/templates/notify/newVersionNotification.html @@ -0,0 +1 @@ +
<%= Notification.NewVersion.Text|l10n|html|replace needle="{version}" replacementKey=version>