diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 0a845929b5..7ad231457e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -237,6 +237,21 @@ public void setDisableAutoShowUpdateDialog(boolean disableAutoShowUpdateDialog) this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog); } + @SerializedName("autoDownloadUpdate") + private final BooleanProperty autoDownloadUpdate = new SimpleBooleanProperty(false); + + public BooleanProperty autoDownloadUpdateProperty() { + return autoDownloadUpdate; + } + + public boolean isAutoDownloadUpdate() { + return autoDownloadUpdate.get(); + } + + public void setAutoDownloadUpdate(boolean autoDownloadUpdate) { + this.autoDownloadUpdate.set(autoDownloadUpdate); + } + @SerializedName("disableAprilFools") private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index ceff5dc8fb..44d8b3f995 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -20,6 +20,7 @@ import com.jfoenix.controls.JFXButton; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.StringProperty; import javafx.css.PseudoClass; @@ -86,7 +87,6 @@ public SettingsPage() { { ObjectProperty updateChannel; { - JFXButton updateButton = FXUtils.newToggleButton4(SVG.UPDATE, 20); updateButton.setOnAction(e -> onUpdate()); updateButton.setPadding(Insets.EMPTY); @@ -139,17 +139,13 @@ protected int getTrailingTextIndex() { updatePaneList.getContent().add(updatePane); } + BooleanProperty preview; { LineToggleButton previewPane = new LineToggleButton(); previewPane.setTitle(i18n("update.preview")); previewPane.setSubtitle(i18n("update.preview.subtitle")); previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty()); - - InvalidationListener checkUpdateListener = e -> { - UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected()); - }; - updateChannel.addListener(checkUpdateListener); - previewPane.selectedProperty().addListener(checkUpdateListener); + preview = previewPane.selectedProperty(); updatePaneList.getContent().add(previewPane); } @@ -162,6 +158,26 @@ protected int getTrailingTextIndex() { updatePaneList.getContent().add(disableAutoShowUpdateDialogPane); } + BooleanProperty autoDownloadUpdate; + { + LineToggleButton autoDownloadUpdatePane = new LineToggleButton(); + autoDownloadUpdatePane.setTitle(i18n("update.auto_download")); + autoDownloadUpdatePane.setSubtitle(i18n("update.auto_download.subtitle")); + autoDownloadUpdatePane.selectedProperty().bindBidirectional(config().autoDownloadUpdateProperty()); + autoDownloadUpdate = autoDownloadUpdatePane.selectedProperty(); + + updatePaneList.getContent().add(autoDownloadUpdatePane); + } + + { + InvalidationListener checkUpdateListener = e -> { + UpdateChecker.requestCheckUpdate(updateChannel.get(), preview.get(), autoDownloadUpdate.get()); + }; + updateChannel.addListener(checkUpdateListener); + preview.addListener(checkUpdateListener); + autoDownloadUpdate.addListener(checkUpdateListener); + } + rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("update")), updatePaneList); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java index b24ce19491..ab286a8142 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java @@ -22,15 +22,24 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + public record RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) { + public static final Map downloadCache = new HashMap<>(); + public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException { try { JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(url), JsonObject.class); @@ -53,6 +62,25 @@ public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String return "[" + version + " from " + url + "]"; } + public void tryDownload() { + Path downloaded = downloadCache.get(this); + if (downloaded != null && FileUtils.verifyHash(downloaded, integrityCheck().algorithm(), integrityCheck().checksum())) return; + + try { + downloaded = Files.createTempFile("hmcl-update-", ".jar"); + } catch (IOException e) { + LOG.warning("Failed to create temp file", e); + return; + } + + var executor = new HMCLDownloadTask(this, downloaded).executor(); + if (executor.test()) { + downloadCache.put(this, downloaded); + } else { + LOG.warning("Failed to download update for " + this, executor.getException()); + } + } + public enum Type { JAR } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java index 998a3da7d2..b040ac9ba5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java @@ -22,7 +22,7 @@ public enum UpdateChannel { STABLE("stable"), DEVELOPMENT("dev"), - NIGHTLY("nightly"); + NIGHTLY("dev"); public final String channelName; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java index df15a3fac1..a86f1624f2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java @@ -56,7 +56,7 @@ private UpdateChecker() { private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false); public static void init() { - requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate()); + requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate(), config().isAutoDownloadUpdate()); } public static RemoteVersion getLatestVersion() { @@ -101,7 +101,7 @@ private static boolean isDevelopmentVersion(String version) { version.contains("SNAPSHOT"); // eg. 3.5.SNAPSHOT } - public static void requestCheckUpdate(UpdateChannel channel, boolean preview) { + public static void requestCheckUpdate(UpdateChannel channel, boolean preview, boolean download) { Platform.runLater(() -> { if (isCheckingUpdate()) return; @@ -112,6 +112,7 @@ public static void requestCheckUpdate(UpdateChannel channel, boolean preview) { try { result = checkUpdate(channel, preview); LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result); + if (download) result.tryDownload(); } catch (Throwable e) { LOG.warning("Failed to check for update", e); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index 95c8f1b772..3a682470e7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.SwingUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -106,22 +107,26 @@ public static void updateFrom(RemoteVersion version) { } Controllers.dialog(new UpgradeDialog(version, () -> { - Path downloaded; - try { - downloaded = Files.createTempFile("hmcl-update-", ".jar"); - } catch (IOException e) { - LOG.warning("Failed to create temp file", e); - return; - } - - Task task = new HMCLDownloadTask(version, downloaded); + Path downloaded = RemoteVersion.downloadCache.get(version); + TaskExecutor executor; + if (downloaded != null && FileUtils.verifyHash(downloaded, version.integrityCheck().algorithm(), version.integrityCheck().checksum())) { + executor = Task.completed(null).executor(); + } else { + try { + downloaded = Files.createTempFile("hmcl-update-", ".jar"); + } catch (IOException e) { + LOG.warning("Failed to create temp file", e); + return; + } - TaskExecutor executor = task.executor(); - Controllers.taskDialog(executor, i18n("message.downloading"), TaskCancellationAction.NORMAL); + Task task = new HMCLDownloadTask(version, downloaded); + executor = task.executor(); + Controllers.taskDialog(executor, i18n("message.downloading"), TaskCancellationAction.NORMAL); + } + final Path finalDownloaded = downloaded; thread(() -> { - boolean success = executor.test(); - - if (success) { + if (executor.test()) { + RemoteVersion.downloadCache.put(version, finalDownloaded); try { if (!IntegrityChecker.isSelfVerified() && !IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK) { throw new IOException("Current JAR is not verified"); @@ -150,7 +155,7 @@ public static void updateFrom(RemoteVersion version) { // Ignore } - requestUpdate(downloaded, getCurrentLocation()); + requestUpdate(finalDownloaded, getCurrentLocation()); EntryPoint.exit(0); } catch (IOException e) { LOG.warning("Failed to update to " + version, e); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 731eaee788..a8c697123f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1630,6 +1630,8 @@ unofficial.hint=You are using an unofficial build of HMCL. We cannot guarantee i update=Update update.accept=Update +update.auto_download=Auto download update +update.auto_download.subtitle=Automatically download launcher updates and notify when ready to install. update.changelog=Changelog update.channel.dev=Beta update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\ diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 90f2157ece..18b798efd3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1420,6 +1420,8 @@ unofficial.hint=你正在使用第三方提供的 HMCL。我們無法保證其 update=啟動器更新 update.accept=更新 +update.auto_download=自動下載更新 +update.auto_download.subtitle=自動在背景下載更新,下載完成後顯示通知 update.changelog=更新日誌 update.channel.dev=開發版 update.channel.dev.hint=你正在使用 HMCL 開發版。開發版包含一些未在穩定版中包含的測試性功能,僅用於體驗新功能。開發版功能未受充分驗證,使用起來可能不穩定!\n\ diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 976a48046c..bdfafc21e5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1425,6 +1425,8 @@ unofficial.hint=你正在使用非官方构建的 HMCL。我们无法保证其 update=启动器更新 update.accept=更新 +update.auto_download=自动下载更新 +update.auto_download.subtitle=自动在后台下载更新,下载完成后显示通知 update.changelog=更新日志 update.channel.dev=开发版 update.channel.dev.hint=你正在使用 HMCL 开发版。开发版包含一些未在稳定版中包含的测试性功能,仅用于体验新功能。开发版功能未受充分验证,使用起来可能不稳定!下载稳定版\n\ diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java index 48fbcdc7c7..54584cebfa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java @@ -103,15 +103,7 @@ protected Path getFile(String algorithm, String hash) { protected boolean fileExists(String algorithm, String hash) { if (hash == null) return false; Path file = getFile(algorithm, hash); - if (Files.exists(file)) { - try { - return DigestUtils.digestToString(algorithm, file).equalsIgnoreCase(hash); - } catch (IOException e) { - return false; - } - } else { - return false; - } + return FileUtils.verifyHash(file, algorithm, hash); } public void tryCacheFile(Path path, String algorithm, String hash) throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index bce609927d..dd3070f39e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -20,6 +20,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.glavo.chardet.DetectedCharset; import org.glavo.chardet.UniversalDetector; +import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.function.ExceptionalConsumer; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -559,4 +560,17 @@ public static EnumSet parsePosixFilePermission(int unixMode return permissions; } + + public static boolean verifyHash(Path file, String algorithm, String hash) { + if (Files.exists(file)) { + try { + return DigestUtils.digestToString(algorithm, file).equalsIgnoreCase(hash); + } catch (IOException e) { + LOG.warning("Failed to verify hash for file " + file, e); + return false; + } + } else { + return false; + } + } }