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..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 @@ -21,16 +21,20 @@ 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.concurrent.atomic.AtomicBoolean; import java.util.Optional; import java.util.function.Consumer; @@ -48,9 +52,17 @@ 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"); + cancelAction = null; FXUtils.setLimitWidth(this, 500); FXUtils.setLimitHeight(this, 300); @@ -59,13 +71,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(); @@ -82,19 +115,54 @@ public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) { setCancel(cancel); - btnCancel.setOnAction(e -> { - 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()); Platform.runLater(() -> lblProgress.setText(message)); }); - onEscPressed(this, btnCancel::fire); + escAction = btnCancel::fire; + onEscPressed(this, () -> { + if (escAction != null) { + escAction.run(); + } + }); + } + + 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) { @@ -115,6 +183,7 @@ public void onStop(boolean success, TaskExecutor executor) { } }); } + applyQueuedStateIfNeeded(); } public StringProperty titleProperty() { @@ -134,4 +203,43 @@ 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(); + applyQueuedStateIfNeeded(); + } + + 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/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 0f5fd1be46..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 @@ -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,14 @@ 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..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; @@ -134,10 +135,29 @@ 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 = "安装模组";//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; + } + 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(file.getFile().getUrl(), dest); task.setName(file.getName()); return task; @@ -148,10 +168,12 @@ public static void download(Profile profile, @Nullable String version, RemoteMod } else { Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); } - } else { + } + else { Controllers.showToast(i18n("install.success")); } - }), i18n("message.downloading"), TaskCancellationAction.NORMAL); + }), i18n("message.downloading"), TaskCancellationAction.NORMAL, + detailPrefix + "-[" + file.getName() + "]"); handler.resolve(); }, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); @@ -301,6 +323,12 @@ 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("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 bd6a40012d..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,6 +143,13 @@ 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_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/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index 50090a1935..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 @@ -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() + "]"//TODO: i18n ); } } catch (IOException e) { 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/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index 0b4f8dbf70..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 @@ -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() + "]";//TODO i18n + break; + } + } + if (detail == null) { + detail = "安装" + libraryId;//TODO i18n + } + 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..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,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") + "]");//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 c6e88a7481..ac44377e0b 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() + "]";//TODO: i18n + 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() + "]");//TODO: i18n } } 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 32e7b24ca8..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 @@ -46,6 +46,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; @@ -189,6 +190,14 @@ protected Skin(RootPage control) { FXUtils.prepareOnMouseEnter(downloadItem, Controllers::prepareDownloadPage); } + AdvancedListItem taskManagerItem = new AdvancedListItem(); + taskManagerItem.setLeftIcon(SVG.LIST); //TODO SVG待更换 + taskManagerItem.setTitle(i18n("task.manage")); + taskManagerItem.setOnAction(e -> { + Controllers.navigate(new TaskCenterPage()); + }); + + // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); launcherSettingsItem.setLeftIcon(SVG.SETTINGS); @@ -232,6 +241,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..b8352d1f9d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenter.java @@ -0,0 +1,220 @@ +/* + * 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; +import org.jackhuang.hmcl.util.platform.OperatingSystem; + +public final class TaskCenter { + private static final TaskCenter INSTANCE = new TaskCenter(); + + 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, TaskKind kind, String name) { + this.executor = executor; + this.title = title; + this.detail = detail; + this.kind = kind; + this.name = name; + } + + public TaskExecutor getExecutor() { + return executor; + } + + public String getTitle() { + return title; + } + + public String getDetail() { + return detail; + } + + public TaskKind getKind() { + return kind; + } + + public String getName() { + return name; + } + } + + 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 final Map started = new HashMap<>(); + private Entry running; + + public ObservableList getEntries() { + return entries; + } + + public Entry getRunningEntry() { + return running; + } + + public ObservableList getCompletedEntries() { + return completedEntries; + } + + 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; + } + + 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); + tryStartNext(); + } + + private synchronized void tryStartNext() { + if (running != null) return; + 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; + + 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(); + }); + } + }); + + 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)); + } + + 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 new file mode 100644 index 0000000000..291dacad80 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/task/TaskCenterPage.java @@ -0,0 +1,233 @@ +/* + * 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.control.Separator; +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.Locale; +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(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); + + 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()) { + 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()) { + 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 + } + })); + } + } + + 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 -> { + 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); + + 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 + 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(); + } +} 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..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,28 +84,32 @@ public static void downloadModpackImpl(Profile profile, String version, RemoteMo i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); return; } - Controllers.taskDialog( - 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), + + 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( + downloadTask, i18n("message.downloading"), - TaskCancellationAction.NORMAL + TaskCancellationAction.NORMAL, + "安装整合包-[" + modpack.getFileName() + "]"//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 cf5f128ebd..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 @@ -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,52 @@ 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); + + 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(); + + TaskCenter.TaskKind kind = (TaskCenter.TaskKind) settings.get("task_kind"); + String taskName = (String) settings.get("task_name"); + + 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(); + if (returnToDownloadList) { + Controllers.getDownloadPage().showGameDownloads(); + Controllers.navigate(Controllers.getDownloadPage()); + } + + pane.fireEvent(new DialogCloseEvent()); + }); + + TaskCenter.getInstance().enqueue(executor, pane.getTitle(), detail, kind, taskName); + pane.refreshTaskList(); + } + Controllers.dialog(pane); - executor.start(); + + if (!backgroundable) { + 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 2f682895b8..ae87a21f5d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1489,6 +1489,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 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; + } }