Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@
<configuration>
<executable>${java.home}/bin/java</executable>
<workingDirectory>${project.basedir}</workingDirectory>
<commandlineArgs>-Djava.net.preferIPv6Addresses=true -Xmx512m --module-path target/dependency --add-modules ${javafx.modules} -cp target/classes:target/dependency/* ${app.main.class}</commandlineArgs>
<commandlineArgs>-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}</commandlineArgs>
</configuration>
</plugin>

Expand Down Expand Up @@ -613,6 +613,7 @@
<javaOptions>
<!--javaOption>-Djava.net.preferIPv6Addresses=true</javaOption-->
<javaOption>-Xmx512m</javaOption>
<javaOption>--sun-misc-unsafe-memory-access=allow</javaOption>
</javaOptions>

<!-- Output directory -->
Expand Down
84 changes: 83 additions & 1 deletion src/main/java/MarkNote.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -413,6 +421,7 @@ public void start(Stage stage) {
if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) {
addNewDocument();
}
checkForUpdatesOnStartup();
});
splash.show();
} else {
Expand All @@ -424,6 +433,7 @@ public void start(Stage stage) {
if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) {
addNewDocument();
}
checkForUpdatesOnStartup();
}
}

Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<String, PanelState> entry : panelStates.entrySet()) {
PanelState state = entry.getValue();
lines.add("panelState=" + entry.getKey() + "|" + state.visible() + "|" + state.docked() + "|" + state.zone());
Expand Down Expand Up @@ -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.
*
Expand Down
88 changes: 88 additions & 0 deletions src/main/java/services/UpdateChecker.java
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
69 changes: 69 additions & 0 deletions src/main/java/ui/UpdateDialog.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions src/main/resources/i18n/messages_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions src/main/resources/i18n/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions src/main/resources/i18n/messages_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Loading
Loading