diff --git a/pom.xml b/pom.xml
index ffbab1e..c46daee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -336,7 +336,7 @@
${java.home}/bin/java
${project.basedir}
- -Djava.net.preferIPv6Addresses=true -Xmx512m --module-path target/dependency --add-modules ${javafx.modules} -cp target/classes:target/dependency/* ${app.main.class}
+ -Djava.net.preferIPv6Addresses=true -Xmx512m --sun-misc-unsafe-memory-access=allow --module-path target/dependency --add-modules ${javafx.modules} -cp target/classes:target/dependency/* ${app.main.class}
@@ -613,6 +613,7 @@
-Xmx512m
+ --sun-misc-unsafe-memory-access=allow
diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java
index 1fe4316..f3ec1e2 100644
--- a/src/main/java/MarkNote.java
+++ b/src/main/java/MarkNote.java
@@ -31,7 +31,15 @@
import ui.WelcomeTab;
import ui.CommitDialog;
import ui.AddRemoteDialog;
+import ui.UpdateDialog;
import java.util.List;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import services.UpdateChecker;
import utils.Debouncer;
import utils.DocumentService;
import utils.GitService;
@@ -413,6 +421,7 @@ public void start(Stage stage) {
if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) {
addNewDocument();
}
+ checkForUpdatesOnStartup();
});
splash.show();
} else {
@@ -424,6 +433,7 @@ public void start(Stage stage) {
if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) {
addNewDocument();
}
+ checkForUpdatesOnStartup();
}
}
@@ -545,7 +555,10 @@ private MenuBar createMenuBar() {
MenuItem aboutItem = new MenuItem(messages.getString("menu.help.about"));
aboutItem.setOnAction(e -> showAboutDialog());
- helpMenu.getItems().addAll(optionsItem, new SeparatorMenuItem(), aboutItem);
+ MenuItem checkUpdateItem = new MenuItem(messages.getString("menu.help.checkUpdate"));
+ checkUpdateItem.setOnAction(e -> checkForUpdatesManual());
+
+ helpMenu.getItems().addAll(optionsItem, checkUpdateItem, new SeparatorMenuItem(), aboutItem);
// == Menu Édition ==
Menu editMenu = new Menu(messages.getString("menu.edit"));
@@ -1510,6 +1523,75 @@ private void showAboutDialog() {
about.showAndWait();
}
+ private void checkForUpdatesOnStartup() {
+ if (!config.isAutoCheckUpdate()) return;
+ new Thread(() -> {
+ UpdateChecker.VersionInfo info =
+ UpdateChecker.checkForUpdate(messages.getString("app.version"));
+ if (info == null) return;
+ if (info.tagName().equals(config.getSkipVersion())) return;
+ Platform.runLater(() -> showUpdateDialog(info));
+ }, "update-check").start();
+ }
+
+ private void checkForUpdatesManual() {
+ new Thread(() -> {
+ UpdateChecker.VersionInfo info =
+ UpdateChecker.checkForUpdate(messages.getString("app.version"));
+ Platform.runLater(() -> {
+ if (info == null) {
+ Alert a = new Alert(Alert.AlertType.INFORMATION);
+ a.initOwner(primaryStage);
+ a.setTitle(messages.getString("update.uptodate.title"));
+ a.setContentText(messages.getString("update.uptodate.content")
+ .replace("{0}", messages.getString("app.version")));
+ a.showAndWait();
+ } else {
+ showUpdateDialog(info);
+ }
+ });
+ }, "update-check-manual").start();
+ }
+
+ private void showUpdateDialog(UpdateChecker.VersionInfo info) {
+ UpdateDialog dlg = new UpdateDialog(messages, primaryStage, info,
+ messages.getString("app.version"));
+ UpdateDialog.UpdateResult result = dlg.showAndGet();
+ if (result == UpdateDialog.UpdateResult.SKIP) {
+ config.setSkipVersion(info.tagName());
+ config.save();
+ } else if (result == UpdateDialog.UpdateResult.UPDATE) {
+ downloadAndInstall(info);
+ }
+ }
+
+ private void downloadAndInstall(UpdateChecker.VersionInfo info) {
+ new Thread(() -> {
+ try {
+ String suffix = info.assetName()
+ .substring(info.assetName().lastIndexOf('.'));
+ Path dest = Files.createTempFile("marknote-update-", suffix);
+ HttpClient client = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.ALWAYS)
+ .build();
+ client.send(
+ HttpRequest.newBuilder(URI.create(info.downloadUrl()))
+ .header("Accept", "application/octet-stream")
+ .build(),
+ HttpResponse.BodyHandlers.ofFile(dest));
+ java.awt.Desktop.getDesktop().open(dest.toFile());
+ } catch (Exception e) {
+ Platform.runLater(() -> {
+ Alert err = new Alert(Alert.AlertType.ERROR);
+ err.initOwner(primaryStage);
+ err.setTitle(messages.getString("update.error.title"));
+ err.setContentText(e.getMessage());
+ err.showAndWait();
+ });
+ }
+ }, "update-download").start();
+ }
+
/**
* Ouvre un fichier CSS de thème dans un ThemeTab.
*/
diff --git a/src/main/java/config/AppConfig.java b/src/main/java/config/AppConfig.java
index 013d586..4c7a268 100644
--- a/src/main/java/config/AppConfig.java
+++ b/src/main/java/config/AppConfig.java
@@ -39,6 +39,9 @@ public class AppConfig {
private String gitUsername = "token";
private String gitToolbarMode = "standard";
+ private boolean autoCheckUpdate = true;
+ private String skipVersion = null;
+
public record PanelState(boolean visible, boolean docked, String zone) {}
/**
@@ -96,6 +99,11 @@ public void load() {
gitUsername = line.substring("gitUsername=".length()).trim();
} else if (line.startsWith("gitToolbarMode=")) {
gitToolbarMode = line.substring("gitToolbarMode=".length()).trim();
+ } else if (line.startsWith("autoCheckUpdate=")) {
+ autoCheckUpdate = Boolean.parseBoolean(line.substring("autoCheckUpdate=".length()).trim());
+ } else if (line.startsWith("skipVersion=")) {
+ String v = line.substring("skipVersion=".length()).trim();
+ skipVersion = v.isEmpty() ? null : v;
} else if (line.startsWith("panelState=")) {
String raw = line.substring("panelState=".length()).trim();
String[] parts = raw.split("\\|", -1);
@@ -138,6 +146,8 @@ public void save() {
lines.add("gitToken=" + gitToken);
lines.add("gitUsername=" + gitUsername);
lines.add("gitToolbarMode=" + gitToolbarMode);
+ lines.add("autoCheckUpdate=" + autoCheckUpdate);
+ lines.add("skipVersion=" + (skipVersion != null ? skipVersion : ""));
for (Map.Entry entry : panelStates.entrySet()) {
PanelState state = entry.getValue();
lines.add("panelState=" + entry.getKey() + "|" + state.visible() + "|" + state.docked() + "|" + state.zone());
@@ -311,6 +321,12 @@ public void setReattachDiagramOnTabClose(boolean reattachDiagramOnTabClose) {
public String getGitToolbarMode() { return gitToolbarMode; }
public void setGitToolbarMode(String mode) { this.gitToolbarMode = (mode != null && !mode.isBlank()) ? mode : "standard"; }
+ public boolean isAutoCheckUpdate() { return autoCheckUpdate; }
+ public void setAutoCheckUpdate(boolean autoCheckUpdate) { this.autoCheckUpdate = autoCheckUpdate; }
+
+ public String getSkipVersion() { return skipVersion; }
+ public void setSkipVersion(String version) { this.skipVersion = version; }
+
/**
* Supprime un fichier de la liste des récents.
*
diff --git a/src/main/java/services/UpdateChecker.java b/src/main/java/services/UpdateChecker.java
new file mode 100644
index 0000000..3a0dd51
--- /dev/null
+++ b/src/main/java/services/UpdateChecker.java
@@ -0,0 +1,88 @@
+package services;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+
+public class UpdateChecker {
+
+ private static final String API_URL =
+ "https://api.github.com/repos/mcgivrer/MarkNote/releases/latest";
+
+ public record VersionInfo(String tagName, String downloadUrl, String assetName) {}
+
+ public static VersionInfo checkForUpdate(String currentVersion) {
+ try {
+ HttpClient client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ HttpRequest req = HttpRequest.newBuilder(URI.create(API_URL))
+ .header("Accept", "application/vnd.github+json")
+ .header("X-GitHub-Api-Version", "2022-11-28")
+ .build();
+ HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
+ if (resp.statusCode() != 200) return null;
+
+ JSONObject json = (JSONObject) new JSONParser().parse(resp.body());
+ String tagName = (String) json.get("tag_name");
+ if (tagName == null || !isNewer(tagName, currentVersion)) return null;
+
+ JSONArray assets = (JSONArray) json.get("assets");
+ JSONObject asset = selectAsset(assets);
+ if (asset == null) return null;
+
+ return new VersionInfo(tagName,
+ (String) asset.get("browser_download_url"),
+ (String) asset.get("name"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static boolean isNewer(String remote, String current) {
+ int[] r = parseParts(remote.startsWith("v") ? remote.substring(1) : remote);
+ int[] c = parseParts(current.startsWith("v") ? current.substring(1) : current);
+ int len = Math.max(r.length, c.length);
+ for (int i = 0; i < len; i++) {
+ int rv = i < r.length ? r[i] : 0;
+ int cv = i < c.length ? c[i] : 0;
+ if (rv > cv) return true;
+ if (rv < cv) return false;
+ }
+ return false;
+ }
+
+ private static int[] parseParts(String version) {
+ String[] parts = version.split("\\.");
+ int[] ints = new int[parts.length];
+ for (int i = 0; i < parts.length; i++) {
+ try {
+ ints[i] = Integer.parseInt(parts[i]);
+ } catch (NumberFormatException e) {
+ ints[i] = 0;
+ }
+ }
+ return ints;
+ }
+
+ private static JSONObject selectAsset(JSONArray assets) {
+ if (assets == null) return null;
+ String os = System.getProperty("os.name").toLowerCase();
+ String ext = os.contains("win") ? ".exe" : os.contains("mac") ? ".dmg" : ".deb";
+ for (Object o : assets) {
+ JSONObject a = (JSONObject) o;
+ String name = (String) a.get("name");
+ String state = (String) a.get("state");
+ if ("uploaded".equals(state) && name != null && name.endsWith(ext)) {
+ return a;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/ui/UpdateDialog.java b/src/main/java/ui/UpdateDialog.java
new file mode 100644
index 0000000..e989de1
--- /dev/null
+++ b/src/main/java/ui/UpdateDialog.java
@@ -0,0 +1,69 @@
+package ui;
+
+import java.util.ResourceBundle;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+import services.UpdateChecker.VersionInfo;
+
+public class UpdateDialog {
+
+ public enum UpdateResult { UPDATE, DECLINE, SKIP }
+
+ private final Stage dialog;
+ private UpdateResult result = UpdateResult.DECLINE;
+
+ public UpdateDialog(ResourceBundle messages, Window owner, VersionInfo info, String currentVersion) {
+ dialog = new Stage();
+ dialog.initModality(Modality.APPLICATION_MODAL);
+ dialog.initOwner(owner);
+ dialog.setTitle(messages.getString("update.available.title"));
+ dialog.setResizable(false);
+
+ Label headerLabel = new Label(
+ messages.getString("update.available.header").replace("{0}", info.tagName()));
+ headerLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
+
+ Label bodyLabel = new Label(
+ messages.getString("update.available.content").replace("{0}", currentVersion));
+
+ CheckBox skipBox = new CheckBox(messages.getString("update.skip"));
+
+ Button downloadBtn = new Button(messages.getString("update.download"));
+ downloadBtn.setDefaultButton(true);
+ downloadBtn.setOnAction(e -> {
+ result = UpdateResult.UPDATE;
+ dialog.close();
+ });
+
+ Button declineBtn = new Button(messages.getString("update.decline"));
+ declineBtn.setOnAction(e -> {
+ result = skipBox.isSelected() ? UpdateResult.SKIP : UpdateResult.DECLINE;
+ dialog.close();
+ });
+
+ HBox buttons = new HBox(10, downloadBtn, declineBtn);
+ buttons.setAlignment(Pos.CENTER_RIGHT);
+
+ VBox root = new VBox(12, headerLabel, bodyLabel, skipBox, buttons);
+ root.setPadding(new Insets(20));
+ root.setPrefWidth(420);
+
+ dialog.setScene(new Scene(root));
+ }
+
+ public UpdateResult showAndGet() {
+ dialog.showAndWait();
+ return result;
+ }
+}
diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties
index 8659f20..09f8112 100644
--- a/src/main/resources/i18n/messages_de.properties
+++ b/src/main/resources/i18n/messages_de.properties
@@ -436,3 +436,16 @@ git.operation.fetch=Fetch\u2026
# Lesemodus
menu.view.readingMode=Lesemodus
reading.mode.exit=Lesemodus beenden
+
+# Update-Prüfung
+menu.help.checkUpdate=Nach Updates suchen...
+update.available.title=Neue Version verfügbar
+update.available.header=MarkNote {0} ist verfügbar
+update.available.content=Sie verwenden derzeit Version {0}.
+update.skip=Diese Version überspringen
+update.download=Herunterladen und installieren
+update.decline=Später
+update.uptodate.title=Aktuell
+update.uptodate.content=Sie verwenden die neueste Version von MarkNote ({0}).
+update.error.title=Update-Prüfung fehlgeschlagen
+update.error.content=Updates konnten nicht geprüft werden. Bitte versuchen Sie es erneut.
diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties
index d55647f..eb023c7 100644
--- a/src/main/resources/i18n/messages_en.properties
+++ b/src/main/resources/i18n/messages_en.properties
@@ -438,3 +438,16 @@ git.operation.fetch=Fetching\u2026
# Reading mode
menu.view.readingMode=Enter Reading Mode
reading.mode.exit=Exit Reading Mode
+
+# Update check
+menu.help.checkUpdate=Check for New Version...
+update.available.title=New Version Available
+update.available.header=MarkNote {0} is available
+update.available.content=You are currently running version {0}.
+update.skip=Skip this version
+update.download=Download & Install
+update.decline=Later
+update.uptodate.title=Up to Date
+update.uptodate.content=You are running the latest version of MarkNote ({0}).
+update.error.title=Update Check Failed
+update.error.content=Unable to check for updates. Please try again later.
diff --git a/src/main/resources/i18n/messages_es.properties b/src/main/resources/i18n/messages_es.properties
index 6d0cc59..bfb4a2d 100644
--- a/src/main/resources/i18n/messages_es.properties
+++ b/src/main/resources/i18n/messages_es.properties
@@ -437,3 +437,16 @@ git.operation.fetch=Recuperando\u2026
menu.view.readingMode=Modo lectura
reading.mode.exit=Salir del modo lectura
+# Comprobación de actualizaciones
+menu.help.checkUpdate=Buscar nueva versión...
+update.available.title=Nueva versión disponible
+update.available.header=MarkNote {0} está disponible
+update.available.content=Está utilizando actualmente la versión {0}.
+update.skip=Omitir esta versión
+update.download=Descargar e instalar
+update.decline=Más tarde
+update.uptodate.title=Actualizado
+update.uptodate.content=Está utilizando la última versión de MarkNote ({0}).
+update.error.title=Error al comprobar actualizaciones
+update.error.content=No se pudo comprobar si hay actualizaciones. Inténtelo de nuevo.
+
diff --git a/src/main/resources/i18n/messages_fr.properties b/src/main/resources/i18n/messages_fr.properties
index 4cf0adb..91c5dac 100644
--- a/src/main/resources/i18n/messages_fr.properties
+++ b/src/main/resources/i18n/messages_fr.properties
@@ -439,3 +439,16 @@ git.operation.fetch=R\u00e9cup\u00e9rer\u2026
# Mode lecture
menu.view.readingMode=Mode lecture
reading.mode.exit=Quitter le mode lecture
+
+# Vérification de mise à jour
+menu.help.checkUpdate=Vérifier les mises à jour...
+update.available.title=Nouvelle version disponible
+update.available.header=MarkNote {0} est disponible
+update.available.content=Vous utilisez actuellement la version {0}.
+update.skip=Ignorer cette version
+update.download=Télécharger et installer
+update.decline=Plus tard
+update.uptodate.title=À jour
+update.uptodate.content=Vous utilisez la dernière version de MarkNote ({0}).
+update.error.title=Échec de la vérification
+update.error.content=Impossible de vérifier les mises à jour. Veuillez réessayer.
diff --git a/src/main/resources/i18n/messages_it.properties b/src/main/resources/i18n/messages_it.properties
index 26ba6dd..4525866 100644
--- a/src/main/resources/i18n/messages_it.properties
+++ b/src/main/resources/i18n/messages_it.properties
@@ -436,3 +436,16 @@ git.operation.fetch=Aggiornamento in corso\u2026
menu.view.readingMode=Modalit\u00e0 lettura
reading.mode.exit=Esci dalla modalit\u00e0 lettura
+# Controllo aggiornamenti
+menu.help.checkUpdate=Verifica nuova versione...
+update.available.title=Nuova versione disponibile
+update.available.header=MarkNote {0} \u00e8 disponibile
+update.available.content=Stai attualmente usando la versione {0}.
+update.skip=Ignora questa versione
+update.download=Scarica e installa
+update.decline=Pi\u00f9 tardi
+update.uptodate.title=Aggiornato
+update.uptodate.content=Stai usando l'ultima versione di MarkNote ({0}).
+update.error.title=Verifica aggiornamenti fallita
+update.error.content=Impossibile verificare gli aggiornamenti. Riprova pi\u00f9 tardi.
+