From 1423329c267f8119550de85aa64dd3fb33975e4c Mon Sep 17 00:00:00 2001 From: lokins Date: Wed, 4 Feb 2026 23:15:35 +0800 Subject: [PATCH 01/28] update --- .../org/jackhuang/hmcl/ui/Controllers.java | 26 +++ .../ui/construct/TaskExecutorDialogPane.java | 49 ++++- .../hmcl/ui/construct/TaskListPane.java | 12 + .../hmcl/ui/download/DownloadPage.java | 53 +++-- .../ModpackInstallWizardProvider.java | 6 + .../ui/download/ModpackSelectionPage.java | 7 +- .../UpdateInstallerWizardProvider.java | 13 ++ .../VanillaInstallWizardProvider.java | 3 + .../hmcl/ui/main/JavaDownloadDialog.java | 7 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 11 + .../jackhuang/hmcl/ui/task/TaskCenter.java | 120 ++++++++++ .../hmcl/ui/task/TaskCenterPage.java | 207 ++++++++++++++++++ .../jackhuang/hmcl/ui/versions/Versions.java | 5 +- .../TaskExecutorDialogWizardDisplayer.java | 26 ++- .../resources/assets/lang/I18N.properties | 7 + 15 files changed, 525 insertions(+), 27 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 4993bb7019..5f2e54702a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -59,6 +59,7 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.terracotta.TerracottaPage; import org.jackhuang.hmcl.ui.versions.GameListPage; import org.jackhuang.hmcl.ui.versions.VersionPage; @@ -534,6 +535,31 @@ public static TaskExecutorDialogPane taskDialog(Task task, String title, Task return pane; } + public static TaskExecutorDialogPane downloadTaskDialog(Task task, String title, TaskCancellationAction onCancel, String detail) { + TaskExecutor executor = task.executor(); + TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel); + + pane.setBackgroundAction(() -> { + pane.fireEvent(new DialogCloseEvent()); + TaskCenter.getInstance().enqueue(executor, title, detail); + }); + + TaskCenter.getInstance().enqueue(executor, title, detail); + return pane; + } + + public static TaskExecutorDialogPane downloadTaskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel,String detail) { + TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel); + + pane.setBackgroundAction(() -> { + pane.fireEvent(new DialogCloseEvent()); + TaskCenter.getInstance().enqueue(executor, title,detail); + }); + + TaskCenter.getInstance().enqueue(executor, title, detail); + return pane; + } + public static void navigate(Node node) { decorator.navigate(node, ContainerAnimations.NAVIGATION, Motion.SHORT4, Motion.EASE); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index cf4bd06c7b..c268ce173d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -21,14 +21,17 @@ import javafx.application.Platform; import javafx.beans.property.StringProperty; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.i18n.I18n; +import org.jackhuang.hmcl.ui.SVG; import org.jetbrains.annotations.NotNull; import java.util.Optional; @@ -48,6 +51,13 @@ public class TaskExecutorDialogPane extends BorderPane { private final Label lblProgress; private final JFXButton btnCancel; private final TaskListPane taskListPane; + private final JFXButton btnBackground; + private Runnable onBackground; + private Runnable escAction; + + public void setEscAction(Runnable action) { + this.escAction = action; + } public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { this.getStyleClass().add("task-executor-dialog-layout"); @@ -59,13 +69,34 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { this.setCenter(center); center.setPadding(new Insets(16)); { + HBox titleBar = new HBox(); + titleBar.setAlignment(Pos.CENTER_LEFT); + titleBar.setSpacing(8); + lblTitle = new Label(); lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;"); + btnBackground = new JFXButton(); + btnBackground.setGraphic(SVG.DOWNLOAD.createIcon(16)); // TODO: 可替换为更合适的后台图标 + btnBackground.getStyleClass().add("toggle-icon4"); + FXUtils.installFastTooltip(btnBackground, i18n("task.move_to_background")); + btnBackground.setOnAction(e -> { + if (onBackground != null) { + onBackground.run(); + } + }); + btnBackground.setVisible(false); + btnBackground.setManaged(false); + + HBox spacer = new HBox(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + titleBar.getChildren().setAll(lblTitle, spacer, btnBackground); + taskListPane = new TaskListPane(); VBox.setVgrow(taskListPane, Priority.ALWAYS); - center.getChildren().setAll(lblTitle, taskListPane); + center.getChildren().setAll(titleBar, taskListPane); } BorderPane bottom = new BorderPane(); @@ -94,7 +125,12 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { Platform.runLater(() -> lblProgress.setText(message)); }); - onEscPressed(this, btnCancel::fire); + escAction = btnCancel::fire; + onEscPressed(this, () -> { + if (escAction != null) { + escAction.run(); + } + }); } public void setExecutor(TaskExecutor executor) { @@ -134,4 +170,13 @@ public void setCancel(TaskCancellationAction onCancel) { runInFX(() -> btnCancel.setDisable(onCancel == null)); } + + public void setBackgroundAction(Runnable action) { + this.onBackground = action; + btnBackground.setVisible(action != null); + btnBackground.setManaged(action != null); + } + public void refreshTaskList() { + taskListPane.refresh(); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 0f5fd1be46..a24aac56cb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -156,6 +156,9 @@ public void onReady(Task task) { public void onRunning(Task task) { if (!task.getSignificance().shouldShow() || task.getName() == null) return; + if (task.getName() == null) { + task.setName(i18n("task.unnamed")); + } if (task instanceof GameAssetDownloadTask) { task.setName(i18n("assets.download_all")); @@ -504,4 +507,13 @@ public void setThrowable(Throwable throwable) { progress.set(0.); } } + public void refresh() { + if (executor == null) return; + Platform.runLater(() -> { + stageNodes.clear(); + listView.getItems().clear(); + addStages(executor.getStages()); + updateProgressNodePadding(); + }); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index ff3f237f80..38a845928a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -134,24 +134,44 @@ public static void download(Profile profile, @Nullable String version, RemoteMod Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory(); + String detailPrefix; + switch (subdirectoryName) { + case "mods": + detailPrefix = "安装模组"; + break; + case "resourcepacks": + detailPrefix = "安装资源包"; + break; + case "shaderpacks": + detailPrefix = "安装光影"; + break; + case "saves": + detailPrefix = "安装世界"; + break; + default: + detailPrefix = "下载"; + break; + } + Controllers.prompt(i18n("archive.file.name"), (result, handler) -> { Path dest = runDirectory.resolve(subdirectoryName).resolve(result); - Controllers.taskDialog(Task.composeAsync(() -> { - var task = new FileDownloadTask(file.getFile().getUrl(), dest); - task.setName(file.getName()); - return task; - }).whenComplete(Schedulers.javafx(), exception -> { - if (exception != null) { - if (exception instanceof CancellationException) { - Controllers.showToast(i18n("message.cancelled")); - } else { - Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); - } - } else { - Controllers.showToast(i18n("install.success")); - } - }), i18n("message.downloading"), TaskCancellationAction.NORMAL); + Controllers.downloadTaskDialog(Task.composeAsync(() -> { + var task = new FileDownloadTask(file.getFile().getUrl(), dest); + task.setName(file.getName()); + return task; + }).whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + if (exception instanceof CancellationException) { + Controllers.showToast(i18n("message.cancelled")); + } else { + Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); + } + } else { + Controllers.showToast(i18n("install.success")); + } + }), i18n("message.downloading"), TaskCancellationAction.NORMAL, + detailPrefix + "-[" + file.getName() + "]"); handler.resolve(); }, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); @@ -301,6 +321,9 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); + settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]"); + settings.put("backgroundable", true); + return finishVersionDownloadingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index bd6a40012d..cd9c382168 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -142,6 +142,12 @@ public Object finish(SettingsMap settings) { } }); + Object name = settings.get("name"); + if (name != null) { + settings.put("task_detail", "安装整合包-[" + name + "]"); + } + settings.put("backgroundable", true); + return finishModpackInstallingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index 50090a1935..76ce27d22a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -146,7 +146,7 @@ private void onChooseRemoteFile() { Path modpack = Files.createTempFile("modpack", ".zip"); handler.resolve(); - Controllers.taskDialog( + Controllers.downloadTaskDialog( new FileDownloadTask(url, modpack) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { @@ -156,9 +156,10 @@ private void onChooseRemoteFile() { } else { handler.reject(e.getMessage()); } - }).executor(true), + }), i18n("message.downloading"), - TaskCancellationAction.NORMAL + TaskCancellationAction.NORMAL, + "安装整合包-[" + modpack.getFileName() + "]" ); } } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index 0b4f8dbf70..dbb2356591 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -75,6 +75,19 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> alertFailureMessage(exception, next)); + String detail = null; + for (Object value : settings.asStringMap().values()) { + if (value instanceof RemoteVersion remoteVersion) { + detail = "安装" + remoteVersion.getLibraryId() + "-[" + remoteVersion.getSelfVersion() + "]"; + break; + } + } + if (detail == null) { + detail = "安装" + libraryId; + } + settings.put("task_detail", detail); + settings.put("backgroundable", true); + // We remove library but not save it, // so if installation failed will not break down current version. Task ret = Task.supplyAsync(() -> version); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java index 03d26e8723..ef6853e06e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java @@ -70,6 +70,9 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); + settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]"); + settings.put("backgroundable", true); + return finishVersionDownloadingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java index 7d6dd5fc66..85b7e782c3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java @@ -159,16 +159,17 @@ protected void onAccept() { if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion)) Controllers.confirm(i18n("download.java.override"), null, () -> { - Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion)) + String detail = "安装Java-[" + javaVersion.majorVersion() + "]"; + Controllers.downloadTaskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion)) .thenComposeAsync(Schedulers.javafx(), realPath -> { if (realPath != null) { JavaManager.removeJava(realPath); } return downloadTask(javaVersion); - }), i18n("download.java"), TaskCancellationAction.NORMAL); + }), i18n("download.java"), TaskCancellationAction.NORMAL, detail); }, null); else - Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL); + Controllers.downloadTaskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL, "安装Java-[" + javaVersion.majorVersion() + "]"); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index ba086aa166..feb146adb6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -45,6 +45,7 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.ui.nbt.NBTFileType; +import org.jackhuang.hmcl.ui.task.TaskCenterPage; import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; import org.jackhuang.hmcl.ui.versions.GameListPopupMenu; import org.jackhuang.hmcl.ui.versions.Versions; @@ -191,6 +192,15 @@ protected Skin(RootPage control) { FXUtils.prepareOnMouseEnter(downloadItem, Controllers::prepareDownloadPage); } + AdvancedListItem taskManagerItem = new AdvancedListItem(); + taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //SVG待更换 + taskManagerItem.setActionButtonVisible(false); + taskManagerItem.setTitle(i18n("task.manage")); + taskManagerItem.setOnAction(e -> { + Controllers.navigate(new TaskCenterPage()); + }); + + // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS)); @@ -236,6 +246,7 @@ else if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOO .add(gameListItem) .add(gameItem) .add(downloadItem) + .add(taskManagerItem) .startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT)) .add(launcherSettingsItem) .add(terracottaItem) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java new file mode 100644 index 0000000000..b5b0fcbc7e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -0,0 +1,120 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * 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 org.jackhuang.hmcl.ui.task; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.task.TaskListener; + +public final class TaskCenter { + private static final TaskCenter INSTANCE = new TaskCenter(); + + public static TaskCenter getInstance() { + return INSTANCE; + } + + public static final class Entry { + private final TaskExecutor executor; + private final String title; + private final String detail; + + public Entry(TaskExecutor executor, String title, String detail) { + this.executor = executor; + this.title = title; + this.detail = detail; + } + + public TaskExecutor getExecutor() { + return executor; + } + + public String getTitle() { + return title; + } + + public String getDetail() { + return detail; + } + } + + private final ObservableList entries = FXCollections.observableArrayList(); + private final ObservableList completedEntries = FXCollections.observableArrayList(); + private final ObservableList failedEntries = FXCollections.observableArrayList(); + + private final Deque queue = new ArrayDeque<>(); + private final Map entryIndex = new HashMap<>(); + private Entry running; + + public ObservableList getEntries() { + return entries; + } + + public ObservableList getCompletedEntries() { + return completedEntries; + } + + public ObservableList getFailedEntries() { + return failedEntries; + } + + public synchronized void enqueue(TaskExecutor executor, String title, String detail) { + if (entryIndex.containsKey(executor)) { + return; + } + Entry entry = new Entry(executor, title, detail); + entryIndex.put(executor, entry); + entries.add(entry); + queue.add(entry); + tryStartNext(); + } + + private synchronized void tryStartNext() { + if (running != null) return; + Entry next = queue.poll(); + if (next == null) return; + + running = next; + next.getExecutor().addTaskListener(new TaskListener() { + @Override + public void onStop(boolean success, TaskExecutor executor) { + Platform.runLater(() -> { + if (running != null) { + entries.remove(running); + entryIndex.remove(executor); + + if (success) { + completedEntries.add(running); + } else { + failedEntries.add(running); + } + } + + running = null; + tryStartNext(); + }); + } + }); + next.getExecutor().start(); + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java new file mode 100644 index 0000000000..27913b3ae1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -0,0 +1,207 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * 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 org.jackhuang.hmcl.ui.task; + +import com.jfoenix.controls.JFXButton; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.ListChangeListener; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.*; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.TaskCancellationAction; + +import java.util.concurrent.CancellationException; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public final class TaskCenterPage extends DecoratorAnimatedPage implements DecoratorPage { + private final ReadOnlyObjectWrapper state = + new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("task.manage"))); + + private final TransitionPane transitionPane = new TransitionPane(); + private final TabHeader tabHeader; + + private final TabHeader.Tab runningTab = new TabHeader.Tab<>("taskRunningTab"); + private final TabHeader.Tab completedTab = new TabHeader.Tab<>("taskCompletedTab"); + private final TabHeader.Tab failedTab = new TabHeader.Tab<>("taskFailedTab"); + private final TabHeader.Tab settingsTab = new TabHeader.Tab<>("taskSettingsTab"); + + private final VBox runningContainer = new VBox(8); + private final VBox completedContainer = new VBox(8); + private final VBox failedContainer = new VBox(8); + + public TaskCenterPage() { + runningTab.setNodeSupplier(this::createRunningPane); + completedTab.setNodeSupplier(this::createCompletedPane); + failedTab.setNodeSupplier(this::createFailedPane); + settingsTab.setNodeSupplier(() -> createPlaceholderPane(i18n("task.settings"))); + + tabHeader = new TabHeader(transitionPane, runningTab, completedTab, failedTab, settingsTab); + tabHeader.select(runningTab); + + AdvancedListBox sideBar = new AdvancedListBox() + .startCategory(i18n("task.manage").toUpperCase()) + .addNavigationDrawerTab(tabHeader, runningTab, i18n("task.running"), SVG.ARROW_FORWARD) + .addNavigationDrawerTab(tabHeader, completedTab, i18n("task.completed"), SVG.CHECK) + .addNavigationDrawerTab(tabHeader, failedTab, i18n("task.failed"), SVG.CLOSE) + .addNavigationDrawerTab(tabHeader, settingsTab, i18n("task.settings"), SVG.SETTINGS); + + FXUtils.setLimitWidth(sideBar, 200); + setLeft(sideBar); + + BorderPane contentWrapper = new BorderPane(); + contentWrapper.getStyleClass().add("card-non-transparent"); + contentWrapper.setPadding(new Insets(12)); + contentWrapper.setCenter(transitionPane); + + StackPane centerPane = new StackPane(contentWrapper); + centerPane.setPadding(new Insets(12)); + setCenter(centerPane); + } + + private ScrollPane createRunningPane() { + ScrollPane scrollPane = new ScrollPane(runningContainer); + scrollPane.setFitToWidth(true); + runningContainer.setPadding(new Insets(12)); + + TaskCenter.getInstance().getEntries().addListener((ListChangeListener) change -> rebuildRunning()); + rebuildRunning(); + + return scrollPane; + } + + private ScrollPane createCompletedPane() { + ScrollPane scrollPane = new ScrollPane(completedContainer); + scrollPane.setFitToWidth(true); + completedContainer.setPadding(new Insets(12)); + + TaskCenter.getInstance().getCompletedEntries() + .addListener((ListChangeListener) change -> rebuildCompleted()); + rebuildCompleted(); + + return scrollPane; + } + + private ScrollPane createFailedPane() { + ScrollPane scrollPane = new ScrollPane(failedContainer); + scrollPane.setFitToWidth(true); + failedContainer.setPadding(new Insets(12)); + + TaskCenter.getInstance().getFailedEntries() + .addListener((ListChangeListener) change -> rebuildFailed()); + rebuildFailed(); + + return scrollPane; + } + + private void rebuildRunning() { + runningContainer.getChildren().clear(); + for (TaskCenter.Entry entry : TaskCenter.getInstance().getEntries()) { + runningContainer.getChildren().add(createRunningItem(entry)); + } + } + + private ScrollPane createPlaceholderPane(String text) { + VBox box = new VBox(); + box.setPadding(new Insets(12)); + box.getChildren().add(new Label(text)); + ScrollPane scrollPane = new ScrollPane(box); + scrollPane.setFitToWidth(true); + return scrollPane; + } + + private void rebuildCompleted() { + completedContainer.getChildren().clear(); + for (TaskCenter.Entry entry : TaskCenter.getInstance().getCompletedEntries()) { + Label item = new Label(entry.getDetail() != null ? entry.getDetail() : entry.getTitle()); + item.getStyleClass().add("md-list-cell"); + item.setPadding(new Insets(10, 12, 10, 12)); + completedContainer.getChildren().add(item); + } + } + + private void rebuildFailed() { + failedContainer.getChildren().clear(); + for (TaskCenter.Entry entry : TaskCenter.getInstance().getFailedEntries()) { + Label item = new Label(entry.getDetail() != null ? entry.getDetail() : entry.getTitle()); + item.getStyleClass().add("md-list-cell"); + item.setPadding(new Insets(10, 12, 10, 12)); + + item.setOnMouseClicked(e -> { + Throwable ex = entry.getExecutor().getException(); + if (ex instanceof CancellationException) { + Controllers.dialog("任务由用户取消", entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } else if (ex != null) { + Controllers.dialog(StringUtils.getStackTrace(ex), entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } else { + Controllers.dialog("任务失败(无异常信息)", entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } + }); + + failedContainer.getChildren().add(item); + } + } + + private Node createRunningItem(TaskCenter.Entry entry) { + HBox row = new HBox(12); + row.getStyleClass().add("md-list-cell"); + row.setPadding(new Insets(8, 12, 8, 12)); + row.setAlignment(Pos.CENTER_LEFT); + + String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); + Label label = new Label(text); + HBox.setHgrow(label, Priority.ALWAYS); + label.setMaxWidth(Double.MAX_VALUE); + + JFXButton cancelButton = new JFXButton(i18n("button.cancel")); + cancelButton.getStyleClass().add("dialog-cancel"); + + cancelButton.setOnAction(e -> { + entry.getExecutor().cancel(); + e.consume(); + }); + cancelButton.setOnMouseClicked(e -> e.consume()); + + row.getChildren().addAll(label, cancelButton); + + row.setOnMouseClicked(e -> { + TaskExecutorDialogPane pane = Controllers.taskDialog(entry.getExecutor(), entry.getTitle(), TaskCancellationAction.NORMAL); + pane.setEscAction(() -> pane.fireEvent(new DialogCloseEvent())); + pane.refreshTaskList(); + }); + + return row; + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state.getReadOnlyProperty(); + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index 82c3c7a8ac..f422d2a334 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -84,7 +84,7 @@ public static void downloadModpackImpl(Profile profile, String version, RemoteMo i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); return; } - Controllers.taskDialog( + Controllers.downloadTaskDialog( new FileDownloadTask(downloadURL, modpack) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { @@ -105,7 +105,8 @@ public static void downloadModpackImpl(Profile profile, String version, RemoteMo } }).executor(true), i18n("message.downloading"), - TaskCancellationAction.NORMAL + TaskCancellationAction.NORMAL, + "安装整合包-[" + modpack.getFileName() + "]" ); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index cf5f128ebd..5ec7ddf35e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -19,6 +19,7 @@ import javafx.beans.property.StringProperty; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskListener; import org.jackhuang.hmcl.ui.Controllers; @@ -58,6 +59,8 @@ else if (title instanceof String titleMessage) } runInFX(() -> { + boolean backgroundable = Boolean.TRUE.equals(settings.get("backgroundable")); + TaskExecutor executor = task.executor(new TaskListener() { @Override public void onStop(boolean success, TaskExecutor executor) { @@ -84,13 +87,32 @@ else if (settings.get("failure_message") instanceof String failureMessage) else if (!settings.containsKey("forbid_failure_message")) Controllers.dialog(appendix, i18n("wizard.failed"), MessageType.ERROR, () -> onEnd()); } - }); } }); + pane.setExecutor(executor); + + if (backgroundable) { + String detail = settings.get("task_detail").toString(); + if (detail == null) { + detail = pane.getTitle(); + } + + String finalDetail = detail; + pane.setBackgroundAction(() -> { + pane.fireEvent(new DialogCloseEvent()); + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), finalDetail); + }); + + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); + } + Controllers.dialog(pane); - executor.start(); + + if (!backgroundable) { + executor.start(); + } }); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 579fdfe5ea..bae6dab535 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1480,6 +1480,13 @@ sponsor.hmcl=Hello Minecraft! Launcher is a FOSS Minecraft launcher that allows system.architecture=Architecture system.operating_system=Operating System +task.completed=Completed +task.failed=Failed +task.manage=Task Manager +task.move_to_background=Move Task to Background +task.running=Running Tasks +task.settings=Task Settings +task.unnamed=Unnamed Task terracotta=Multiplayer terracotta.terracotta=Terracotta | Multiplayer terracotta.status=Lobby From 27eae281e971d9d9e7c1df886ece29a96d4facee Mon Sep 17 00:00:00 2001 From: lokins Date: Wed, 4 Feb 2026 23:18:04 +0800 Subject: [PATCH 02/28] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java | 2 +- .../main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index b5b0fcbc7e..f2b93668f0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -117,4 +117,4 @@ public void onStop(boolean success, TaskExecutor executor) { }); next.getExecutor().start(); } -} \ No newline at end of file +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 27913b3ae1..adb8617d0b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -204,4 +204,4 @@ private Node createRunningItem(TaskCenter.Entry entry) { public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); } -} \ No newline at end of file +} From af8fe52838b8e1d63618da83631a7bf1df9eb3e2 Mon Sep 17 00:00:00 2001 From: lokins Date: Wed, 4 Feb 2026 23:24:30 +0800 Subject: [PATCH 03/28] update --- .../jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java | 1 + .../java/org/jackhuang/hmcl/ui/construct/TaskListPane.java | 1 + .../java/org/jackhuang/hmcl/ui/download/DownloadPage.java | 4 ++-- .../main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index c268ce173d..d43d549d15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -176,6 +176,7 @@ public void setBackgroundAction(Runnable action) { btnBackground.setVisible(action != null); btnBackground.setManaged(action != null); } + public void refreshTaskList() { taskListPane.refresh(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index a24aac56cb..7f27055b21 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -507,6 +507,7 @@ public void setThrowable(Throwable throwable) { progress.set(0.); } } + public void refresh() { if (executor == null) return; Platform.runLater(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 38a845928a..6f2b8583a7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -158,9 +158,9 @@ public static void download(Profile profile, @Nullable String version, RemoteMod Controllers.downloadTaskDialog(Task.composeAsync(() -> { var task = new FileDownloadTask(file.getFile().getUrl(), dest); - task.setName(file.getName()); + task.setName(file.getName()); return task; - }).whenComplete(Schedulers.javafx(), exception -> { + }).whenComplete(Schedulers.javafx(), exception -> { if (exception != null) { if (exception instanceof CancellationException) { Controllers.showToast(i18n("message.cancelled")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index adb8617d0b..93888f1934 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; +import java.util.Locale; import java.util.concurrent.CancellationException; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -67,7 +68,7 @@ public TaskCenterPage() { tabHeader.select(runningTab); AdvancedListBox sideBar = new AdvancedListBox() - .startCategory(i18n("task.manage").toUpperCase()) + .startCategory(i18n("task.manage").toUpperCase(Locale.ROOT)) .addNavigationDrawerTab(tabHeader, runningTab, i18n("task.running"), SVG.ARROW_FORWARD) .addNavigationDrawerTab(tabHeader, completedTab, i18n("task.completed"), SVG.CHECK) .addNavigationDrawerTab(tabHeader, failedTab, i18n("task.failed"), SVG.CLOSE) From e40d564dbcdbb05be5855919c61b2e3ef76f783f Mon Sep 17 00:00:00 2001 From: lokins Date: Wed, 4 Feb 2026 23:29:00 +0800 Subject: [PATCH 04/28] update --- .../hmcl/ui/download/DownloadPage.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 6f2b8583a7..9cc36bf3bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -157,21 +157,22 @@ public static void download(Profile profile, @Nullable String version, RemoteMod Path dest = runDirectory.resolve(subdirectoryName).resolve(result); Controllers.downloadTaskDialog(Task.composeAsync(() -> { - var task = new FileDownloadTask(file.getFile().getUrl(), dest); + var task = new FileDownloadTask(file.getFile().getUrl(), dest); task.setName(file.getName()); - return task; + return task; }).whenComplete(Schedulers.javafx(), exception -> { - if (exception != null) { - if (exception instanceof CancellationException) { - Controllers.showToast(i18n("message.cancelled")); - } else { - Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); - } - } else { - Controllers.showToast(i18n("install.success")); - } - }), i18n("message.downloading"), TaskCancellationAction.NORMAL, - detailPrefix + "-[" + file.getName() + "]"); + if (exception != null) { + if (exception instanceof CancellationException) { + Controllers.showToast(i18n("message.cancelled")); + } else { + Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); + } + } + else { + Controllers.showToast(i18n("install.success")); + } + }), i18n("message.downloading"), TaskCancellationAction.NORMAL, + detailPrefix + "-[" + file.getName() + "]"); handler.resolve(); }, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); From cc25198bcc37705fa7ec903f01d7dfa1050bc6d6 Mon Sep 17 00:00:00 2001 From: lokins Date: Wed, 4 Feb 2026 23:32:16 +0800 Subject: [PATCH 05/28] update --- .../main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 9cc36bf3bd..fb24559521 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -172,7 +172,7 @@ public static void download(Profile profile, @Nullable String version, RemoteMod Controllers.showToast(i18n("install.success")); } }), i18n("message.downloading"), TaskCancellationAction.NORMAL, - detailPrefix + "-[" + file.getName() + "]"); + detailPrefix + "-[" + file.getName() + "]"); handler.resolve(); }, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); From 7f0bbaec78fc07c7a75e0b42070ba96ada89f464 Mon Sep 17 00:00:00 2001 From: lokins Date: Thu, 5 Feb 2026 00:19:52 +0800 Subject: [PATCH 06/28] update --- .../ui/wizard/TaskExecutorDialogWizardDisplayer.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index 5ec7ddf35e..9996a49fb2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -94,15 +94,12 @@ else if (!settings.containsKey("forbid_failure_message")) pane.setExecutor(executor); if (backgroundable) { - String detail = settings.get("task_detail").toString(); - if (detail == null) { - detail = pane.getTitle(); - } + Object detailObj = settings.get("task_detail"); + String detail = detailObj != null ? detailObj.toString() : pane.getTitle(); - String finalDetail = detail; pane.setBackgroundAction(() -> { pane.fireEvent(new DialogCloseEvent()); - TaskCenter.getInstance().enqueue(executor, pane.getTitle(), finalDetail); + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); }); TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); From 98b18a8e4ab3ec4f29359d9fa44a98e8bc3a1531 Mon Sep 17 00:00:00 2001 From: lokins Date: Thu, 5 Feb 2026 22:51:45 +0800 Subject: [PATCH 07/28] update --- .../ui/construct/TaskExecutorDialogPane.java | 37 ++++++++++++++++ .../jackhuang/hmcl/ui/task/TaskCenter.java | 21 +++++++++- .../hmcl/ui/task/TaskCenterPage.java | 42 ++++++++++++------- 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index d43d549d15..f787240848 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -27,6 +27,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; @@ -34,12 +35,14 @@ import org.jackhuang.hmcl.ui.SVG; import org.jetbrains.annotations.NotNull; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.Optional; import java.util.function.Consumer; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class TaskExecutorDialogPane extends BorderPane { private TaskExecutor executor; @@ -61,6 +64,7 @@ public void setEscAction(Runnable action) { public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { this.getStyleClass().add("task-executor-dialog-layout"); + cancelAction = null; FXUtils.setLimitWidth(this, 500); FXUtils.setLimitHeight(this, 300); @@ -114,6 +118,11 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { setCancel(cancel); btnCancel.setOnAction(e -> { + if (cancelAction != null) { + cancelAction.run(); + return; + } + Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel); if (onCancel.getCancellationAction() != null) { onCancel.getCancellationAction().accept(this); @@ -171,13 +180,41 @@ public void setCancel(TaskCancellationAction onCancel) { runInFX(() -> btnCancel.setDisable(onCancel == null)); } + private final AtomicBoolean background = new AtomicBoolean(false); + public void setBackgroundAction(Runnable action) { this.onBackground = action; + background.set(false); + btnBackground.setVisible(action != null); btnBackground.setManaged(action != null); + + if (action != null) { + btnBackground.setDisable(false); + btnBackground.setOnAction(e -> { + if (!background.compareAndSet(false, true)) { + return; + } + btnBackground.setDisable(true); + onBackground.run(); + }); + } else { + btnBackground.setDisable(false); + btnBackground.setOnAction(null); + } } public void refreshTaskList() { taskListPane.refresh(); } + + private Runnable cancelAction; + + public void setCancelAction(Runnable action) { + this.cancelAction = action; + } + + public void setCancelText(String text) { + btnCancel.setText(text); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index f2b93668f0..aa53894d7b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -64,6 +64,7 @@ public String getDetail() { private final Deque queue = new ArrayDeque<>(); private final Map entryIndex = new HashMap<>(); + private final Map started = new HashMap<>(); private Entry running; public ObservableList getEntries() { @@ -79,9 +80,15 @@ public ObservableList getFailedEntries() { } public synchronized void enqueue(TaskExecutor executor, String title, String detail) { + if (!Platform.isFxApplicationThread()) { + Platform.runLater(() -> enqueue(executor, title, detail)); + return; + } + if (entryIndex.containsKey(executor)) { return; } + Entry entry = new Entry(executor, title, detail); entryIndex.put(executor, entry); entries.add(entry); @@ -94,14 +101,23 @@ private synchronized void tryStartNext() { Entry next = queue.poll(); if (next == null) return; + TaskExecutor executor = next.getExecutor(); + if (Boolean.TRUE.equals(started.get(executor))) { + tryStartNext(); + return; + } + + started.put(executor, true); running = next; - next.getExecutor().addTaskListener(new TaskListener() { + + executor.addTaskListener(new TaskListener() { @Override public void onStop(boolean success, TaskExecutor executor) { Platform.runLater(() -> { if (running != null) { entries.remove(running); entryIndex.remove(executor); + started.remove(executor); if (success) { completedEntries.add(running); @@ -115,6 +131,7 @@ public void onStop(boolean success, TaskExecutor executor) { }); } }); - next.getExecutor().start(); + + executor.start(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 93888f1934..5ecd79bf8f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -26,6 +26,7 @@ import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; import javafx.scene.layout.*; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; @@ -141,32 +142,25 @@ private ScrollPane createPlaceholderPane(String text) { private void rebuildCompleted() { completedContainer.getChildren().clear(); for (TaskCenter.Entry entry : TaskCenter.getInstance().getCompletedEntries()) { - Label item = new Label(entry.getDetail() != null ? entry.getDetail() : entry.getTitle()); - item.getStyleClass().add("md-list-cell"); - item.setPadding(new Insets(10, 12, 10, 12)); - completedContainer.getChildren().add(item); + String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); + completedContainer.getChildren().add(createLineItem(text, null)); } } private void rebuildFailed() { failedContainer.getChildren().clear(); for (TaskCenter.Entry entry : TaskCenter.getInstance().getFailedEntries()) { - Label item = new Label(entry.getDetail() != null ? entry.getDetail() : entry.getTitle()); - item.getStyleClass().add("md-list-cell"); - item.setPadding(new Insets(10, 12, 10, 12)); - - item.setOnMouseClicked(e -> { + String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); + failedContainer.getChildren().add(createLineItem(text, () -> { Throwable ex = entry.getExecutor().getException(); if (ex instanceof CancellationException) { - Controllers.dialog("任务由用户取消", entry.getTitle(), MessageDialogPane.MessageType.ERROR); + Controllers.dialog("任务由用户取消", entry.getTitle(), MessageDialogPane.MessageType.ERROR);//TODO: i18n } else if (ex != null) { Controllers.dialog(StringUtils.getStackTrace(ex), entry.getTitle(), MessageDialogPane.MessageType.ERROR); } else { - Controllers.dialog("任务失败(无异常信息)", entry.getTitle(), MessageDialogPane.MessageType.ERROR); + Controllers.dialog("任务失败(无异常信息)", entry.getTitle(), MessageDialogPane.MessageType.ERROR);//TODO: i18n } - }); - - failedContainer.getChildren().add(item); + })); } } @@ -195,12 +189,32 @@ private Node createRunningItem(TaskCenter.Entry entry) { row.setOnMouseClicked(e -> { TaskExecutorDialogPane pane = Controllers.taskDialog(entry.getExecutor(), entry.getTitle(), TaskCancellationAction.NORMAL); pane.setEscAction(() -> pane.fireEvent(new DialogCloseEvent())); + pane.setCancelText("关闭");//TODO: i18n + pane.setCancelAction(() -> pane.fireEvent(new DialogCloseEvent())); pane.refreshTaskList(); }); return row; } + private Node createLineItem(String text, Runnable onClick) { + Label label = new Label(text); + label.setPadding(new Insets(10, 12, 10, 12)); + label.setMaxWidth(Double.MAX_VALUE); + label.setPrefWidth(Double.MAX_VALUE); + + Separator separator = new Separator(); + separator.setMaxWidth(Double.MAX_VALUE); + separator.setPrefWidth(Double.MAX_VALUE); + + VBox box = new VBox(label, separator); + box.setFillWidth(true); + if (onClick != null) { + box.setOnMouseClicked(e -> onClick.run()); + } + return box; + } + @Override public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); From eaf13c500174244612023e3ff027cefe607c2b60 Mon Sep 17 00:00:00 2001 From: lokins Date: Thu, 5 Feb 2026 22:54:54 +0800 Subject: [PATCH 08/28] update --- .../org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index f787240848..f301e1197d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -27,7 +27,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; -import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; @@ -42,7 +41,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class TaskExecutorDialogPane extends BorderPane { private TaskExecutor executor; From f779a4fe079d567fcf04d73915e5aa260be4ae62 Mon Sep 17 00:00:00 2001 From: lokins Date: Fri, 6 Feb 2026 00:48:42 +0800 Subject: [PATCH 09/28] update --- .../hmcl/ui/download/DownloadPage.java | 12 +++--- .../ModpackInstallWizardProvider.java | 2 +- .../ui/download/ModpackSelectionPage.java | 2 +- .../UpdateInstallerWizardProvider.java | 4 +- .../VanillaInstallWizardProvider.java | 2 +- .../hmcl/ui/main/JavaDownloadDialog.java | 4 +- .../jackhuang/hmcl/ui/versions/Versions.java | 43 ++++++++++--------- 7 files changed, 36 insertions(+), 33 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index fb24559521..3c3545aa90 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -137,19 +137,19 @@ public static void download(Profile profile, @Nullable String version, RemoteMod String detailPrefix; switch (subdirectoryName) { case "mods": - detailPrefix = "安装模组"; + detailPrefix = "安装模组";//TODO i18n break; case "resourcepacks": - detailPrefix = "安装资源包"; + detailPrefix = "安装资源包";//TODO i18n break; case "shaderpacks": - detailPrefix = "安装光影"; + detailPrefix = "安装光影";//TODO i18n break; case "saves": - detailPrefix = "安装世界"; + detailPrefix = "安装世界";//TODO i18n break; default: - detailPrefix = "下载"; + detailPrefix = "下载";//TODO i18n break; } @@ -322,7 +322,7 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); - settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]"); + settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]");//TODO i18n settings.put("backgroundable", true); return finishVersionDownloadingAsync(settings); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index cd9c382168..db4b8c5e72 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -144,7 +144,7 @@ public Object finish(SettingsMap settings) { Object name = settings.get("name"); if (name != null) { - settings.put("task_detail", "安装整合包-[" + name + "]"); + settings.put("task_detail", "安装整合包-[" + name + "]");//TODO: i18n } settings.put("backgroundable", true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index 76ce27d22a..83a4d2ebe4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -159,7 +159,7 @@ private void onChooseRemoteFile() { }), i18n("message.downloading"), TaskCancellationAction.NORMAL, - "安装整合包-[" + modpack.getFileName() + "]" + "安装整合包-[" + modpack.getFileName() + "]"//TODO: i18n ); } } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index dbb2356591..1b165ecbc4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -78,12 +78,12 @@ public Object finish(SettingsMap settings) { String detail = null; for (Object value : settings.asStringMap().values()) { if (value instanceof RemoteVersion remoteVersion) { - detail = "安装" + remoteVersion.getLibraryId() + "-[" + remoteVersion.getSelfVersion() + "]"; + detail = "安装" + remoteVersion.getLibraryId() + "-[" + remoteVersion.getSelfVersion() + "]";//TODO i18n break; } } if (detail == null) { - detail = "安装" + libraryId; + detail = "安装" + libraryId;//TODO i18n } settings.put("task_detail", detail); settings.put("backgroundable", true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java index ef6853e06e..3928f28c8b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java @@ -70,7 +70,7 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); - settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]"); + settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]");//TODO i18n settings.put("backgroundable", true); return finishVersionDownloadingAsync(settings); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java index 85b7e782c3..779ccac47b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java @@ -159,7 +159,7 @@ protected void onAccept() { if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion)) Controllers.confirm(i18n("download.java.override"), null, () -> { - String detail = "安装Java-[" + javaVersion.majorVersion() + "]"; + String detail = "安装Java-[" + javaVersion.majorVersion() + "]";//TODO: i18n Controllers.downloadTaskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion)) .thenComposeAsync(Schedulers.javafx(), realPath -> { if (realPath != null) { @@ -169,7 +169,7 @@ protected void onAccept() { }), i18n("download.java"), TaskCancellationAction.NORMAL, detail); }, null); else - Controllers.downloadTaskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL, "安装Java-[" + javaVersion.majorVersion() + "]"); + Controllers.downloadTaskDialog(downloadTask(javaVersion), i18n("download.java.process"), TaskCancellationAction.NORMAL, "安装Java-[" + javaVersion.majorVersion() + "]");//TODO: i18n } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index f422d2a334..6ec8fbfa32 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -84,29 +84,32 @@ public static void downloadModpackImpl(Profile profile, String version, RemoteMo i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); return; } + + Task downloadTask = new FileDownloadTask(downloadURL, modpack) + .whenComplete(Schedulers.javafx(), e -> { + if (e == null) { + ModpackInstallWizardProvider installWizardProvider; + if (version != null) + installWizardProvider = new ModpackInstallWizardProvider(profile, modpack, version); + else + installWizardProvider = new ModpackInstallWizardProvider(profile, modpack); + if (StringUtils.isNotBlank(mod.getIconUrl())) + installWizardProvider.setIconUrl(mod.getIconUrl()); + Controllers.getDecorator().startWizard(installWizardProvider); + } else if (e instanceof CancellationException) { + Controllers.showToast(i18n("message.cancelled")); + } else { + Controllers.dialog( + i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), + i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); + } + }); + Controllers.downloadTaskDialog( - new FileDownloadTask(downloadURL, modpack) - .whenComplete(Schedulers.javafx(), e -> { - if (e == null) { - ModpackInstallWizardProvider installWizardProvider; - if (version != null) - installWizardProvider = new ModpackInstallWizardProvider(profile, modpack, version); - else - installWizardProvider = new ModpackInstallWizardProvider(profile, modpack); - if (StringUtils.isNotBlank(mod.getIconUrl())) - installWizardProvider.setIconUrl(mod.getIconUrl()); - Controllers.getDecorator().startWizard(installWizardProvider); - } else if (e instanceof CancellationException) { - Controllers.showToast(i18n("message.cancelled")); - } else { - Controllers.dialog( - i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), - i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); - } - }).executor(true), + downloadTask, i18n("message.downloading"), TaskCancellationAction.NORMAL, - "安装整合包-[" + modpack.getFileName() + "]" + "安装整合包-[" + modpack.getFileName() + "]"//TODO:i18n ); } From 82bd0b86ab97003b197492fd50247c8ea6aeff46 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:28:13 +0800 Subject: [PATCH 10/28] update --- .../ui/construct/TaskExecutorDialogPane.java | 50 +++++++++++++++---- .../hmcl/ui/download/DownloadPage.java | 1 + .../jackhuang/hmcl/ui/task/TaskCenter.java | 12 +++++ .../hmcl/ui/task/TaskCenterPage.java | 5 ++ .../TaskExecutorDialogWizardDisplayer.java | 12 +++++ .../hmcl/task/AsyncTaskExecutor.java | 4 ++ 6 files changed, 73 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index f301e1197d..fa4371ca28 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -29,6 +29,7 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.ui.SVG; @@ -115,17 +116,7 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { setCancel(cancel); - btnCancel.setOnAction(e -> { - if (cancelAction != null) { - cancelAction.run(); - return; - } - - Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel); - if (onCancel.getCancellationAction() != null) { - onCancel.getCancellationAction().accept(this); - } - }); + btnCancel.setOnAction(e -> handleCancelOrClose()); speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> { String message = I18n.formatSpeed(speedEvent.getSpeed()); @@ -140,6 +131,41 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { }); } + private boolean isQueuedNotStarted() { + if (executor == null) { + return false; + } + if (executor instanceof AsyncTaskExecutor asyncExecutor) { + return !asyncExecutor.isStarted(); + } + return false; + } + + private void handleCancelOrClose() { + if (isQueuedNotStarted()) { + fireEvent(new DialogCloseEvent()); + return; + } + + if (cancelAction != null) { + cancelAction.run(); + return; + } + + Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel); + if (onCancel.getCancellationAction() != null) { + onCancel.getCancellationAction().accept(this); + } + } + + private void applyQueuedStateIfNeeded() { + if (isQueuedNotStarted()) { + setCancelText("关闭"); // TODO: i18n + } else { + setCancelText(i18n("button.cancel")); + } + } + public void setExecutor(TaskExecutor executor) { setExecutor(executor, true); } @@ -158,6 +184,7 @@ public void onStop(boolean success, TaskExecutor executor) { } }); } + applyQueuedStateIfNeeded(); } public StringProperty titleProperty() { @@ -204,6 +231,7 @@ public void setBackgroundAction(Runnable action) { public void refreshTaskList() { taskListPane.refresh(); + applyQueuedStateIfNeeded(); } private Runnable cancelAction; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 3c3545aa90..2f26e1441e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -324,6 +324,7 @@ public Object finish(SettingsMap settings) { settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]");//TODO i18n settings.put("backgroundable", true); + settings.put("return_to_download_list", true); return finishVersionDownloadingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index aa53894d7b..4ecc173565 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -71,6 +71,10 @@ public ObservableList getEntries() { return entries; } + public Entry getRunningEntry() { + return running; + } + public ObservableList getCompletedEntries() { return completedEntries; } @@ -134,4 +138,12 @@ public void onStop(boolean success, TaskExecutor executor) { executor.start(); } + + public synchronized boolean contains(TaskExecutor executor) { + return entryIndex.containsKey(executor); + } + + public synchronized boolean isStarted(TaskExecutor executor) { + return Boolean.TRUE.equals(started.get(executor)); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 5ecd79bf8f..0aa3fcf546 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -187,6 +187,11 @@ private Node createRunningItem(TaskCenter.Entry entry) { row.getChildren().addAll(label, cancelButton); row.setOnMouseClicked(e -> { + if (entry != TaskCenter.getInstance().getRunningEntry()) { + Controllers.dialog("任务等待中", entry.getTitle(), MessageDialogPane.MessageType.INFO);//TODO: i18n + return; + } + TaskExecutorDialogPane pane = Controllers.taskDialog(entry.getExecutor(), entry.getTitle(), TaskCancellationAction.NORMAL); pane.setEscAction(() -> pane.fireEvent(new DialogCloseEvent())); pane.setCancelText("关闭");//TODO: i18n diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index 9996a49fb2..cf48f44759 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.wizard; import javafx.beans.property.StringProperty; +import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.task.TaskExecutor; @@ -93,6 +94,14 @@ else if (!settings.containsKey("forbid_failure_message")) pane.setExecutor(executor); + pane.addEventHandler(DialogCloseEvent.CLOSE, event -> { + if (executor instanceof AsyncTaskExecutor asyncExecutor && !asyncExecutor.isStarted()) { + onEnd(); + Controllers.getDownloadPage().showGameDownloads(); + Controllers.navigate(Controllers.getDownloadPage()); + } + }); + if (backgroundable) { Object detailObj = settings.get("task_detail"); String detail = detailObj != null ? detailObj.toString() : pane.getTitle(); @@ -100,15 +109,18 @@ else if (!settings.containsKey("forbid_failure_message")) pane.setBackgroundAction(() -> { pane.fireEvent(new DialogCloseEvent()); TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); + pane.refreshTaskList(); }); TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); + pane.refreshTaskList(); } Controllers.dialog(pane); if (!backgroundable) { executor.start(); + pane.refreshTaskList(); } }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java index fe16a8a2a1..526b0a90e0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java @@ -334,4 +334,8 @@ private static Exception convertInterruptedException(Exception e) { public static void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { AsyncTaskExecutor.uncaughtExceptionHandler = uncaughtExceptionHandler; } + + public boolean isStarted() { + return future != null; + } } From 7f56b66359df6f408d2ce7da2507399e5ae546d0 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:34:21 +0800 Subject: [PATCH 11/28] update --- .../java/org/jackhuang/hmcl/ui/main/RootPage.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index feb146adb6..8c1951a34b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -66,7 +66,6 @@ import java.util.stream.Collectors; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; -import static org.jackhuang.hmcl.ui.FXUtils.wrap; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -172,7 +171,7 @@ protected Skin(RootPage control) { // third item in left sidebar AdvancedListItem gameItem = new AdvancedListItem(); - gameItem.setLeftGraphic(wrap(SVG.FORMAT_LIST_BULLETED)); + gameItem.setLeftGraphic(FXUtils.wrap(SVG.FORMAT_LIST_BULLETED)); gameItem.setActionButtonVisible(false); gameItem.setTitle(i18n("version.manage")); gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); @@ -180,7 +179,7 @@ protected Skin(RootPage control) { // forth item in left sidebar AdvancedListItem downloadItem = new AdvancedListItem(); - downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD)); + downloadItem.setLeftGraphic(FXUtils.wrap(SVG.DOWNLOAD)); downloadItem.setActionButtonVisible(false); downloadItem.setTitle(i18n("download")); downloadItem.setOnAction(e -> { @@ -193,7 +192,7 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //SVG待更换 + taskManagerItem.setLeftGraphic(FXUtils.wrap(SVG.LIST)); //TODO SVG待更换 taskManagerItem.setActionButtonVisible(false); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { @@ -203,7 +202,7 @@ protected Skin(RootPage control) { // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); - launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS)); + launcherSettingsItem.setLeftGraphic(FXUtils.wrap(SVG.SETTINGS)); launcherSettingsItem.setActionButtonVisible(false); launcherSettingsItem.setTitle(i18n("settings")); launcherSettingsItem.setOnAction(e -> { @@ -216,7 +215,7 @@ protected Skin(RootPage control) { // sixth item in left sidebar AdvancedListItem terracottaItem = new AdvancedListItem(); - terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2)); + terracottaItem.setLeftGraphic(FXUtils.wrap(SVG.GRAPH2)); terracottaItem.setActionButtonVisible(false); terracottaItem.setTitle(i18n("terracotta")); terracottaItem.setOnAction(e -> { From 051ded909247d44ece067c5035319263ba47c0b9 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:37:56 +0800 Subject: [PATCH 12/28] update --- .../main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 8c1951a34b..3e99ddd410 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -66,6 +66,7 @@ import java.util.stream.Collectors; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.ui.FXUtils.wrap; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -179,7 +180,7 @@ protected Skin(RootPage control) { // forth item in left sidebar AdvancedListItem downloadItem = new AdvancedListItem(); - downloadItem.setLeftGraphic(FXUtils.wrap(SVG.DOWNLOAD)); + downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD)); downloadItem.setActionButtonVisible(false); downloadItem.setTitle(i18n("download")); downloadItem.setOnAction(e -> { @@ -192,7 +193,7 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(FXUtils.wrap(SVG.LIST)); //TODO SVG待更换 + taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //SVG待更换 taskManagerItem.setActionButtonVisible(false); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { @@ -202,7 +203,7 @@ protected Skin(RootPage control) { // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); - launcherSettingsItem.setLeftGraphic(FXUtils.wrap(SVG.SETTINGS)); + launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS)); launcherSettingsItem.setActionButtonVisible(false); launcherSettingsItem.setTitle(i18n("settings")); launcherSettingsItem.setOnAction(e -> { @@ -215,7 +216,7 @@ protected Skin(RootPage control) { // sixth item in left sidebar AdvancedListItem terracottaItem = new AdvancedListItem(); - terracottaItem.setLeftGraphic(FXUtils.wrap(SVG.GRAPH2)); + terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2)); terracottaItem.setActionButtonVisible(false); terracottaItem.setTitle(i18n("terracotta")); terracottaItem.setOnAction(e -> { From 72e0e05eb3ebe393fdf6742b7489fbad471e546b Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:38:27 +0800 Subject: [PATCH 13/28] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 3e99ddd410..d7967a8da6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -172,7 +172,7 @@ protected Skin(RootPage control) { // third item in left sidebar AdvancedListItem gameItem = new AdvancedListItem(); - gameItem.setLeftGraphic(FXUtils.wrap(SVG.FORMAT_LIST_BULLETED)); + gameItem.setLeftGraphic(wrap(SVG.FORMAT_LIST_BULLETED)); gameItem.setActionButtonVisible(false); gameItem.setTitle(i18n("version.manage")); gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); @@ -193,7 +193,7 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //SVG待更换 + taskManagerItem.setLeftGraphic(FXUtils.wrap(SVG.LIST)); //TODO SVG待更换 taskManagerItem.setActionButtonVisible(false); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { From 641b02574c08870d53c5da423e740c1d2142afd3 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:42:19 +0800 Subject: [PATCH 14/28] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index d7967a8da6..8e97528c01 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -193,7 +193,7 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(FXUtils.wrap(SVG.LIST)); //TODO SVG待更换 + taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //TODO SVG待更换 taskManagerItem.setActionButtonVisible(false); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { From a949cc149b1e4768987233ac13adde08d55a70d7 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:52:36 +0800 Subject: [PATCH 15/28] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 8e97528c01..66cc6cdeb8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -193,8 +193,8 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //TODO SVG待更换 - taskManagerItem.setActionButtonVisible(false); + taskManagerItem.setLeftGraphic(SVG.LIST.createIcon(16)); // TODO SVG待更换 + taskManagerItem.actionButtonVisibleProperty().set(false); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { Controllers.navigate(new TaskCenterPage()); From 699a52e4659573e9f4847bdd1fcf0d603d83c858 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 00:56:05 +0800 Subject: [PATCH 16/28] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 66cc6cdeb8..8e97528c01 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -193,8 +193,8 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(SVG.LIST.createIcon(16)); // TODO SVG待更换 - taskManagerItem.actionButtonVisibleProperty().set(false); + taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //TODO SVG待更换 + taskManagerItem.setActionButtonVisible(false); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { Controllers.navigate(new TaskCenterPage()); From ded3ff864a0db6a03fee33f9fc539601df33282d Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 17:44:42 +0800 Subject: [PATCH 17/28] update --- HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 6ddd56b462..bdfdfec2ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -191,8 +191,7 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftGraphic(wrap(SVG.LIST)); //TODO SVG待更换 - taskManagerItem.setActionButtonVisible(false); + taskManagerItem.setLeftIcon(SVG.LIST); //TODO SVG待更换 taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { Controllers.navigate(new TaskCenterPage()); From bd632ce6cb3ecba1d07e14e7a8d9fb61c127627c Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 17:51:26 +0800 Subject: [PATCH 18/28] update --- .../org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index fa4371ca28..7a7f9194ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -29,7 +29,6 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.ui.SVG; From 248704f9c46b1f79cb0b2c84d09be8cd1ef6e498 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 18:58:28 +0800 Subject: [PATCH 19/28] update --- .../hmcl/ui/download/DownloadPage.java | 3 + .../hmcl/ui/download/InstallersPage.java | 4 +- .../hmcl/ui/download/LocalModpackPage.java | 6 +- .../ModpackInstallWizardProvider.java | 10 ++- .../hmcl/ui/download/RemoteModpackPage.java | 4 +- .../jackhuang/hmcl/ui/task/TaskCenter.java | 75 ++++++++++++++++++- .../hmcl/ui/task/TaskCenterPage.java | 8 +- .../TaskExecutorDialogWizardDisplayer.java | 20 ++++- 8 files changed, 115 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 2f26e1441e..c3ea56aa34 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.versions.DownloadListPage; import org.jackhuang.hmcl.ui.versions.HMCLLocalizedDownloadListPage; import org.jackhuang.hmcl.ui.versions.VersionPage; @@ -325,6 +326,8 @@ public Object finish(SettingsMap settings) { settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]");//TODO i18n settings.put("backgroundable", true); settings.put("return_to_download_list", true); + settings.put("task_kind", TaskCenter.TaskKind.GAME_INSTALL); + settings.put("task_name", settings.get("name")); return finishVersionDownloadingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java index d0c4d7b361..7dc6d5fc42 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java @@ -26,6 +26,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.RequiredValidator; import org.jackhuang.hmcl.ui.construct.Validator; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.util.SettingsMap; @@ -41,7 +42,8 @@ public InstallersPage(WizardController controller, HMCLGameRepository repository txtName.getValidators().addAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)), + new Validator(i18n("install.new_game.already_exists"), str -> + !repository.versionIdConflicts(str) && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.GAME_INSTALL, str)), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty())); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java index 064f580d52..ac19724e1b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.RequiredValidator; import org.jackhuang.hmcl.ui.construct.Validator; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.util.SettingsMap; import org.jackhuang.hmcl.util.StringUtils; @@ -67,12 +68,13 @@ public LocalModpackPage(WizardController controller) { if (installAsVersion) { txtModpackName.getValidators().setAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)), + new Validator(i18n("install.new_game.already_exists"), str -> + !profile.getRepository().versionIdConflicts(str) && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.MODPACK_INSTALL, str)), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); } else { txtModpackName.getValidators().setAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str) && Profiles.getProfiles().stream().noneMatch(p -> p.getName().equals(str))), + new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str) && Profiles.getProfiles().stream().noneMatch(p -> p.getName().equals(str)) && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.MODPACK_INSTALL, str)), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); } }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index db4b8c5e72..dde8ce4a45 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -30,6 +30,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.util.SettingsMap; @@ -142,11 +143,12 @@ public Object finish(SettingsMap settings) { } }); - Object name = settings.get("name"); - if (name != null) { - settings.put("task_detail", "安装整合包-[" + name + "]");//TODO: i18n + String taskName = settings.get(LocalModpackPage.MODPACK_NAME); + if (taskName != null) { + settings.put("task_detail", "安装整合包-[" + taskName + "]");//TODO: i18n } - settings.put("backgroundable", true); + settings.put("task_kind", TaskCenter.TaskKind.MODPACK_INSTALL); + settings.put("task_name", taskName); return finishModpackInstallingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java index 2ace1017fe..9645536e84 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.RequiredValidator; import org.jackhuang.hmcl.ui.construct.Validator; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.util.SettingsMap; import org.jackhuang.hmcl.util.StringUtils; @@ -67,7 +68,8 @@ public RemoteModpackPage(WizardController controller) { txtModpackName.setText(manifest.getName().trim()); txtModpackName.getValidators().addAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)), + new Validator(i18n("install.new_game.already_exists"), str -> + !profile.getRepository().versionIdConflicts(str) && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.MODPACK_INSTALL, str)), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index 4ecc173565..b8352d1f9d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -26,6 +26,7 @@ import javafx.collections.ObservableList; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskListener; +import org.jackhuang.hmcl.util.platform.OperatingSystem; public final class TaskCenter { private static final TaskCenter INSTANCE = new TaskCenter(); @@ -34,15 +35,25 @@ public static TaskCenter getInstance() { return INSTANCE; } + public enum TaskKind { + GAME_INSTALL, + MODPACK_INSTALL, + OTHER + } + public static final class Entry { private final TaskExecutor executor; private final String title; private final String detail; + private final TaskKind kind; + private final String name; - public Entry(TaskExecutor executor, String title, String detail) { + public Entry(TaskExecutor executor, String title, String detail, TaskKind kind, String name) { this.executor = executor; this.title = title; this.detail = detail; + this.kind = kind; + this.name = name; } public TaskExecutor getExecutor() { @@ -56,6 +67,14 @@ public String getTitle() { public String getDetail() { return detail; } + + public TaskKind getKind() { + return kind; + } + + public String getName() { + return name; + } } private final ObservableList entries = FXCollections.observableArrayList(); @@ -93,7 +112,24 @@ public synchronized void enqueue(TaskExecutor executor, String title, String det return; } - Entry entry = new Entry(executor, title, detail); + Entry entry = new Entry(executor, title, detail,TaskKind.OTHER, null); + entryIndex.put(executor, entry); + entries.add(entry); + queue.add(entry); + tryStartNext(); + } + + public synchronized void enqueue(TaskExecutor executor, String title, String detail, TaskKind kind, String name) { + if (!Platform.isFxApplicationThread()) { + Platform.runLater(() -> enqueue(executor, title, detail, kind, name)); + return; + } + + if (entryIndex.containsKey(executor)) { + return; + } + + Entry entry = new Entry(executor, title, detail, kind, name); entryIndex.put(executor, entry); entries.add(entry); queue.add(entry); @@ -146,4 +182,39 @@ public synchronized boolean contains(TaskExecutor executor) { public synchronized boolean isStarted(TaskExecutor executor) { return Boolean.TRUE.equals(started.get(executor)); } + + public synchronized boolean hasQueuedInstallName(TaskKind kind, String name) { + if (name == null || kind == null) { + return false; + } + for (Entry entry : entries) { + if (entry.getKind() != kind || entry.getName() == null) { + continue; + } + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { + if (entry.getName().equalsIgnoreCase(name)) return true; + } else { + if (entry.getName().equals(name)) return true; + } + } + return false; + } + + public synchronized boolean cancelQueued(TaskExecutor executor) { + Entry entry = entryIndex.get(executor); + if (entry == null) { + return false; + } + + if (Boolean.TRUE.equals(started.get(executor))) { + } + + queue.remove(entry); + entries.remove(entry); + entryIndex.remove(executor); + started.remove(executor); + failedEntries.add(entry); + return true; + } + } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 0aa3fcf546..291dacad80 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -179,9 +179,15 @@ private Node createRunningItem(TaskCenter.Entry entry) { cancelButton.getStyleClass().add("dialog-cancel"); cancelButton.setOnAction(e -> { - entry.getExecutor().cancel(); + TaskCenter taskCenter = TaskCenter.getInstance(); + if (taskCenter.isStarted(entry.getExecutor())) { + entry.getExecutor().cancel(); + } else { + taskCenter.cancelQueued(entry.getExecutor()); + } e.consume(); }); + cancelButton.setOnMouseClicked(e -> e.consume()); row.getChildren().addAll(label, cancelButton); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index cf48f44759..158bad6ea8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -95,7 +95,8 @@ else if (!settings.containsKey("forbid_failure_message")) pane.setExecutor(executor); pane.addEventHandler(DialogCloseEvent.CLOSE, event -> { - if (executor instanceof AsyncTaskExecutor asyncExecutor && !asyncExecutor.isStarted()) { + boolean returnToDownloadList = Boolean.TRUE.equals(settings.get("return_to_download_list")); + if (returnToDownloadList) { onEnd(); Controllers.getDownloadPage().showGameDownloads(); Controllers.navigate(Controllers.getDownloadPage()); @@ -106,13 +107,24 @@ else if (!settings.containsKey("forbid_failure_message")) Object detailObj = settings.get("task_detail"); String detail = detailObj != null ? detailObj.toString() : pane.getTitle(); + TaskCenter.TaskKind kind = (TaskCenter.TaskKind) settings.get("task_kind"); + String taskName = (String) settings.get("task_name"); + pane.setBackgroundAction(() -> { - pane.fireEvent(new DialogCloseEvent()); - TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); pane.refreshTaskList(); + + boolean returnToDownloadList = Boolean.TRUE.equals(settings.get("return_to_download_list")); + onEnd(); + if (returnToDownloadList) { + Controllers.getDownloadPage().showGameDownloads(); + Controllers.navigate(Controllers.getDownloadPage()); + } + + pane.fireEvent(new DialogCloseEvent()); }); - TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail); + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); pane.refreshTaskList(); } From ea23b956c13d604f6b855143de468313dee948bb Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 7 Feb 2026 19:01:04 +0800 Subject: [PATCH 20/28] update --- .../hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index 158bad6ea8..b04a83bcc1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui.wizard; import javafx.beans.property.StringProperty; -import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.task.TaskExecutor; From dff9e085d0ac7df863ffc8c21c17a2524a39d7b1 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 21 Mar 2026 10:31:29 +0800 Subject: [PATCH 21/28] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=EF=BC=8C=E9=87=8D=E5=88=B6=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8=E7=95=8C=E9=9D=A2=EF=BC=8C=E9=80=82?= =?UTF-8?q?=E9=85=8D=E6=9B=B4=E5=A4=9A=E5=90=8E=E5=8F=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 5 +- .../org/jackhuang/hmcl/ui/Controllers.java | 10 +- .../ui/construct/TaskExecutorDialogPane.java | 2 +- .../hmcl/ui/construct/TaskListPane.java | 2 +- .../hmcl/ui/download/DownloadPage.java | 2 +- .../ModpackInstallWizardProvider.java | 1 + .../ui/download/ModpackSelectionPage.java | 2 +- .../VanillaInstallWizardProvider.java | 6 +- .../hmcl/ui/export/ExportWizardProvider.java | 4 + .../hmcl/ui/main/JavaDownloadDialog.java | 8 +- .../org/jackhuang/hmcl/ui/main/MainPage.java | 11 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 19 +- .../jackhuang/hmcl/ui/task/TaskCenter.java | 130 ++++++---- .../hmcl/ui/task/TaskCenterPage.java | 233 ++++++++++++------ .../hmcl/ui/versions/DownloadPage.java | 5 +- .../hmcl/ui/versions/InstallerListPage.java | 4 +- .../hmcl/ui/versions/ModListPage.java | 5 +- .../hmcl/ui/versions/ModUpdatesPage.java | 5 +- .../jackhuang/hmcl/ui/versions/Versions.java | 17 +- .../hmcl/ui/versions/WorldBackupsPage.java | 5 +- .../hmcl/ui/versions/WorldExportPage.java | 8 + .../ui/wizard/SinglePageWizardProvider.java | 1 + .../TaskExecutorDialogWizardDisplayer.java | 10 +- .../hmcl/ui/wizard/WizardSinglePage.java | 7 + .../resources/assets/lang/I18N.properties | 27 +- .../resources/assets/lang/I18N_zh.properties | 33 +++ .../assets/lang/I18N_zh_CN.properties | 33 +++ .../hmcl/task/AsyncTaskExecutor.java | 2 +- 28 files changed, 421 insertions(+), 176 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 823b3c0677..68a2e5b9fd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -667,7 +667,7 @@ private static CompletableFuture downloadJava(GameJavaVersion javaV MessageType.QUESTION) .yesOrNo(() -> { DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider(); - Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion) + Controllers.downloadTaskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { future.complete(result); @@ -679,7 +679,8 @@ private static CompletableFuture downloadJava(GameJavaVersion javaV } future.completeExceptionally(new CancellationException()); } - }), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException()))); + }), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())), + i18n("task.detail.java_download")); }, () -> future.completeExceptionally(new CancellationException())).build()); return future; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 91f7e751bd..b17d3fd139 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -595,23 +595,23 @@ public static TaskExecutorDialogPane downloadTaskDialog(Task task, String tit TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel); pane.setBackgroundAction(() -> { - pane.fireEvent(new DialogCloseEvent()); TaskCenter.getInstance().enqueue(executor, title, detail); + pane.fireEvent(new DialogCloseEvent()); }); - TaskCenter.getInstance().enqueue(executor, title, detail); + executor.start(); return pane; } - public static TaskExecutorDialogPane downloadTaskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel,String detail) { + public static TaskExecutorDialogPane downloadTaskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel, String detail) { TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel); pane.setBackgroundAction(() -> { + TaskCenter.getInstance().enqueue(executor, title, detail); pane.fireEvent(new DialogCloseEvent()); - TaskCenter.getInstance().enqueue(executor, title,detail); }); - TaskCenter.getInstance().enqueue(executor, title, detail); + executor.start(); return pane; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index b0b0cd719d..f1a1599b72 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -165,7 +165,7 @@ private void handleCancelOrClose() { private void applyQueuedStateIfNeeded() { if (isQueuedNotStarted()) { - setCancelText("关闭"); // TODO: i18n + setCancelText(i18n("button.close")); } else { setCancelText(i18n("button.cancel")); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index e1dd8f0975..9b7e91bae2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -531,7 +531,7 @@ public void refresh() { Platform.runLater(() -> { stageNodes.clear(); listView.getItems().clear(); - addStages(executor.getStages()); + addStagesHints(executor.getHints()); updateProgressNodePadding(); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index ae0f8b8256..0270810280 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -157,7 +157,7 @@ public static void download(DownloadProvider downloadProvider, Profile profile, Controllers.prompt(i18n("archive.file.name"), (result, handler) -> { Path dest = runDirectory.resolve(subdirectoryName).resolve(result); - Controllers.taskDialog(Task.composeAsync(() -> { + Controllers.downloadTaskDialog(Task.composeAsync(() -> { var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.getFile().getUrl()), dest); task.setName(file.getName()); return task; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index f87e1b8b5f..23ab06c7a2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -149,6 +149,7 @@ public Object finish(SettingsMap settings) { if (taskName != null) { settings.put("task_detail", "安装整合包-[" + taskName + "]");//TODO: i18n } + settings.put("backgroundable", true); settings.put("task_kind", TaskCenter.TaskKind.MODPACK_INSTALL); settings.put("task_name", taskName); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index 21eb2a8c78..d5ccb44ac5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -144,7 +144,7 @@ private void onChooseRemoteFile() { // otherwise we still consider the file as modpack zip file // since casually the url may not ends with ".zip" Path modpack = Files.createTempFile("modpack", ".zip"); - Controllers.taskDialog( + Controllers.downloadTaskDialog( new FileDownloadTask(url, modpack) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java index 3928f28c8b..17a04e0ca7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.wizard.WizardController; @@ -70,8 +71,11 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); - settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]");//TODO i18n + String name = (String) settings.get("name"); + settings.put("task_detail", "安装游戏-[" + name + "]");//TODO i18n settings.put("backgroundable", true); + settings.put("task_kind", TaskCenter.TaskKind.GAME_INSTALL); + settings.put("task_name", name); return finishVersionDownloadingAsync(settings); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java index 54bcadbb11..8afad3f3e5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java @@ -46,6 +46,7 @@ import java.util.List; import static org.jackhuang.hmcl.setting.ConfigHolder.config; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class ExportWizardProvider implements WizardProvider { private final Profile profile; @@ -68,6 +69,9 @@ public Object finish(SettingsMap settings) { exportInfo.setWhitelist(whitelist); String modpackType = settings.get(ModpackTypeSelectionPage.MODPACK_TYPE); + settings.put("backgroundable", true); + settings.put("task_detail", i18n("task.detail.modpack_export", exportInfo.getName())); + return exportWithLauncher(modpackType, exportInfo, modpackFile); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java index 1a9efa1dd1..0c5819fe41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java @@ -200,7 +200,8 @@ protected void onAccept() { } }); - Controllers.taskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL); + Controllers.downloadTaskDialog(task, i18n("download.java.process"), TaskCancellationAction.NORMAL, + i18n("task.detail.java_download")); } } @@ -363,7 +364,7 @@ private void onDownload() { if (version == null) return; - Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri())) + Controllers.downloadTaskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri())) .setExecutor(Schedulers.io()) .thenComposeAsync(json -> { DiscoResult result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class)); @@ -425,7 +426,8 @@ else if (StringUtils.isNotBlank(fileInfo.getChecksumUri())) Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed")); } } - })), i18n("java.download"), TaskCancellationAction.NORMAL); + })), i18n("java.download"), TaskCancellationAction.NORMAL, + i18n("task.detail.java_download")); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 3393035fc6..a46fbde5ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -52,6 +52,7 @@ import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; @@ -326,6 +327,13 @@ private void launchNoGame() { .sorted() .findFirst() .orElseThrow(() -> new IOException("No versions found"))) + .thenComposeAsync(Schedulers.javafx(), version -> { + String gameVersion = version.getGameVersion(); + if (TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.GAME_INSTALL, gameVersion)) { + throw new CancellationException(i18n("install.new_game.already_exists")); + } + return Task.completed(version); + }) .thenComposeAsync(version -> { Profile profile = Profiles.getSelectedProfile(); DefaultDependencyManager dependency = profile.getDependency(); @@ -350,7 +358,8 @@ private void launchNoGame() { MessageDialogPane.MessageType.WARNING); } }); - Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL); + Controllers.downloadTaskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL, + i18n("task.detail.game_install")); } private void onUpgrade() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index bdfdfec2ac..85e8535759 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -46,6 +46,8 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.ui.nbt.NBTFileType; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.task.TaskCenterPage; import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; import org.jackhuang.hmcl.ui.versions.GameListPopupMenu; @@ -289,12 +291,17 @@ private void onRefreshedVersions(HMCLGameRepository repository) { if (modpackFile != null) { Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile)) .thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding)) - .thenApplyAsync(modpack -> ModpackHelper - .getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null) - .executor()) - .thenAcceptAsync(Schedulers.javafx(), executor -> { - Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL); - executor.start(); + .thenAcceptAsync(Schedulers.javafx(), modpack -> { + String modpackName = modpack.getName(); + if (TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.MODPACK_INSTALL, modpackName)) { + Controllers.dialog(i18n("install.new_game.already_exists"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING); + return; + } + TaskExecutor executor = ModpackHelper + .getInstallTask(repository.getProfile(), modpackFile, modpackName, modpack, null) + .executor(); + Controllers.downloadTaskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL, + i18n("task.detail.modpack_install")); }).start(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index b8352d1f9d..fc981ca031 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -26,8 +26,11 @@ import javafx.collections.ObservableList; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskListener; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + public final class TaskCenter { private static final TaskCenter INSTANCE = new TaskCenter(); @@ -38,6 +41,8 @@ public static TaskCenter getInstance() { public enum TaskKind { GAME_INSTALL, MODPACK_INSTALL, + JAVA_DOWNLOAD, + MOD_UPDATE, OTHER } @@ -102,29 +107,22 @@ public ObservableList getFailedEntries() { return failedEntries; } - public synchronized void enqueue(TaskExecutor executor, String title, String detail) { - if (!Platform.isFxApplicationThread()) { - Platform.runLater(() -> enqueue(executor, title, detail)); - return; - } - - if (entryIndex.containsKey(executor)) { - return; - } + private void assertFxThread() { + assert Platform.isFxApplicationThread() : "TaskCenter must be accessed from FX Application Thread"; + } - Entry entry = new Entry(executor, title, detail,TaskKind.OTHER, null); - entryIndex.put(executor, entry); - entries.add(entry); - queue.add(entry); - tryStartNext(); + public void enqueue(TaskExecutor executor, String title, String detail) { + enqueue(executor, title, detail, TaskKind.OTHER, null); } - public synchronized void enqueue(TaskExecutor executor, String title, String detail, TaskKind kind, String name) { + public void enqueue(TaskExecutor executor, String title, String detail, TaskKind kind, String name) { if (!Platform.isFxApplicationThread()) { Platform.runLater(() -> enqueue(executor, title, detail, kind, name)); return; } + assertFxThread(); + if (entryIndex.containsKey(executor)) { return; } @@ -136,54 +134,76 @@ public synchronized void enqueue(TaskExecutor executor, String title, String det tryStartNext(); } - private synchronized void tryStartNext() { - if (running != null) return; - Entry next = queue.poll(); - if (next == null) return; + private void tryStartNext() { + assertFxThread(); - TaskExecutor executor = next.getExecutor(); - if (Boolean.TRUE.equals(started.get(executor))) { - tryStartNext(); + while (running == null) { + Entry next = queue.poll(); + if (next == null) return; + + TaskExecutor executor = next.getExecutor(); + if (Boolean.TRUE.equals(started.get(executor))) { + continue; + } + + started.put(executor, true); + running = next; + + executor.addTaskListener(new TaskListener() { + @Override + public void onStop(boolean success, TaskExecutor executor) { + Platform.runLater(() -> onTaskStopped(executor, success)); + } + }); + + executor.start(); return; } + } + + private void onTaskStopped(TaskExecutor executor, boolean success) { + assertFxThread(); + + Entry stoppedEntry = entryIndex.remove(executor); + started.remove(executor); - started.put(executor, true); - running = next; - - executor.addTaskListener(new TaskListener() { - @Override - public void onStop(boolean success, TaskExecutor executor) { - Platform.runLater(() -> { - if (running != null) { - entries.remove(running); - entryIndex.remove(executor); - started.remove(executor); - - if (success) { - completedEntries.add(running); - } else { - failedEntries.add(running); - } - } - - running = null; - tryStartNext(); - }); + if (stoppedEntry != null) { + entries.remove(stoppedEntry); + if (success) { + completedEntries.add(stoppedEntry); + } else { + failedEntries.add(stoppedEntry); } - }); - executor.start(); + // Show toast notification for background tasks + String detail = stoppedEntry.getDetail() != null ? stoppedEntry.getDetail() : stoppedEntry.getTitle(); + if (success) { + Controllers.showToast(i18n("task.toast.success", detail)); + } else { + Controllers.showToast(i18n("task.toast.failed", detail)); + } + } + + if (running != null && running.getExecutor() == executor) { + running = null; + } + + tryStartNext(); } - public synchronized boolean contains(TaskExecutor executor) { + public boolean contains(TaskExecutor executor) { + assertFxThread(); return entryIndex.containsKey(executor); } - public synchronized boolean isStarted(TaskExecutor executor) { + public boolean isStarted(TaskExecutor executor) { + assertFxThread(); return Boolean.TRUE.equals(started.get(executor)); } - public synchronized boolean hasQueuedInstallName(TaskKind kind, String name) { + public boolean hasQueuedInstallName(TaskKind kind, String name) { + assertFxThread(); + if (name == null || kind == null) { return false; } @@ -200,20 +220,30 @@ public synchronized boolean hasQueuedInstallName(TaskKind kind, String name) { return false; } - public synchronized boolean cancelQueued(TaskExecutor executor) { + public boolean cancelQueued(TaskExecutor executor) { + assertFxThread(); + Entry entry = entryIndex.get(executor); if (entry == null) { return false; } if (Boolean.TRUE.equals(started.get(executor))) { + // Task is already running — cancel it properly + executor.cancel(); } queue.remove(entry); entries.remove(entry); entryIndex.remove(executor); started.remove(executor); + + if (running != null && running.getExecutor() == executor) { + running = null; + } + failedEntries.add(entry); + tryStartNext(); return true; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 291dacad80..05ef35b2af 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -26,7 +26,6 @@ import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.control.Separator; import javafx.scene.layout.*; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; @@ -48,32 +47,32 @@ public final class TaskCenterPage extends DecoratorAnimatedPage implements Decor new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("task.manage"))); private final TransitionPane transitionPane = new TransitionPane(); - private final TabHeader tabHeader; private final TabHeader.Tab runningTab = new TabHeader.Tab<>("taskRunningTab"); private final TabHeader.Tab completedTab = new TabHeader.Tab<>("taskCompletedTab"); private final TabHeader.Tab failedTab = new TabHeader.Tab<>("taskFailedTab"); - private final TabHeader.Tab settingsTab = new TabHeader.Tab<>("taskSettingsTab"); - private final VBox runningContainer = new VBox(8); - private final VBox completedContainer = new VBox(8); - private final VBox failedContainer = new VBox(8); + private final VBox runningContainer = new VBox(10); + private final VBox completedContainer = new VBox(2); + private final VBox failedContainer = new VBox(2); + + private final Label runningEmpty = new Label(i18n("task.empty.running")); + private final Label completedEmpty = new Label(i18n("task.empty.completed")); + private final Label failedEmpty = new Label(i18n("task.empty.failed")); public TaskCenterPage() { runningTab.setNodeSupplier(this::createRunningPane); completedTab.setNodeSupplier(this::createCompletedPane); failedTab.setNodeSupplier(this::createFailedPane); - settingsTab.setNodeSupplier(() -> createPlaceholderPane(i18n("task.settings"))); - tabHeader = new TabHeader(transitionPane, runningTab, completedTab, failedTab, settingsTab); + TabHeader tabHeader = new TabHeader(transitionPane, runningTab, completedTab, failedTab); tabHeader.select(runningTab); AdvancedListBox sideBar = new AdvancedListBox() .startCategory(i18n("task.manage").toUpperCase(Locale.ROOT)) .addNavigationDrawerTab(tabHeader, runningTab, i18n("task.running"), SVG.ARROW_FORWARD) .addNavigationDrawerTab(tabHeader, completedTab, i18n("task.completed"), SVG.CHECK) - .addNavigationDrawerTab(tabHeader, failedTab, i18n("task.failed"), SVG.CLOSE) - .addNavigationDrawerTab(tabHeader, settingsTab, i18n("task.settings"), SVG.SETTINGS); + .addNavigationDrawerTab(tabHeader, failedTab, i18n("task.failed"), SVG.CLOSE); FXUtils.setLimitWidth(sideBar, 200); setLeft(sideBar); @@ -86,144 +85,220 @@ public TaskCenterPage() { StackPane centerPane = new StackPane(contentWrapper); centerPane.setPadding(new Insets(12)); setCenter(centerPane); + + styleEmptyLabel(runningEmpty); + styleEmptyLabel(completedEmpty); + styleEmptyLabel(failedEmpty); + + TaskCenter.getInstance().getEntries().addListener( + (ListChangeListener) change -> rebuildRunning()); + TaskCenter.getInstance().getCompletedEntries().addListener( + (ListChangeListener) change -> rebuildCompleted()); + TaskCenter.getInstance().getFailedEntries().addListener( + (ListChangeListener) change -> rebuildFailed()); + } + + private static void styleEmptyLabel(Label label) { + label.setStyle("-fx-text-fill: -fx-secondary-text-color; -fx-font-size: 13px;"); + label.setPadding(new Insets(24)); } private ScrollPane createRunningPane() { ScrollPane scrollPane = new ScrollPane(runningContainer); scrollPane.setFitToWidth(true); runningContainer.setPadding(new Insets(12)); - - TaskCenter.getInstance().getEntries().addListener((ListChangeListener) change -> rebuildRunning()); rebuildRunning(); - return scrollPane; } private ScrollPane createCompletedPane() { - ScrollPane scrollPane = new ScrollPane(completedContainer); - scrollPane.setFitToWidth(true); - completedContainer.setPadding(new Insets(12)); + VBox wrapper = new VBox(8); + wrapper.setPadding(new Insets(12)); - TaskCenter.getInstance().getCompletedEntries() - .addListener((ListChangeListener) change -> rebuildCompleted()); - rebuildCompleted(); + JFXButton clearButton = new JFXButton(i18n("task.clear")); + clearButton.getStyleClass().add("dialog-cancel"); + clearButton.setOnAction(e -> TaskCenter.getInstance().getCompletedEntries().clear()); + HBox toolbar = new HBox(clearButton); + toolbar.setAlignment(Pos.CENTER_RIGHT); + + wrapper.getChildren().addAll(toolbar, completedContainer); + + ScrollPane scrollPane = new ScrollPane(wrapper); + scrollPane.setFitToWidth(true); + rebuildCompleted(); return scrollPane; } private ScrollPane createFailedPane() { - ScrollPane scrollPane = new ScrollPane(failedContainer); - scrollPane.setFitToWidth(true); - failedContainer.setPadding(new Insets(12)); + VBox wrapper = new VBox(8); + wrapper.setPadding(new Insets(12)); - TaskCenter.getInstance().getFailedEntries() - .addListener((ListChangeListener) change -> rebuildFailed()); - rebuildFailed(); + JFXButton clearButton = new JFXButton(i18n("task.clear")); + clearButton.getStyleClass().add("dialog-cancel"); + clearButton.setOnAction(e -> TaskCenter.getInstance().getFailedEntries().clear()); + + HBox toolbar = new HBox(clearButton); + toolbar.setAlignment(Pos.CENTER_RIGHT); + + wrapper.getChildren().addAll(toolbar, failedContainer); + ScrollPane scrollPane = new ScrollPane(wrapper); + scrollPane.setFitToWidth(true); + rebuildFailed(); return scrollPane; } + // ── rebuild ────────────────────────────────────────────────────── + private void rebuildRunning() { runningContainer.getChildren().clear(); + if (TaskCenter.getInstance().getEntries().isEmpty()) { + runningContainer.getChildren().add(runningEmpty); + return; + } for (TaskCenter.Entry entry : TaskCenter.getInstance().getEntries()) { - runningContainer.getChildren().add(createRunningItem(entry)); + runningContainer.getChildren().add(createRunningCard(entry)); } } - private ScrollPane createPlaceholderPane(String text) { - VBox box = new VBox(); - box.setPadding(new Insets(12)); - box.getChildren().add(new Label(text)); - ScrollPane scrollPane = new ScrollPane(box); - scrollPane.setFitToWidth(true); - return scrollPane; - } - private void rebuildCompleted() { completedContainer.getChildren().clear(); + if (TaskCenter.getInstance().getCompletedEntries().isEmpty()) { + completedContainer.getChildren().add(completedEmpty); + return; + } for (TaskCenter.Entry entry : TaskCenter.getInstance().getCompletedEntries()) { - String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); - completedContainer.getChildren().add(createLineItem(text, null)); + completedContainer.getChildren().add(createHistoryItem(entry, true)); } } private void rebuildFailed() { failedContainer.getChildren().clear(); + if (TaskCenter.getInstance().getFailedEntries().isEmpty()) { + failedContainer.getChildren().add(failedEmpty); + return; + } for (TaskCenter.Entry entry : TaskCenter.getInstance().getFailedEntries()) { - String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); - failedContainer.getChildren().add(createLineItem(text, () -> { - Throwable ex = entry.getExecutor().getException(); - if (ex instanceof CancellationException) { - Controllers.dialog("任务由用户取消", entry.getTitle(), MessageDialogPane.MessageType.ERROR);//TODO: i18n - } else if (ex != null) { - Controllers.dialog(StringUtils.getStackTrace(ex), entry.getTitle(), MessageDialogPane.MessageType.ERROR); - } else { - Controllers.dialog("任务失败(无异常信息)", entry.getTitle(), MessageDialogPane.MessageType.ERROR);//TODO: i18n - } - })); + failedContainer.getChildren().add(createHistoryItem(entry, false)); } } - private Node createRunningItem(TaskCenter.Entry entry) { - HBox row = new HBox(12); - row.getStyleClass().add("md-list-cell"); - row.setPadding(new Insets(8, 12, 8, 12)); - row.setAlignment(Pos.CENTER_LEFT); + // ── running card ───────────────────────────────────────────────── - String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); - Label label = new Label(text); - HBox.setHgrow(label, Priority.ALWAYS); - label.setMaxWidth(Double.MAX_VALUE); + private Node createRunningCard(TaskCenter.Entry entry) { + VBox card = new VBox(6); + card.getStyleClass().add("card-non-transparent"); + card.setPadding(new Insets(10, 14, 10, 14)); + + HBox header = new HBox(8); + header.setAlignment(Pos.CENTER_LEFT); + + Label kindTag = createKindTag(entry.getKind()); + + String titleText = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); + Label titleLabel = new Label(titleText); + titleLabel.setStyle("-fx-font-size: 13px; -fx-font-weight: bold;"); + HBox.setHgrow(titleLabel, Priority.ALWAYS); + titleLabel.setMaxWidth(Double.MAX_VALUE); + + boolean isRunning = TaskCenter.getInstance().getRunningEntry() == entry; + Label statusLabel = new Label(isRunning ? i18n("task.status.running") : i18n("task.waiting")); + statusLabel.setStyle(isRunning + ? "-fx-text-fill: #2196F3; -fx-font-size: 11px;" + : "-fx-text-fill: -fx-secondary-text-color; -fx-font-size: 11px;"); JFXButton cancelButton = new JFXButton(i18n("button.cancel")); cancelButton.getStyleClass().add("dialog-cancel"); - cancelButton.setOnAction(e -> { - TaskCenter taskCenter = TaskCenter.getInstance(); - if (taskCenter.isStarted(entry.getExecutor())) { + TaskCenter tc = TaskCenter.getInstance(); + if (tc.isStarted(entry.getExecutor())) { entry.getExecutor().cancel(); } else { - taskCenter.cancelQueued(entry.getExecutor()); + tc.cancelQueued(entry.getExecutor()); } e.consume(); }); - cancelButton.setOnMouseClicked(e -> e.consume()); - row.getChildren().addAll(label, cancelButton); + header.getChildren().addAll(kindTag, titleLabel, statusLabel, cancelButton); + card.getChildren().add(header); + + if (isRunning) { + TaskListPane taskListPane = new TaskListPane(); + taskListPane.setExecutor(entry.getExecutor()); + taskListPane.setMaxHeight(120); + taskListPane.setPrefHeight(120); + card.getChildren().add(taskListPane); + } - row.setOnMouseClicked(e -> { + card.setOnMouseClicked(e -> { if (entry != TaskCenter.getInstance().getRunningEntry()) { - Controllers.dialog("任务等待中", entry.getTitle(), MessageDialogPane.MessageType.INFO);//TODO: i18n + Controllers.dialog(i18n("task.waiting"), entry.getTitle(), MessageDialogPane.MessageType.INFO); return; } - TaskExecutorDialogPane pane = Controllers.taskDialog(entry.getExecutor(), entry.getTitle(), TaskCancellationAction.NORMAL); pane.setEscAction(() -> pane.fireEvent(new DialogCloseEvent())); - pane.setCancelText("关闭");//TODO: i18n + pane.setCancelText(i18n("button.close")); pane.setCancelAction(() -> pane.fireEvent(new DialogCloseEvent())); pane.refreshTaskList(); }); - return row; + return card; } - private Node createLineItem(String text, Runnable onClick) { + // ── history item (completed / failed) ──────────────────────────── + + private Node createHistoryItem(TaskCenter.Entry entry, boolean success) { + HBox row = new HBox(8); + row.setPadding(new Insets(8, 12, 8, 12)); + row.setAlignment(Pos.CENTER_LEFT); + + Node icon = success + ? SVG.CHECK.createIcon(14) + : SVG.CLOSE.createIcon(14); + + Label kindTag = createKindTag(entry.getKind()); + + String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); Label label = new Label(text); - label.setPadding(new Insets(10, 12, 10, 12)); + label.setStyle("-fx-font-size: 12px;"); + HBox.setHgrow(label, Priority.ALWAYS); label.setMaxWidth(Double.MAX_VALUE); - label.setPrefWidth(Double.MAX_VALUE); - Separator separator = new Separator(); - separator.setMaxWidth(Double.MAX_VALUE); - separator.setPrefWidth(Double.MAX_VALUE); + row.getChildren().addAll(icon, kindTag, label); - VBox box = new VBox(label, separator); - box.setFillWidth(true); - if (onClick != null) { - box.setOnMouseClicked(e -> onClick.run()); + if (!success) { + row.setStyle("-fx-cursor: hand;"); + row.setOnMouseClicked(e -> { + Throwable ex = entry.getExecutor().getException(); + if (ex instanceof CancellationException) { + Controllers.dialog(i18n("task.cancelled"), entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } else if (ex != null) { + Controllers.dialog(StringUtils.getStackTrace(ex), entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } else { + Controllers.dialog(i18n("task.failed.no_exception"), entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } + }); } - return box; + + return row; + } + + // ── kind tag ───────────────────────────────────────────────────── + + private static Label createKindTag(TaskCenter.TaskKind kind) { + String text = switch (kind) { + case GAME_INSTALL -> i18n("task.kind.game_install"); + case MODPACK_INSTALL -> i18n("task.kind.modpack_install"); + case JAVA_DOWNLOAD -> i18n("task.kind.java_download"); + case MOD_UPDATE -> i18n("task.kind.mod_update"); + case OTHER -> i18n("task.kind.other"); + }; + Label tag = new Label(text); + tag.setStyle("-fx-background-color: -fx-base-color; -fx-background-radius: 3; " + + "-fx-padding: 1 6 1 6; -fx-font-size: 10px; -fx-text-fill: white;"); + return tag; } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index ed2e4a8d42..d1780994e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -196,14 +196,15 @@ public void saveAs(RemoteMod mod, RemoteMod.Version file) { return; } - Controllers.taskDialog( + Controllers.downloadTaskDialog( Task.composeAsync(() -> { var task = new FileDownloadTask(file.getFile().getUrl(), dest, file.getFile().getIntegrityCheck()); task.setName(file.getName()); return task; }), i18n("message.downloading"), - TaskCancellationAction.NORMAL); + TaskCancellationAction.NORMAL, + i18n("message.downloading") + " - " + file.getName()); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java index c8d8791f16..29344bcced 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java @@ -161,8 +161,8 @@ public void onStop(boolean success, TaskExecutor executor) { }); } }); - Controllers.taskDialog(executor, i18n("install.installer.install_offline"), TaskCancellationAction.NO_CANCEL); - executor.start(); + Controllers.downloadTaskDialog(executor, i18n("install.installer.install_offline"), TaskCancellationAction.NO_CANCEL, + i18n("task.detail.install_offline")); } private class InstallerListPageSkin extends ToolbarListPageSkin { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 3c738da352..d04b6f4ed4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -237,7 +237,7 @@ public void openModFolder() { public void checkUpdates(Collection mods) { Objects.requireNonNull(mods); - Runnable action = () -> Controllers.taskDialog(Task + Runnable action = () -> Controllers.downloadTaskDialog(Task .composeAsync(() -> { Optional gameVersion = profile.getRepository().getGameVersion(instanceId); if (gameVersion.isPresent()) { @@ -256,7 +256,8 @@ public void checkUpdates(Collection mods) { } }) .withStagesHints("update.checking"), - i18n("mods.check_updates"), TaskCancellationAction.NORMAL); + i18n("mods.check_updates"), TaskCancellationAction.NORMAL, + i18n("task.detail.mod_check_updates")); if (profile.getRepository().isModpack(instanceId)) { Controllers.confirm( diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index c0c0a2f46f..bb0d0a65ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -136,7 +136,7 @@ private void updateMods() { .filter(o -> o.enabled.get()) .map(object -> pair(object.data.getLocalMod(), object.data.getCandidate())) .collect(Collectors.toList())); - Controllers.taskDialog( + Controllers.downloadTaskDialog( task.whenComplete(Schedulers.javafx(), exception -> { fireEvent(new PageCloseEvent()); if (!task.getFailedMods().isEmpty()) { @@ -151,7 +151,8 @@ private void updateMods() { } }), i18n("mods.check_updates"), - TaskCancellationAction.NORMAL); + TaskCancellationAction.NORMAL, + i18n("task.detail.mod_update")); } private void exportList() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index 6f52a2b731..dc056ac67e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -32,6 +32,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.task.TaskCenter; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.account.CreateAccountPane; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; @@ -85,7 +86,7 @@ public static void downloadModpackImpl(DownloadProvider downloadProvider, Profil i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); return; } - Controllers.taskDialog( + Controllers.downloadTaskDialog( new FileDownloadTask(downloadURLs, modpack) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { @@ -148,7 +149,10 @@ public static CompletableFuture renameVersion(Profile profile, String ve handler.reject(i18n("version.manage.rename.fail")); } }, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), - new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version))); + new Validator(i18n("install.new_game.already_exists"), newVersionName -> + (!profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)) + && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.GAME_INSTALL, newVersionName) + && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.MODPACK_INSTALL, newVersionName))); } public static void exportVersion(Profile profile, String version) { @@ -180,7 +184,10 @@ public static void duplicateVersion(Profile profile, String version) { .addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm"))) .addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), - new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName)))) + new Validator(i18n("install.new_game.already_exists"), newVersionName -> + !profile.getRepository().versionIdConflicts(newVersionName) + && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.GAME_INSTALL, newVersionName) + && !TaskCenter.getInstance().hasQueuedInstallName(TaskCenter.TaskKind.MODPACK_INSTALL, newVersionName)))) .addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false))); } @@ -191,8 +198,8 @@ public static void updateVersion(Profile profile, String version) { public static void updateGameAssets(Profile profile, String version) { TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true) .executor(); - Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL); - executor.start(); + Controllers.downloadTaskDialog(executor, i18n("version.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL, + i18n("task.detail.redownload_assets", version)); } public static void cleanVersion(Profile profile, String id) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java index 0a93925726..476aacd16f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java @@ -128,7 +128,7 @@ protected Skin createDefaultSkin() { } void createBackup() { - Controllers.taskDialog(new WorldBackupTask(world, backupsDir, false).setName(i18n("world.backup.processing")).thenApplyAsync(path -> { + Controllers.downloadTaskDialog(new WorldBackupTask(world, backupsDir, false).setName(i18n("world.backup.processing")).thenApplyAsync(path -> { Matcher matcher = backupFileNamePattern.matcher(path.getFileName().toString()); if (!matcher.matches()) { throw new AssertionError("Wrong backup file name" + path); @@ -153,7 +153,8 @@ void createBackup() { LOG.warning("Failed to create backup", exception); Controllers.dialog(i18n("world.backup.create.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING); } - }), i18n("world.backup"), null); + }), i18n("world.backup"), null, + i18n("task.detail.world_backup", world.getWorldName())); } private final class WorldBackupsPageSkin extends ToolbarListPageSkin { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java index d7fee2c151..660f922f06 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java @@ -28,6 +28,8 @@ import java.nio.file.Path; import java.nio.file.Paths; +import org.jackhuang.hmcl.util.SettingsMap; + import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class WorldExportPage extends WizardSinglePage { @@ -73,6 +75,12 @@ public String getTitle() { return i18n("world.export.wizard", world.getFileName()); } + @Override + protected void onFinishSettings(SettingsMap settings) { + settings.put("backgroundable", true); + settings.put("task_detail", i18n("task.detail.world_export", worldName.get())); + } + @Override protected Object finish() { return Task.runAsync(i18n("world.export.wizard", worldName.get()), () -> world.export(Paths.get(path.get()), worldName.get())); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java index c30e41affe..f608867625 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java @@ -37,6 +37,7 @@ public void start(SettingsMap settings) { @Override public Object finish(SettingsMap settings) { + page.onFinishSettings(settings); return page.finish(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index b04a83bcc1..0ead83bea8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -111,7 +111,6 @@ else if (!settings.containsKey("forbid_failure_message")) pane.setBackgroundAction(() -> { TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); - pane.refreshTaskList(); boolean returnToDownloadList = Boolean.TRUE.equals(settings.get("return_to_download_list")); onEnd(); @@ -122,17 +121,12 @@ else if (!settings.containsKey("forbid_failure_message")) pane.fireEvent(new DialogCloseEvent()); }); - - TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); - pane.refreshTaskList(); } Controllers.dialog(pane); - if (!backgroundable) { - executor.start(); - pane.refreshTaskList(); - } + executor.start(); + pane.refreshTaskList(); }); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java index d6c2673c5f..7850f8f81b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java @@ -29,6 +29,13 @@ protected WizardSinglePage(Runnable onFinish) { protected abstract Object finish(); + /** + * Called before {@link #finish()} to allow subclasses to inject settings + * (e.g. backgroundable, task_detail) into the wizard settings map. + */ + protected void onFinishSettings(SettingsMap settings) { + } + @Override public void cleanup(SettingsMap settings) { } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 21226528d1..0864956ed3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1492,13 +1492,38 @@ sponsor.hmcl=Hello Minecraft! Launcher is a FOSS Minecraft launcher that allows system.architecture=Architecture system.operating_system=Operating System +button.close=Close +task.cancelled=Task cancelled by user +task.clear=Clear task.completed=Completed +task.detail.game_install=Quick Install Game +task.detail.install_offline=Install Offline Installer +task.detail.java_download=Download Java Runtime +task.detail.mod_check_updates=Check Mod Updates +task.detail.mod_update=Update Mods +task.detail.modpack_export=Export Modpack - %s +task.detail.modpack_install=Install Modpack +task.detail.redownload_assets=Redownload Assets - %s +task.detail.world_backup=Backup World - %s +task.detail.world_export=Export World - %s +task.empty.completed=No completed tasks +task.empty.failed=No failed tasks +task.empty.running=No running tasks task.failed=Failed +task.failed.no_exception=Task failed (no exception details) +task.kind.game_install=Game +task.kind.java_download=Java +task.kind.mod_update=Mod +task.kind.modpack_install=Modpack +task.kind.other=Task task.manage=Task Manager task.move_to_background=Move Task to Background task.running=Running Tasks -task.settings=Task Settings +task.status.running=Running +task.toast.failed=Task failed: %s +task.toast.success=Task completed: %s task.unnamed=Unnamed Task +task.waiting=Task is waiting in queue terracotta=Multiplayer terracotta.terracotta=Terracotta | Multiplayer terracotta.status=Lobby diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0f7d9c6633..b76e264468 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1277,6 +1277,39 @@ sponsor.hmcl=Hello Minecraft! Launcher 是一個免費、自由、開源的 Mine system.architecture=架構 system.operating_system=作業系統 +button.close=關閉 +task.cancelled=任務由使用者取消 +task.clear=清空 +task.completed=已完成 +task.detail.game_install=快速安裝遊戲 +task.detail.install_offline=安裝離線安裝器 +task.detail.java_download=下載 Java 執行環境 +task.detail.mod_check_updates=檢查模組更新 +task.detail.mod_update=更新模組 +task.detail.modpack_export=匯出整合包 - %s +task.detail.modpack_install=安裝整合包 +task.detail.redownload_assets=重新下載資源 - %s +task.detail.world_backup=備份世界 - %s +task.detail.world_export=匯出世界 - %s +task.empty.completed=暫無已完成的任務 +task.empty.failed=暫無失敗的任務 +task.empty.running=暫無正在執行的任務 +task.failed=失敗 +task.failed.no_exception=任務失敗(無異常資訊) +task.kind.game_install=遊戲 +task.kind.java_download=Java +task.kind.mod_update=模組 +task.kind.modpack_install=整合包 +task.kind.other=任務 +task.manage=任務管理器 +task.move_to_background=將任務移至後臺 +task.running=正在執行的任務 +task.status.running=執行中 +task.toast.failed=任務失敗:%s +task.toast.success=任務完成:%s +task.unnamed=未命名任務 +task.waiting=任務正在佇列中等待 + terracotta=多人遊戲 terracotta.terracotta=Terracotta | 陶瓦聯機 terracotta.status=聯機大廳 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 9e408399c3..eafbeb4e7c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1282,6 +1282,39 @@ sponsor.hmcl=Hello Minecraft! Launcher 是一个免费、自由、开放源代 system.architecture=架构 system.operating_system=操作系统 +button.close=关闭 +task.cancelled=任务由用户取消 +task.clear=清空 +task.completed=已完成 +task.detail.game_install=快速安装游戏 +task.detail.install_offline=安装离线安装器 +task.detail.java_download=下载 Java 运行时 +task.detail.mod_check_updates=检查模组更新 +task.detail.mod_update=更新模组 +task.detail.modpack_export=导出整合包 - %s +task.detail.modpack_install=安装整合包 +task.detail.redownload_assets=重新下载资源 - %s +task.detail.world_backup=备份世界 - %s +task.detail.world_export=导出世界 - %s +task.empty.completed=暂无已完成的任务 +task.empty.failed=暂无失败的任务 +task.empty.running=暂无正在运行的任务 +task.failed=失败 +task.failed.no_exception=任务失败(无异常信息) +task.kind.game_install=游戏 +task.kind.java_download=Java +task.kind.mod_update=模组 +task.kind.modpack_install=整合包 +task.kind.other=任务 +task.manage=任务管理器 +task.move_to_background=将任务移至后台 +task.running=正在运行的任务 +task.status.running=运行中 +task.toast.failed=任务失败:%s +task.toast.success=任务完成:%s +task.unnamed=未命名任务 +task.waiting=任务正在队列中等待 + terracotta=多人联机 terracotta.terracotta=Terracotta | 陶瓦联机 terracotta.status=联机大厅 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java index 526b0a90e0..7df8e1df80 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java @@ -33,7 +33,7 @@ */ public final class AsyncTaskExecutor extends TaskExecutor { - private CompletableFuture future; + private volatile CompletableFuture future; public AsyncTaskExecutor(Task task) { super(task); From 7f9eb7a927a09ea789fdb945ba7015352e23cafc Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 21 Mar 2026 10:36:54 +0800 Subject: [PATCH 22/28] =?UTF-8?q?=E4=BF=AE=E6=AD=A3i18n=E5=86=85=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=8B=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index b76e264468..5679bde553 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1295,7 +1295,7 @@ task.empty.completed=暫無已完成的任務 task.empty.failed=暫無失敗的任務 task.empty.running=暫無正在執行的任務 task.failed=失敗 -task.failed.no_exception=任務失敗(無異常資訊) +task.failed.no_exception=任務失敗(無異常資訊) task.kind.game_install=遊戲 task.kind.java_download=Java task.kind.mod_update=模組 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 eafbeb4e7c..be30182dec 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1300,7 +1300,7 @@ task.empty.completed=暂无已完成的任务 task.empty.failed=暂无失败的任务 task.empty.running=暂无正在运行的任务 task.failed=失败 -task.failed.no_exception=任务失败(无异常信息) +task.failed.no_exception=任务失败(无异常信息) task.kind.game_install=游戏 task.kind.java_download=Java task.kind.mod_update=模组 From 29db22908d201f7a9ed9de5ae461b9ad45b345d6 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 21 Mar 2026 10:47:33 +0800 Subject: [PATCH 23/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20executor=20=E5=8F=8C?= =?UTF-8?q?=E9=87=8D=20start=20=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E6=B8=85=E9=99=A4=E4=B8=80=E9=83=A8=E5=88=86=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=B9=B6=E4=BF=AE=E5=A4=8D=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/construct/TaskExecutorDialogPane.java | 46 ++----------------- .../jackhuang/hmcl/ui/task/TaskCenter.java | 6 ++- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index f1a1599b72..39d41932d7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -35,7 +35,6 @@ import org.jetbrains.annotations.NotNull; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.Optional; import java.util.function.Consumer; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -55,6 +54,7 @@ public class TaskExecutorDialogPane extends BorderPane { private final JFXButton btnBackground; private Runnable onBackground; private Runnable escAction; + private Runnable cancelAction; public void setEscAction(Runnable action) { this.escAction = action; @@ -62,7 +62,6 @@ public void setEscAction(Runnable action) { public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { this.getStyleClass().add("task-executor-dialog-layout"); - cancelAction = null; FXUtils.setLimitWidth(this, 500); FXUtils.setLimitHeight(this, 300); @@ -116,6 +115,10 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { setCancel(cancel); btnCancel.setOnAction(e -> { + if (cancelAction != null) { + cancelAction.run(); + return; + } if (onCancel.getCancellationAction() != null) { if (executor != null) executor.cancel(); @@ -136,41 +139,6 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { }); } - private boolean isQueuedNotStarted() { - if (executor == null) { - return false; - } - if (executor instanceof AsyncTaskExecutor asyncExecutor) { - return !asyncExecutor.isStarted(); - } - return false; - } - - private void handleCancelOrClose() { - if (isQueuedNotStarted()) { - fireEvent(new DialogCloseEvent()); - return; - } - - if (cancelAction != null) { - cancelAction.run(); - return; - } - - Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel); - if (onCancel.getCancellationAction() != null) { - onCancel.getCancellationAction().accept(this); - } - } - - private void applyQueuedStateIfNeeded() { - if (isQueuedNotStarted()) { - setCancelText(i18n("button.close")); - } else { - setCancelText(i18n("button.cancel")); - } - } - public void setExecutor(TaskExecutor executor) { setExecutor(executor, true); } @@ -189,7 +157,6 @@ public void onStop(boolean success, TaskExecutor executor) { } }); } - applyQueuedStateIfNeeded(); } public StringProperty titleProperty() { @@ -236,11 +203,8 @@ public void setBackgroundAction(Runnable action) { public void refreshTaskList() { taskListPane.refresh(); - applyQueuedStateIfNeeded(); } - private Runnable cancelAction; - public void setCancelAction(Runnable action) { this.cancelAction = action; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index fc981ca031..22788c8735 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -24,6 +24,7 @@ import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskListener; import org.jackhuang.hmcl.ui.Controllers; @@ -156,7 +157,10 @@ public void onStop(boolean success, TaskExecutor executor) { } }); - executor.start(); + // Only start if not already running externally (e.g. started by downloadTaskDialog before enqueue) + if (!(executor instanceof AsyncTaskExecutor ate && ate.isStarted())) { + executor.start(); + } return; } } From f0d9dc7fac439ea650aa5ef533d24e0ad3bf5488 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 21 Mar 2026 11:00:04 +0800 Subject: [PATCH 24/28] =?UTF-8?q?=E9=80=82=E9=85=8Di18n=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9SVG=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 1 + .../hmcl/ui/download/DownloadPage.java | 29 ++++++------------- .../ModpackInstallWizardProvider.java | 2 +- .../ui/download/ModpackSelectionPage.java | 2 +- .../UpdateInstallerWizardProvider.java | 4 +-- .../VanillaInstallWizardProvider.java | 2 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 2 +- .../jackhuang/hmcl/ui/versions/Versions.java | 2 +- .../resources/assets/lang/I18N.properties | 9 ++++++ .../resources/assets/lang/I18N_zh.properties | 9 ++++++ .../assets/lang/I18N_zh_CN.properties | 9 ++++++ 11 files changed, 44 insertions(+), 27 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index c7a99881db..fd19569d51 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -43,6 +43,7 @@ public enum SVG { CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"), CHECK("M9.55 18 3.85 12.3 5.275 10.875 9.55 15.15 18.725 5.975 20.15 7.4 9.55 18Z"), CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"), + CHECKLIST("M5.55 19.5 1.85 15.8 3.275 14.35 5.55 16.625 10.15 12.025 11.575 13.475 5.55 19.5ZM5.55 11.5 1.85 7.8 3.275 6.35 5.55 8.625 10.15 4.025 11.575 5.475 5.55 11.5ZM13 17V15H22V17H13ZM13 9V7H22V9H13Z"), CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"), CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"), CONTENT_CUT("M19 21l-7-7-2.35 2.35q.2.375.275.8T10 18q0 1.65-1.175 2.825T6 22q-1.65 0-2.825-1.175T2 18t1.175-2.825T6 14q.425 0 .85.075t.8.275L10 12 7.65 9.65q-.375.2-.8.275T6 10q-1.65 0-2.825-1.175T2 6q0-1.65 1.175-2.825T6 2q1.65 0 2.825 1.175T10 6q0 .425-.075.85t-.275.8L22 20v1H19Zm-4-10-2-2 6-6h3v1l-7 7ZM7.4125 7.4125Q8 6.825 8 6t-.5875-1.4125Q6.825 4 6 4t-1.4125.5875Q4 5.175 4 6t.5875 1.4125T6 8t1.4125-.5875ZM12.35 12.35q.15-.15.15-.35t-.15-.35-.35-.15-.35.15-.15.35.15.35.35.15.35-.15ZM7.4125 19.4125Q8 18.825 8 18t-.5875-1.4125Q6.825 16 6 16t-1.4125.5875Q4 17.175 4 18t.5875 1.4125Q5.175 20 6 20t1.4125-.5875Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 0270810280..20a744f092 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -135,24 +135,13 @@ public static void download(DownloadProvider downloadProvider, Profile profile, Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory(); - String detailPrefix; - switch (subdirectoryName) { - case "mods": - detailPrefix = "安装模组";//TODO i18n - break; - case "resourcepacks": - detailPrefix = "安装资源包";//TODO i18n - break; - case "shaderpacks": - detailPrefix = "安装光影";//TODO i18n - break; - case "saves": - detailPrefix = "安装世界";//TODO i18n - break; - default: - detailPrefix = "下载";//TODO i18n - break; - } + String detailKey = switch (subdirectoryName) { + case "mods" -> "task.detail.install_mod"; + case "resourcepacks" -> "task.detail.install_resourcepack"; + case "shaderpacks" -> "task.detail.install_shaderpack"; + case "saves" -> "task.detail.install_world"; + default -> "task.detail.download"; + }; Controllers.prompt(i18n("archive.file.name"), (result, handler) -> { Path dest = runDirectory.resolve(subdirectoryName).resolve(result); @@ -173,7 +162,7 @@ public static void download(DownloadProvider downloadProvider, Profile profile, Controllers.showToast(i18n("install.success")); } }), i18n("message.downloading"), TaskCancellationAction.NORMAL, - detailPrefix + "-[" + file.getName() + "]"); + i18n(detailKey, file.getName())); handler.resolve(); }, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); @@ -323,7 +312,7 @@ public Object finish(SettingsMap settings) { settings.put("success_message", i18n("install.success")); settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); - settings.put("task_detail", "安装游戏-[" + settings.get("name") + "]");//TODO i18n + settings.put("task_detail", i18n("task.detail.install_game", (String) settings.get("name"))); settings.put("backgroundable", true); settings.put("return_to_download_list", true); settings.put("task_kind", TaskCenter.TaskKind.GAME_INSTALL); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index 23ab06c7a2..e78e53b3e5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -147,7 +147,7 @@ public Object finish(SettingsMap settings) { String taskName = settings.get(LocalModpackPage.MODPACK_NAME); if (taskName != null) { - settings.put("task_detail", "安装整合包-[" + taskName + "]");//TODO: i18n + settings.put("task_detail", i18n("task.detail.install_modpack", taskName)); } settings.put("backgroundable", true); settings.put("task_kind", TaskCenter.TaskKind.MODPACK_INSTALL); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index d5ccb44ac5..c49640cbff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -157,7 +157,7 @@ private void onChooseRemoteFile() { }), i18n("message.downloading"), TaskCancellationAction.NORMAL, - "安装整合包-[" + modpack.getFileName() + "]"//TODO: i18n + i18n("task.detail.install_modpack", modpack.getFileName().toString()) ); } } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index 2faad425cd..1670e34736 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -77,12 +77,12 @@ public Object finish(SettingsMap settings) { String detail = null; for (Object value : settings.asStringMap().values()) { if (value instanceof RemoteVersion remoteVersion) { - detail = "安装" + remoteVersion.getLibraryId() + "-[" + remoteVersion.getSelfVersion() + "]";//TODO i18n + detail = i18n("task.detail.install_library", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()); break; } } if (detail == null) { - detail = "安装" + libraryId;//TODO i18n + detail = i18n("task.detail.install_library.fallback", libraryId); } settings.put("task_detail", detail); settings.put("backgroundable", true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java index 17a04e0ca7..4d86665e5b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java @@ -72,7 +72,7 @@ public Object finish(SettingsMap settings) { settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next)); String name = (String) settings.get("name"); - settings.put("task_detail", "安装游戏-[" + name + "]");//TODO i18n + settings.put("task_detail", i18n("task.detail.install_game", name)); settings.put("backgroundable", true); settings.put("task_kind", TaskCenter.TaskKind.GAME_INSTALL); settings.put("task_name", name); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 85e8535759..231ae8f3bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -193,7 +193,7 @@ protected Skin(RootPage control) { } AdvancedListItem taskManagerItem = new AdvancedListItem(); - taskManagerItem.setLeftIcon(SVG.LIST); //TODO SVG待更换 + taskManagerItem.setLeftIcon(SVG.CHECKLIST); taskManagerItem.setTitle(i18n("task.manage")); taskManagerItem.setOnAction(e -> { Controllers.navigate(new TaskCenterPage()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index dc056ac67e..b27d672529 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -108,7 +108,7 @@ public static void downloadModpackImpl(DownloadProvider downloadProvider, Profil }), i18n("message.downloading"), TaskCancellationAction.NORMAL, - "安装整合包-[" + modpack.getFileName() + "]"//TODO:i18n + i18n("task.detail.install_modpack", modpack.getFileName().toString()) ); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 0864956ed3..65fd6d9c73 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1496,8 +1496,17 @@ button.close=Close task.cancelled=Task cancelled by user task.clear=Clear task.completed=Completed +task.detail.download=Download - %s task.detail.game_install=Quick Install Game +task.detail.install_game=Install Game - %s +task.detail.install_library=Install %1$s - %2$s +task.detail.install_library.fallback=Install %s +task.detail.install_mod=Install Mod - %s +task.detail.install_modpack=Install Modpack - %s task.detail.install_offline=Install Offline Installer +task.detail.install_resourcepack=Install Resource Pack - %s +task.detail.install_shaderpack=Install Shader Pack - %s +task.detail.install_world=Install World - %s task.detail.java_download=Download Java Runtime task.detail.mod_check_updates=Check Mod Updates task.detail.mod_update=Update Mods diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5679bde553..30595389da 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1281,8 +1281,17 @@ button.close=關閉 task.cancelled=任務由使用者取消 task.clear=清空 task.completed=已完成 +task.detail.download=下載 - %s task.detail.game_install=快速安裝遊戲 +task.detail.install_game=安裝遊戲 - %s +task.detail.install_library=安裝%1$s - %2$s +task.detail.install_library.fallback=安裝%s +task.detail.install_mod=安裝模組 - %s +task.detail.install_modpack=安裝整合包 - %s task.detail.install_offline=安裝離線安裝器 +task.detail.install_resourcepack=安裝資源包 - %s +task.detail.install_shaderpack=安裝光影 - %s +task.detail.install_world=安裝世界 - %s task.detail.java_download=下載 Java 執行環境 task.detail.mod_check_updates=檢查模組更新 task.detail.mod_update=更新模組 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 be30182dec..310f93a7bb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1286,8 +1286,17 @@ button.close=关闭 task.cancelled=任务由用户取消 task.clear=清空 task.completed=已完成 +task.detail.download=下载 - %s task.detail.game_install=快速安装游戏 +task.detail.install_game=安装游戏 - %s +task.detail.install_library=安装%1$s - %2$s +task.detail.install_library.fallback=安装%s +task.detail.install_mod=安装模组 - %s +task.detail.install_modpack=安装整合包 - %s task.detail.install_offline=安装离线安装器 +task.detail.install_resourcepack=安装资源包 - %s +task.detail.install_shaderpack=安装光影 - %s +task.detail.install_world=安装世界 - %s task.detail.java_download=下载 Java 运行时 task.detail.mod_check_updates=检查模组更新 task.detail.mod_update=更新模组 From 93667fc42483dd8f942aea7867389c0a2dcf6a8a Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 21 Mar 2026 21:00:25 +0800 Subject: [PATCH 25/28] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=90=8E=E5=9C=A8=E5=A0=86=E6=A0=88=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E9=A1=B5=E9=9D=A2=E7=9B=B4=E6=8E=A5=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=99=A8=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/main/SettingsPage.java | 8 +- .../jackhuang/hmcl/ui/task/TaskCenter.java | 4 + .../hmcl/ui/task/TaskCenterPage.java | 95 ++++++++++++------- HMCL/src/main/resources/assets/css/root.css | 9 ++ 4 files changed, 76 insertions(+), 40 deletions(-) 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 40012b4426..1919609180 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 @@ -302,7 +302,7 @@ else if (locale.isSameLanguage(currentLocale)) openLogFolderButton.setDisable(true); JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export")); - logButton.setOnAction(e -> onExportLogs()); + logButton.setOnAction(e -> exportLogs()); HBox buttonBox = new HBox(); buttonBox.setSpacing(10); @@ -329,7 +329,7 @@ private void onUpdate() { UpdateHandler.updateFrom(target); } - private static String getEntryName(Set entryNames, String name) { + public static String getEntryName(Set entryNames, String name) { if (entryNames.add(name)) { return name; } @@ -347,7 +347,7 @@ private static String getEntryName(Set entryNames, String name) { /// If no exception occurs, this method returns `true`; /// If an exception occurs while reading from `input`, this method returns `false`; /// If an exception occurs while writing to `output`, this method will throw it as is. - private static boolean exportLogFile(ZipOutputStream output, + public static boolean exportLogFile(ZipOutputStream output, Path file, // For logging String entryName, InputStream input, @@ -378,7 +378,7 @@ private static boolean exportLogFile(ZipOutputStream output, } } - private void onExportLogs() { + public static void exportLogs() { thread(() -> { String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")); List recentLogFiles = LOG.findRecentLogFiles(5); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index 22788c8735..73f9e858e4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -21,6 +21,7 @@ import java.util.Deque; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CancellationException; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -185,6 +186,9 @@ private void onTaskStopped(TaskExecutor executor, boolean success) { Controllers.showToast(i18n("task.toast.success", detail)); } else { Controllers.showToast(i18n("task.toast.failed", detail)); + if (!(stoppedEntry.getExecutor().getException() instanceof CancellationException)) { + TaskCenterPage.showFailedTaskDialog(stoppedEntry); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 05ef35b2af..ca45d9b60c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -34,12 +34,14 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.main.SettingsPage; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import java.util.Locale; import java.util.concurrent.CancellationException; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class TaskCenterPage extends DecoratorAnimatedPage implements DecoratorPage { @@ -53,8 +55,8 @@ public final class TaskCenterPage extends DecoratorAnimatedPage implements Decor private final TabHeader.Tab failedTab = new TabHeader.Tab<>("taskFailedTab"); private final VBox runningContainer = new VBox(10); - private final VBox completedContainer = new VBox(2); - private final VBox failedContainer = new VBox(2); + private final VBox completedContainer = new VBox(8); + private final VBox failedContainer = new VBox(8); private final Label runningEmpty = new Label(i18n("task.empty.running")); private final Label completedEmpty = new Label(i18n("task.empty.completed")); @@ -100,32 +102,49 @@ public TaskCenterPage() { private static void styleEmptyLabel(Label label) { label.setStyle("-fx-text-fill: -fx-secondary-text-color; -fx-font-size: 13px;"); - label.setPadding(new Insets(24)); + } + + private static StackPane createCenteredEmpty(Label label) { + StackPane pane = new StackPane(label); + pane.setAlignment(Pos.CENTER); + VBox.setVgrow(pane, Priority.ALWAYS); + return pane; } private ScrollPane createRunningPane() { ScrollPane scrollPane = new ScrollPane(runningContainer); scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); runningContainer.setPadding(new Insets(12)); rebuildRunning(); return scrollPane; } + private static HBox createClearToolbar(Runnable onClear) { + HBox toolbar = new HBox(); + toolbar.setAlignment(Pos.CENTER_LEFT); + toolbar.setPickOnBounds(false); + toolbar.setStyle("-fx-border-color: -monet-outline-variant; -fx-border-width: 0 0 1 0;"); + toolbar.setPadding(new Insets(0, 0, 4, 0)); + toolbar.getChildren().setAll( + createToolbarButton2(i18n("task.clear"), SVG.DELETE, onClear) + ); + return toolbar; + } + private ScrollPane createCompletedPane() { VBox wrapper = new VBox(8); wrapper.setPadding(new Insets(12)); - JFXButton clearButton = new JFXButton(i18n("task.clear")); - clearButton.getStyleClass().add("dialog-cancel"); - clearButton.setOnAction(e -> TaskCenter.getInstance().getCompletedEntries().clear()); - - HBox toolbar = new HBox(clearButton); - toolbar.setAlignment(Pos.CENTER_RIGHT); + HBox toolbar = createClearToolbar(() -> + TaskCenter.getInstance().getCompletedEntries().clear()); wrapper.getChildren().addAll(toolbar, completedContainer); + VBox.setVgrow(completedContainer, Priority.ALWAYS); ScrollPane scrollPane = new ScrollPane(wrapper); scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); rebuildCompleted(); return scrollPane; } @@ -134,17 +153,15 @@ private ScrollPane createFailedPane() { VBox wrapper = new VBox(8); wrapper.setPadding(new Insets(12)); - JFXButton clearButton = new JFXButton(i18n("task.clear")); - clearButton.getStyleClass().add("dialog-cancel"); - clearButton.setOnAction(e -> TaskCenter.getInstance().getFailedEntries().clear()); - - HBox toolbar = new HBox(clearButton); - toolbar.setAlignment(Pos.CENTER_RIGHT); + HBox toolbar = createClearToolbar(() -> + TaskCenter.getInstance().getFailedEntries().clear()); wrapper.getChildren().addAll(toolbar, failedContainer); + VBox.setVgrow(failedContainer, Priority.ALWAYS); ScrollPane scrollPane = new ScrollPane(wrapper); scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); rebuildFailed(); return scrollPane; } @@ -154,7 +171,7 @@ private ScrollPane createFailedPane() { private void rebuildRunning() { runningContainer.getChildren().clear(); if (TaskCenter.getInstance().getEntries().isEmpty()) { - runningContainer.getChildren().add(runningEmpty); + runningContainer.getChildren().add(createCenteredEmpty(runningEmpty)); return; } for (TaskCenter.Entry entry : TaskCenter.getInstance().getEntries()) { @@ -165,7 +182,7 @@ private void rebuildRunning() { private void rebuildCompleted() { completedContainer.getChildren().clear(); if (TaskCenter.getInstance().getCompletedEntries().isEmpty()) { - completedContainer.getChildren().add(completedEmpty); + completedContainer.getChildren().add(createCenteredEmpty(completedEmpty)); return; } for (TaskCenter.Entry entry : TaskCenter.getInstance().getCompletedEntries()) { @@ -176,7 +193,7 @@ private void rebuildCompleted() { private void rebuildFailed() { failedContainer.getChildren().clear(); if (TaskCenter.getInstance().getFailedEntries().isEmpty()) { - failedContainer.getChildren().add(failedEmpty); + failedContainer.getChildren().add(createCenteredEmpty(failedEmpty)); return; } for (TaskCenter.Entry entry : TaskCenter.getInstance().getFailedEntries()) { @@ -188,8 +205,8 @@ private void rebuildFailed() { private Node createRunningCard(TaskCenter.Entry entry) { VBox card = new VBox(6); - card.getStyleClass().add("card-non-transparent"); card.setPadding(new Insets(10, 14, 10, 14)); + card.setStyle("-fx-border-color: -monet-outline-variant; -fx-border-width: 0 0 1 0;"); HBox header = new HBox(8); header.setAlignment(Pos.CENTER_LEFT); @@ -198,15 +215,13 @@ private Node createRunningCard(TaskCenter.Entry entry) { String titleText = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); Label titleLabel = new Label(titleText); - titleLabel.setStyle("-fx-font-size: 13px; -fx-font-weight: bold;"); + titleLabel.getStyleClass().add("title-label"); HBox.setHgrow(titleLabel, Priority.ALWAYS); titleLabel.setMaxWidth(Double.MAX_VALUE); boolean isRunning = TaskCenter.getInstance().getRunningEntry() == entry; Label statusLabel = new Label(isRunning ? i18n("task.status.running") : i18n("task.waiting")); - statusLabel.setStyle(isRunning - ? "-fx-text-fill: #2196F3; -fx-font-size: 11px;" - : "-fx-text-fill: -fx-secondary-text-color; -fx-font-size: 11px;"); + statusLabel.getStyleClass().add("subtitle-label"); JFXButton cancelButton = new JFXButton(i18n("button.cancel")); cancelButton.getStyleClass().add("dialog-cancel"); @@ -253,6 +268,7 @@ private Node createHistoryItem(TaskCenter.Entry entry, boolean success) { HBox row = new HBox(8); row.setPadding(new Insets(8, 12, 8, 12)); row.setAlignment(Pos.CENTER_LEFT); + row.getStyleClass().add("md-list-cell"); Node icon = success ? SVG.CHECK.createIcon(14) @@ -262,7 +278,7 @@ private Node createHistoryItem(TaskCenter.Entry entry, boolean success) { String text = entry.getDetail() != null ? entry.getDetail() : entry.getTitle(); Label label = new Label(text); - label.setStyle("-fx-font-size: 12px;"); + label.getStyleClass().add("subtitle-label"); HBox.setHgrow(label, Priority.ALWAYS); label.setMaxWidth(Double.MAX_VALUE); @@ -270,21 +286,29 @@ private Node createHistoryItem(TaskCenter.Entry entry, boolean success) { if (!success) { row.setStyle("-fx-cursor: hand;"); - row.setOnMouseClicked(e -> { - Throwable ex = entry.getExecutor().getException(); - if (ex instanceof CancellationException) { - Controllers.dialog(i18n("task.cancelled"), entry.getTitle(), MessageDialogPane.MessageType.ERROR); - } else if (ex != null) { - Controllers.dialog(StringUtils.getStackTrace(ex), entry.getTitle(), MessageDialogPane.MessageType.ERROR); - } else { - Controllers.dialog(i18n("task.failed.no_exception"), entry.getTitle(), MessageDialogPane.MessageType.ERROR); - } - }); + row.setOnMouseClicked(e -> showFailedTaskDialog(entry)); } return row; } + // ── failed task dialog ───────────────────────────────────────────── + + static void showFailedTaskDialog(TaskCenter.Entry entry) { + Throwable ex = entry.getExecutor().getException(); + if (ex instanceof CancellationException) { + Controllers.dialog(i18n("task.cancelled"), entry.getTitle(), MessageDialogPane.MessageType.ERROR); + } else { + String message = ex != null + ? StringUtils.getStackTrace(ex) + : i18n("task.failed.no_exception"); + Controllers.dialog(new MessageDialogPane.Builder(message, entry.getTitle(), MessageDialogPane.MessageType.ERROR) + .addAction(i18n("settings.launcher.launcher_log.export"), SettingsPage::exportLogs) + .ok(null) + .build()); + } + } + // ── kind tag ───────────────────────────────────────────────────── private static Label createKindTag(TaskCenter.TaskKind kind) { @@ -296,8 +320,7 @@ private static Label createKindTag(TaskCenter.TaskKind kind) { case OTHER -> i18n("task.kind.other"); }; Label tag = new Label(text); - tag.setStyle("-fx-background-color: -fx-base-color; -fx-background-radius: 3; " - + "-fx-padding: 1 6 1 6; -fx-font-size: 10px; -fx-text-fill: white;"); + tag.getStyleClass().add("tag"); return tag; } diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 5b035f97f6..f619b4cf16 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -331,6 +331,15 @@ -fx-pref-height: 0; } +.tag { + -fx-text-fill: -monet-on-secondary-container; + -fx-background-color: -monet-secondary-container; + -fx-background-radius: 2; + -fx-padding: 2; + -fx-font-weight: normal; + -fx-font-size: 12px; +} + .two-line-list-item > .first-line .tag { -fx-text-fill: -monet-on-secondary-container; -fx-background-color: -monet-secondary-container; From 7007ac1bed70b7186a6ac4386f56e295d4da5857 Mon Sep 17 00:00:00 2001 From: lokins Date: Sat, 21 Mar 2026 23:05:51 +0800 Subject: [PATCH 26/28] =?UTF-8?q?=E6=9B=B4=E6=96=B0UI=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E9=87=8D=E5=90=8D=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0tooltip=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B9=E2=80=9C=E8=87=AA=E5=8A=A8=E7=A7=BB?= =?UTF-8?q?=E4=BA=A4=E5=90=8E=E5=8F=B0=E4=BB=BB=E5=8A=A1=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/setting/Config.java | 15 +++++ .../org/jackhuang/hmcl/ui/Controllers.java | 16 +++-- .../ui/construct/TaskExecutorDialogPane.java | 26 ++++++++ .../ui/download/ModpackSelectionPage.java | 5 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 2 +- .../jackhuang/hmcl/ui/main/SettingsPage.java | 9 +++ .../jackhuang/hmcl/ui/task/TaskCenter.java | 21 ++++++- .../hmcl/ui/task/TaskCenterPage.java | 14 +++-- .../TaskExecutorDialogWizardDisplayer.java | 63 ++++++++++++++----- .../resources/assets/lang/I18N.properties | 5 ++ .../resources/assets/lang/I18N_zh.properties | 5 ++ .../assets/lang/I18N_zh_CN.properties | 5 ++ 12 files changed, 152 insertions(+), 34 deletions(-) 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 0320e13b18..7ea046a387 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -689,6 +689,21 @@ public void setDisableAutoGameOptions(boolean disableAutoGameOptions) { this.disableAutoGameOptions.set(disableAutoGameOptions); } + @SerializedName("autoBackgroundTask") + private final BooleanProperty autoBackgroundTask = new SimpleBooleanProperty(false); + + public BooleanProperty autoBackgroundTaskProperty() { + return autoBackgroundTask; + } + + public boolean isAutoBackgroundTask() { + return autoBackgroundTask.get(); + } + + public void setAutoBackgroundTask(boolean autoBackgroundTask) { + this.autoBackgroundTask.set(autoBackgroundTask); + } + // Accounts @SerializedName("authlibInjectorServers") diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index b17d3fd139..e1ce1c6583 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -592,15 +592,7 @@ public static TaskExecutorDialogPane taskDialog(Task task, String title, Task public static TaskExecutorDialogPane downloadTaskDialog(Task task, String title, TaskCancellationAction onCancel, String detail) { TaskExecutor executor = task.executor(); - TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel); - - pane.setBackgroundAction(() -> { - TaskCenter.getInstance().enqueue(executor, title, detail); - pane.fireEvent(new DialogCloseEvent()); - }); - - executor.start(); - return pane; + return downloadTaskDialog(executor, title, onCancel, detail); } public static TaskExecutorDialogPane downloadTaskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel, String detail) { @@ -644,6 +636,12 @@ public static void onHyperlinkAction(String href) { } } + public static boolean isDialogShowing() { + if (decorator == null) return false; + return decorator.getDecorator().getDrawerWrapper() != null + && decorator.getDecorator().getDrawerWrapper().getProperties().get(DialogUtils.PROPERTY_DIALOG_INSTANCE) != null; + } + public static boolean isStopped() { return decorator == null; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java index 39d41932d7..28752dca2a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskExecutorDialogPane.java @@ -212,4 +212,30 @@ public void setCancelAction(Runnable action) { public void setCancelText(String text) { btnCancel.setText(text); } + + private Label lblWaiting; + + public void setWaitingForBackground(boolean waiting) { + if (waiting) { + if (lblWaiting == null) { + lblWaiting = new Label(i18n("task.waiting_for_background")); + lblWaiting.setStyle("-fx-text-fill: -fx-secondary-text-color; -fx-font-size: 13px;"); + lblWaiting.setWrapText(true); + } + taskListPane.setVisible(false); + taskListPane.setManaged(false); + lblProgress.setVisible(false); + lblProgress.setManaged(false); + ((VBox) getCenter()).getChildren().add(lblWaiting); + } else { + taskListPane.setVisible(true); + taskListPane.setManaged(true); + lblProgress.setVisible(true); + lblProgress.setManaged(true); + lblProgress.setText(""); + if (lblWaiting != null) { + ((VBox) getCenter()).getChildren().remove(lblWaiting); + } + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index c49640cbff..5386b81831 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -144,7 +144,7 @@ private void onChooseRemoteFile() { // otherwise we still consider the file as modpack zip file // since casually the url may not ends with ".zip" Path modpack = Files.createTempFile("modpack", ".zip"); - Controllers.downloadTaskDialog( + Controllers.taskDialog( new FileDownloadTask(url, modpack) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { @@ -156,8 +156,7 @@ private void onChooseRemoteFile() { } }), i18n("message.downloading"), - TaskCancellationAction.NORMAL, - i18n("task.detail.install_modpack", modpack.getFileName().toString()) + TaskCancellationAction.NORMAL ); } } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 231ae8f3bb..4e307a416c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -198,7 +198,7 @@ protected Skin(RootPage control) { taskManagerItem.setOnAction(e -> { Controllers.navigate(new TaskCenterPage()); }); - + FXUtils.installFastTooltip(taskManagerItem, i18n("task.manage.hint")); // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); 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 1919609180..d995b7d98a 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 @@ -288,6 +288,15 @@ else if (locale.isSameLanguage(currentLocale)) settingsPane.getContent().add(disableAutoGameOptionsPane); } + { + LineToggleButton autoBackgroundTaskPane = new LineToggleButton(); + autoBackgroundTaskPane.setTitle(i18n("settings.launcher.auto_background_task")); + autoBackgroundTaskPane.setSubtitle(i18n("settings.launcher.auto_background_task.subtitle")); + autoBackgroundTaskPane.selectedProperty().bindBidirectional(config().autoBackgroundTaskProperty()); + + settingsPane.getContent().add(autoBackgroundTaskPane); + } + { BorderPane debugPane = new BorderPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index 73f9e858e4..69652f9896 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -129,6 +129,22 @@ public void enqueue(TaskExecutor executor, String title, String detail, TaskKind return; } + // Deduplicate by kind+name (e.g. same game/modpack install) + if (kind != null && name != null && hasQueuedInstallName(kind, name)) { + return; + } + + // Deduplicate by detail (or title if detail is null) to prevent repeated downloads + String deduplicationKey = detail != null ? detail : title; + if (deduplicationKey != null) { + for (Entry existing : entries) { + String existingKey = existing.getDetail() != null ? existing.getDetail() : existing.getTitle(); + if (deduplicationKey.equals(existingKey)) { + return; + } + } + } + Entry entry = new Entry(executor, title, detail, kind, name); entryIndex.put(executor, entry); entries.add(entry); @@ -180,13 +196,14 @@ private void onTaskStopped(TaskExecutor executor, boolean success) { failedEntries.add(stoppedEntry); } - // Show toast notification for background tasks + // Show notification for background tasks String detail = stoppedEntry.getDetail() != null ? stoppedEntry.getDetail() : stoppedEntry.getTitle(); if (success) { Controllers.showToast(i18n("task.toast.success", detail)); } else { Controllers.showToast(i18n("task.toast.failed", detail)); - if (!(stoppedEntry.getExecutor().getException() instanceof CancellationException)) { + if (!(stoppedEntry.getExecutor().getException() instanceof CancellationException) + && !Controllers.isDialogShowing()) { TaskCenterPage.showFailedTaskDialog(stoppedEntry); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index ca45d9b60c..9eee6115f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -124,11 +124,17 @@ private static HBox createClearToolbar(Runnable onClear) { HBox toolbar = new HBox(); toolbar.setAlignment(Pos.CENTER_LEFT); toolbar.setPickOnBounds(false); + toolbar.setMinHeight(24); + toolbar.setMaxHeight(24); + toolbar.setPrefHeight(24); toolbar.setStyle("-fx-border-color: -monet-outline-variant; -fx-border-width: 0 0 1 0;"); - toolbar.setPadding(new Insets(0, 0, 4, 0)); - toolbar.getChildren().setAll( - createToolbarButton2(i18n("task.clear"), SVG.DELETE, onClear) - ); + + JFXButton btn = createToolbarButton2(i18n("task.clear"), SVG.DELETE, onClear); + btn.setMinHeight(24); + btn.setMaxHeight(24); + btn.setPrefHeight(24); + + toolbar.getChildren().setAll(btn); return toolbar; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index 0ead83bea8..ccf30d9c89 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -33,6 +33,7 @@ import java.util.Queue; import java.util.concurrent.CancellationException; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -61,10 +62,16 @@ else if (title instanceof String titleMessage) runInFX(() -> { boolean backgroundable = Boolean.TRUE.equals(settings.get("backgroundable")); + // Track whether this task has been moved to background + final boolean[] movedToBackground = {false}; + TaskExecutor executor = task.executor(new TaskListener() { @Override public void onStop(boolean success, TaskExecutor executor) { runInFX(() -> { + // If task was moved to background, TaskCenter handles notifications + if (movedToBackground[0]) return; + if (success) { if (settings.get("success_message") instanceof String successMessage) Controllers.dialog(successMessage, null, MessageType.SUCCESS, () -> onEnd()); @@ -91,17 +98,6 @@ else if (!settings.containsKey("forbid_failure_message")) } }); - pane.setExecutor(executor); - - pane.addEventHandler(DialogCloseEvent.CLOSE, event -> { - boolean returnToDownloadList = Boolean.TRUE.equals(settings.get("return_to_download_list")); - if (returnToDownloadList) { - onEnd(); - Controllers.getDownloadPage().showGameDownloads(); - Controllers.navigate(Controllers.getDownloadPage()); - } - }); - if (backgroundable) { Object detailObj = settings.get("task_detail"); String detail = detailObj != null ? detailObj.toString() : pane.getTitle(); @@ -109,7 +105,17 @@ else if (!settings.containsKey("forbid_failure_message")) TaskCenter.TaskKind kind = (TaskCenter.TaskKind) settings.get("task_kind"); String taskName = (String) settings.get("task_name"); - pane.setBackgroundAction(() -> { + if (config().isAutoBackgroundTask()) { + // Auto-background: enqueue directly without showing dialog + movedToBackground[0] = true; + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); + Controllers.showToast(i18n("task.auto_background.enqueued", detail != null ? detail : pane.getTitle())); + onEnd(); + return; + } + + Runnable moveToBackground = () -> { + movedToBackground[0] = true; TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); boolean returnToDownloadList = Boolean.TRUE.equals(settings.get("return_to_download_list")); @@ -120,13 +126,40 @@ else if (!settings.containsKey("forbid_failure_message")) } pane.fireEvent(new DialogCloseEvent()); - }); + }; + + // Manual mode: check if background tasks are running, wait if so + if (!TaskCenter.getInstance().getEntries().isEmpty()) { + pane.setWaitingForBackground(true); + TaskCenter.getInstance().getEntries().addListener( + (javafx.collections.ListChangeListener) change -> { + if (TaskCenter.getInstance().getEntries().isEmpty()) { + pane.setWaitingForBackground(false); + executor.start(); + pane.refreshTaskList(); + } + }); + } + pane.setBackgroundAction(moveToBackground); } + pane.setExecutor(executor); + + pane.addEventHandler(DialogCloseEvent.CLOSE, event -> { + boolean returnToDownloadList = Boolean.TRUE.equals(settings.get("return_to_download_list")); + if (returnToDownloadList) { + onEnd(); + Controllers.getDownloadPage().showGameDownloads(); + Controllers.navigate(Controllers.getDownloadPage()); + } + }); + Controllers.dialog(pane); - executor.start(); - pane.refreshTaskList(); + if (!backgroundable || TaskCenter.getInstance().getEntries().isEmpty()) { + executor.start(); + pane.refreshTaskList(); + } }); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 65fd6d9c73..b237820eee 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1425,6 +1425,8 @@ settings.launcher.common_path.tooltip=HMCL will put all game assets and dependen settings.launcher.debug=Debug settings.launcher.disable_april_fools=Do not enable April Fools features settings.launcher.disable_auto_game_options=Do not switch game language +settings.launcher.auto_background_task=Automatically move tasks to background +settings.launcher.auto_background_task.subtitle=Supported tasks will be automatically moved to the background task queue after starting settings.launcher.download=Download settings.launcher.download.threads=Threads settings.launcher.download.threads.auto=Automatically Determine @@ -1525,7 +1527,9 @@ task.kind.java_download=Java task.kind.mod_update=Mod task.kind.modpack_install=Modpack task.kind.other=Task +task.auto_background.enqueued="%1$s" has been added to the background task queue task.manage=Task Manager +task.manage.hint=View and manage background download tasks task.move_to_background=Move Task to Background task.running=Running Tasks task.status.running=Running @@ -1533,6 +1537,7 @@ task.toast.failed=Task failed: %s task.toast.success=Task completed: %s task.unnamed=Unnamed Task task.waiting=Task is waiting in queue +task.waiting_for_background=There are background tasks in progress. This task will start automatically after they are completed. terracotta=Multiplayer terracotta.terracotta=Terracotta | Multiplayer terracotta.status=Lobby diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 30595389da..0b0d3e2b16 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1210,6 +1210,8 @@ settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元 settings.launcher.debug=除錯 settings.launcher.disable_april_fools=不啟用愚人節功能 settings.launcher.disable_auto_game_options=不自動切換遊戲語言 +settings.launcher.auto_background_task=自動將任務移至後臺 +settings.launcher.auto_background_task.subtitle=支援後臺處理的任務啟動後將自動加入後臺任務佇列 settings.launcher.download=下載 settings.launcher.download.threads=執行緒數 settings.launcher.download.threads.auto=自動選取執行緒數 @@ -1310,7 +1312,9 @@ task.kind.java_download=Java task.kind.mod_update=模組 task.kind.modpack_install=整合包 task.kind.other=任務 +task.auto_background.enqueued="%1$s" 已加入後臺任務佇列 task.manage=任務管理器 +task.manage.hint=檢視和管理後臺下載任務 task.move_to_background=將任務移至後臺 task.running=正在執行的任務 task.status.running=執行中 @@ -1318,6 +1322,7 @@ task.toast.failed=任務失敗:%s task.toast.success=任務完成:%s task.unnamed=未命名任務 task.waiting=任務正在佇列中等待 +task.waiting_for_background=有後臺任務正在進行中,本任務將在後臺任務全部完成後自動開始。 terracotta=多人遊戲 terracotta.terracotta=Terracotta | 陶瓦聯機 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 310f93a7bb..5cc6737c5c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1215,6 +1215,8 @@ settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库 settings.launcher.debug=调试 settings.launcher.disable_april_fools=不启用愚人节功能 settings.launcher.disable_auto_game_options=不自动切换游戏语言 +settings.launcher.auto_background_task=自动将任务移至后台 +settings.launcher.auto_background_task.subtitle=支持后台处理的任务启动后将自动加入后台任务队列 settings.launcher.download=下载 settings.launcher.download.threads=线程数 settings.launcher.download.threads.auto=自动选择线程数 @@ -1315,7 +1317,9 @@ task.kind.java_download=Java task.kind.mod_update=模组 task.kind.modpack_install=整合包 task.kind.other=任务 +task.auto_background.enqueued="%1$s" 已加入后台任务队列 task.manage=任务管理器 +task.manage.hint=查看和管理后台下载任务 task.move_to_background=将任务移至后台 task.running=正在运行的任务 task.status.running=运行中 @@ -1323,6 +1327,7 @@ task.toast.failed=任务失败:%s task.toast.success=任务完成:%s task.unnamed=未命名任务 task.waiting=任务正在队列中等待 +task.waiting_for_background=有后台任务正在进行中,本任务将在后台任务全部完成后自动开始。 terracotta=多人联机 terracotta.terracotta=Terracotta | 陶瓦联机 From 084506310d95445fed82ff7550fa71460f441de7 Mon Sep 17 00:00:00 2001 From: lokins Date: Sun, 22 Mar 2026 00:37:06 +0800 Subject: [PATCH 27/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=EF=BC=8C=E5=8E=BB=E9=99=A4=E5=A4=9A=E4=BD=99?= =?UTF-8?q?=E7=9A=84=E6=BB=9A=E5=8A=A8=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/construct/AdvancedListBox.java | 7 ---- .../hmcl/ui/decorator/DecoratorSkin.java | 36 ++++++++++++++++++- .../jackhuang/hmcl/ui/task/TaskCenter.java | 5 +-- .../hmcl/ui/task/TaskCenterPage.java | 14 +++++--- .../TaskExecutorDialogWizardDisplayer.java | 3 ++ .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 8 files changed, 54 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java index b0eb8fc57b..3affe4939c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java @@ -46,13 +46,6 @@ public class AdvancedListBox extends ScrollPane { setVbarPolicy(ScrollBarPolicy.NEVER); container.getStyleClass().add("advanced-list-box-content"); - - this.addEventFilter(MouseEvent.MOUSE_ENTERED, event -> { - if (container.getHeight() > getHeight()) - setVbarPolicy(ScrollBarPolicy.AS_NEEDED); - }); - this.addEventFilter(MouseEvent.MOUSE_EXITED, - event -> setVbarPolicy(ScrollBarPolicy.NEVER)); } public AdvancedListBox add(Node child) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java index baa9786b9e..dfc55f71d9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java @@ -46,14 +46,19 @@ import javafx.util.Duration; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.theme.Themes; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.task.TaskCenter; +import org.jackhuang.hmcl.ui.task.TaskCenterPage; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + public class DecoratorSkin extends SkinBase { private final StackPane root, parent; private final StackPane titleContainer; @@ -231,6 +236,35 @@ public DecoratorSkin(Decorator control) { buttonsContainer.setAlignment(Pos.TOP_RIGHT); buttonsContainer.setMaxHeight(40); { + // Background task indicator + JFXButton btnTask = new JFXButton(); + btnTask.setFocusTraversable(false); + btnTask.getStyleClass().add("jfx-decorator-button"); + btnTask.setOnAction(e -> Controllers.navigate(new TaskCenterPage())); + FXUtils.installFastTooltip(btnTask, i18n("task.manage")); + + Label taskBadge = new Label(); + taskBadge.setStyle("-fx-font-size: 10px; -fx-text-fill: white; -fx-background-color: #F44336; " + + "-fx-background-radius: 8; -fx-min-width: 16; -fx-min-height: 16; " + + "-fx-alignment: center; -fx-padding: 0 3 0 3;"); + + StackPane taskIconPane = new StackPane(); + taskIconPane.getChildren().add(SVG.CHECKLIST.createIcon(Themes.titleFillProperty())); + taskIconPane.getChildren().add(taskBadge); + StackPane.setAlignment(taskBadge, Pos.TOP_RIGHT); + StackPane.setMargin(taskBadge, new Insets(-4, -6, 0, 0)); + btnTask.setGraphic(taskIconPane); + + Runnable updateTaskIndicator = () -> { + int count = TaskCenter.getInstance().getEntries().size(); + btnTask.setVisible(count > 0); + btnTask.setManaged(count > 0); + taskBadge.setText(String.valueOf(count)); + }; + updateTaskIndicator.run(); + TaskCenter.getInstance().getEntries().addListener( + (ListChangeListener) change -> updateTaskIndicator.run()); + JFXButton btnHelp = new JFXButton(); btnHelp.setFocusTraversable(false); btnHelp.setGraphic(SVG.HELP.createIcon(Themes.titleFillProperty())); @@ -249,7 +283,7 @@ public DecoratorSkin(Decorator control) { btnClose.getStyleClass().add("jfx-decorator-button"); btnClose.setOnAction(e -> skinnable.close()); - buttonsContainer.getChildren().setAll(btnHelp, btnMin, btnClose); + buttonsContainer.getChildren().setAll(btnTask, btnHelp, btnMin, btnClose); } AnchorPane layer = new AnchorPane(); layer.setPickOnBounds(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java index 69652f9896..86ebcc1c33 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -200,10 +200,11 @@ private void onTaskStopped(TaskExecutor executor, boolean success) { String detail = stoppedEntry.getDetail() != null ? stoppedEntry.getDetail() : stoppedEntry.getTitle(); if (success) { Controllers.showToast(i18n("task.toast.success", detail)); + } else if (stoppedEntry.getExecutor().getException() instanceof CancellationException) { + Controllers.showToast(i18n("task.toast.cancelled", detail)); } else { Controllers.showToast(i18n("task.toast.failed", detail)); - if (!(stoppedEntry.getExecutor().getException() instanceof CancellationException) - && !Controllers.isDialogShowing()) { + if (!Controllers.isDialogShowing()) { TaskCenterPage.showFailedTaskDialog(stoppedEntry); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java index 9eee6115f6..36cfda04a1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -276,9 +276,15 @@ private Node createHistoryItem(TaskCenter.Entry entry, boolean success) { row.setAlignment(Pos.CENTER_LEFT); row.getStyleClass().add("md-list-cell"); - Node icon = success - ? SVG.CHECK.createIcon(14) - : SVG.CLOSE.createIcon(14); + boolean cancelled = !success && entry.getExecutor().getException() instanceof CancellationException; + Node icon; + if (success) { + icon = SVG.CHECK.createIcon(14); + } else if (cancelled) { + icon = SVG.CANCEL.createIcon(14); + } else { + icon = SVG.CLOSE.createIcon(14); + } Label kindTag = createKindTag(entry.getKind()); @@ -290,7 +296,7 @@ private Node createHistoryItem(TaskCenter.Entry entry, boolean success) { row.getChildren().addAll(icon, kindTag, label); - if (!success) { + if (!success && !cancelled) { row.setStyle("-fx-cursor: hand;"); row.setOnMouseClicked(e -> showFailedTaskDialog(entry)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index ccf30d9c89..031048b201 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -110,6 +110,9 @@ else if (!settings.containsKey("forbid_failure_message")) movedToBackground[0] = true; TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); Controllers.showToast(i18n("task.auto_background.enqueued", detail != null ? detail : pane.getTitle())); + // onEnd() cleans up wizard state and navigates back to the parent page + // (e.g. game install list, modpack list, mod page, etc.) + // movedToBackground prevents onStop from navigating again on task completion onEnd(); return; } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b237820eee..65aa9d637c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1533,6 +1533,7 @@ task.manage.hint=View and manage background download tasks task.move_to_background=Move Task to Background task.running=Running Tasks task.status.running=Running +task.toast.cancelled=Task cancelled: %s task.toast.failed=Task failed: %s task.toast.success=Task completed: %s task.unnamed=Unnamed Task diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0b0d3e2b16..1b372e7e2b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1318,6 +1318,7 @@ task.manage.hint=檢視和管理後臺下載任務 task.move_to_background=將任務移至後臺 task.running=正在執行的任務 task.status.running=執行中 +task.toast.cancelled=任務已取消:%s task.toast.failed=任務失敗:%s task.toast.success=任務完成:%s task.unnamed=未命名任務 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 5cc6737c5c..a3962f77a4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1323,6 +1323,7 @@ task.manage.hint=查看和管理后台下载任务 task.move_to_background=将任务移至后台 task.running=正在运行的任务 task.status.running=运行中 +task.toast.cancelled=任务已取消:%s task.toast.failed=任务失败:%s task.toast.success=任务完成:%s task.unnamed=未命名任务 From fd4df3ee6000e6634fa9cbca033efa6598811936 Mon Sep 17 00:00:00 2001 From: lokins Date: Sun, 22 Mar 2026 00:39:29 +0800 Subject: [PATCH 28/28] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java index 3affe4939c..a4b1aac487 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java @@ -21,7 +21,6 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ScrollPane; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox;