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 ebee0ade961..ce108f53927 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -29,7 +29,10 @@ import javafx.scene.control.ButtonBase; import javafx.scene.control.Label; import javafx.scene.layout.Region; -import javafx.stage.*; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.Screen; +import javafx.stage.Stage; import javafx.util.Duration; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; @@ -46,12 +49,12 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.decorator.Decorator; import org.jackhuang.hmcl.ui.download.DownloadPage; +import org.jackhuang.hmcl.ui.instances.GameInstancePage; +import org.jackhuang.hmcl.ui.instances.GameListPage; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; import org.jackhuang.hmcl.ui.terracotta.TerracottaPage; -import org.jackhuang.hmcl.ui.instances.GameListPage; -import org.jackhuang.hmcl.ui.instances.GameInstancePage; -import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; @@ -70,10 +73,7 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; -import static org.jackhuang.hmcl.setting.SettingsManager.settings; -import static org.jackhuang.hmcl.setting.SettingsManager.getAuthlibInjectorServers; -import static org.jackhuang.hmcl.setting.SettingsManager.state; -import static org.jackhuang.hmcl.setting.SettingsManager.userState; +import static org.jackhuang.hmcl.setting.SettingsManager.*; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -501,6 +501,19 @@ public static TaskExecutorDialogPane taskDialog(Task task, String title, @Not return pane; } + public static TaskLoadingDialog taskLoadingDialog(TaskExecutor executor, String title, @NotNull TaskCancellationAction onCancel) { + TaskLoadingDialog pane = new TaskLoadingDialog(title, onCancel, executor); + dialog(pane); + return pane; + } + + public static TaskLoadingDialog taskLoadingDialog(Task task, String title, @NotNull TaskCancellationAction onCancel) { + TaskExecutor executor = task.executor(); + TaskLoadingDialog pane = taskLoadingDialog(executor, title, onCancel); + executor.start(); + 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/TaskLoadingDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskLoadingDialog.java new file mode 100644 index 00000000000..bab96ad3330 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskLoadingDialog.java @@ -0,0 +1,80 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.construct; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; +import com.jfoenix.controls.JFXSpinner; +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.VBox; +import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.task.TaskListener; +import org.jackhuang.hmcl.theme.Themes; +import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jetbrains.annotations.NotNull; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public final class TaskLoadingDialog extends JFXDialogLayout { + private final Label lblTitle; + + public TaskLoadingDialog(String title, @NotNull TaskCancellationAction onCancel, TaskExecutor executor) { + var vbox = new VBox(20); + vbox.setPadding(new Insets(10)); + vbox.setAlignment(Pos.CENTER); + + lblTitle = new Label(title); + lblTitle.setStyle("-fx-font-size: 16px; -fx-font-weight: BOLD;"); + + JFXSpinner spinner = new JFXSpinner(); + + JFXButton cancelButton = new JFXButton(i18n("button.cancel")); + + if (onCancel.getCancellationAction() != null) { + cancelButton.setOnAction(event -> onCancel.getCancellationAction().accept(this)); + } else cancelButton.setDisable(true); + + cancelButton.setTextFill(Themes.getColorScheme().getOnSurface()); + + executor.addTaskListener(new TaskListener() { + @Override + public void onStop(boolean success, TaskExecutor executor) { + Platform.runLater(() -> fireEvent(new DialogCloseEvent())); + } + }); + + vbox.getChildren().addAll(lblTitle, spinner, cancelButton); + getChildren().setAll(vbox); + } + + public StringProperty titleProperty() { + return lblTitle.textProperty(); + } + + public String getTitle() { + return lblTitle.getText(); + } + + public void setTitle(String currentState) { + lblTitle.setText(currentState); + } +} 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 6ffb6676068..9b59e56397a 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 @@ -45,6 +45,7 @@ import java.util.List; import static org.jackhuang.hmcl.setting.SettingsManager.settings; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class ExportWizardProvider implements WizardProvider { private final HMCLGameRepository repository; @@ -57,6 +58,8 @@ public ExportWizardProvider(HMCLGameRepository repository, GameInstanceID instan @Override public void start(SettingsMap settings) { + settings.put("use_loading_dialog", "true"); + settings.put("title", i18n("modpack.exporting")); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java index b48622eb98c..d0cadbafe97 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java @@ -177,7 +177,7 @@ private void updateFiles() { private void exportList() { Path path = Paths.get("hmcl-mod-update-list-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".csv").toAbsolutePath(); - Controllers.taskDialog(Task.runAsync(() -> { + Controllers.taskLoadingDialog(Task.runAsync(() -> { CSVTable csvTable = new CSVTable(); csvTable.set(0, 0, "Source File Name"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java index 7aad9021f94..8784c3c2191 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java @@ -131,7 +131,7 @@ protected Skin createDefaultSkin() { } void createBackup() { - Controllers.taskDialog(new WorldBackupTask(world, backupsDir, false).setName(i18n("world.backup.processing")).thenApplyAsync(path -> { + Controllers.taskLoadingDialog(new WorldBackupTask(world, backupsDir, false).thenApplyAsync(path -> { Matcher matcher = backupFileNamePattern.matcher(path.getFileName().toString()); if (!matcher.matches()) { throw new AssertionError("Wrong backup file name" + path); @@ -156,7 +156,7 @@ 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"), TaskCancellationAction.NO_CANCEL); + }), i18n("world.backup.processing"), TaskCancellationAction.NO_CANCEL); } private final class WorldBackupsPageSkin extends ToolbarListPageSkin { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java index b3eeb858956..12035c908d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java @@ -37,8 +37,8 @@ import org.jackhuang.hmcl.java.JavaInfo; import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaRuntime; -import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; @@ -50,17 +50,19 @@ import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; -import org.jackhuang.hmcl.util.tree.ArchiveFileTree; -import org.jetbrains.annotations.Nullable; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; +import org.jackhuang.hmcl.util.tree.ArchiveFileTree; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -348,7 +350,7 @@ private void onRemove(JavaRuntime java) { Controllers.confirm( i18n("java.uninstall.confirm"), i18n("message.warning"), - () -> Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL), + () -> Controllers.taskLoadingDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL), null ); } else { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java index c9354736704..163482a9c42 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/ThemePackManagementPage.java @@ -23,11 +23,7 @@ import com.jfoenix.controls.JFXTextField; import javafx.animation.PauseTransition; import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -35,11 +31,7 @@ import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -251,7 +243,7 @@ private void confirmApplyTheme(ThemePackManager.InstalledThemePack themePack, Th private void applyTheme(ThemePackManager.InstalledThemePack themePack, Theme theme) { try { if (Themes.shouldWaitForThemeBackground()) { - Controllers.taskDialog( + Controllers.taskLoadingDialog( Themes.applyTheme(themePack, theme), i18n("launcher.background.loading"), TaskCancellationAction.NO_CANCEL); 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 ccba6dd81fb..4ddb3e7bb91 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 @@ -25,6 +25,7 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; +import org.jackhuang.hmcl.ui.construct.TaskLoadingDialog; import org.jackhuang.hmcl.util.SettingsMap; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; @@ -43,65 +44,82 @@ public TaskExecutorDialogWizardDisplayer(Queue cancelQueue) { @Override public void handleTask(SettingsMap settings, Task task) { - TaskExecutorDialogPane pane = new TaskExecutorDialogPane(new TaskCancellationAction(it -> { - it.fireEvent(new DialogCloseEvent()); - onEnd(); - })); - - pane.setTitle(i18n("message.doing")); - if (settings.containsKey("title")) { - Object title = settings.get("title"); - if (title instanceof StringProperty titleProperty) - pane.titleProperty().bind(titleProperty); - else if (title instanceof String titleMessage) - pane.setTitle(titleMessage); - } + TaskExecutor executor = task.executor(new TaskListener() { + @Override + public void onStop(boolean success, TaskExecutor executor) { + runInFX(() -> { + if (success) { + if (settings.get("success_message") instanceof String successMessage) + Controllers.dialog(successMessage, null, MessageType.SUCCESS, () -> onEnd()); + else if (!settings.containsKey("forbid_success_message")) + Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd()); + } else { + if (executor.getException() == null) { + onEnd(); + return; + } - runInFX(() -> { - TaskExecutor executor = task.executor(new TaskListener() { - @Override - public void onStop(boolean success, TaskExecutor executor) { - runInFX(() -> { - if (success) { - if (settings.get("success_message") instanceof String successMessage) - Controllers.dialog(successMessage, null, MessageType.SUCCESS, () -> onEnd()); - else if (!settings.containsKey("forbid_success_message")) - Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd()); - } else { - if (executor.getException() == null) { - onEnd(); - return; - } + if (executor.getException() instanceof CancellationException) { + onEnd(); + return; + } - if (executor.getException() instanceof CancellationException) { + if (executor.getException().getCause() instanceof OutOfMemoryError outOfMemoryError) { + try { + Controllers.dialog(StringUtils.getStackTrace(outOfMemoryError), null, MessageType.ERROR, () -> onEnd()); + } catch (OutOfMemoryError ignored) { onEnd(); - return; } + return; + } - if (executor.getException().getCause() instanceof OutOfMemoryError outOfMemoryError) { - try { - Controllers.dialog(StringUtils.getStackTrace(outOfMemoryError), null, MessageType.ERROR, () -> onEnd()); - } catch (OutOfMemoryError ignored) { - onEnd(); - } - return; - } + String appendix = StringUtils.getStackTrace(executor.getException()); + if (settings.get(WizardProvider.FailureCallback.KEY) != null) + settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd()); + else if (settings.get("failure_message") instanceof String failureMessage) + Controllers.dialog(appendix, failureMessage, MessageType.ERROR, () -> onEnd()); + else if (!settings.containsKey("forbid_failure_message")) + Controllers.dialog(appendix, i18n("wizard.failed"), MessageType.ERROR, () -> onEnd()); + } - String appendix = StringUtils.getStackTrace(executor.getException()); - if (settings.get(WizardProvider.FailureCallback.KEY) != null) - settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd()); - else if (settings.get("failure_message") instanceof String failureMessage) - Controllers.dialog(appendix, failureMessage, MessageType.ERROR, () -> onEnd()); - else if (!settings.containsKey("forbid_failure_message")) - Controllers.dialog(appendix, i18n("wizard.failed"), MessageType.ERROR, () -> onEnd()); - } + }); + } + }); + + + var cancel = new TaskCancellationAction(it -> { + it.fireEvent(new DialogCloseEvent()); + onEnd(); + }); + + if (settings.containsKey("use_loading_dialog")) { + TaskLoadingDialog taskLoadingDialog = new TaskLoadingDialog(i18n("message.doing"), cancel, executor); + + if (settings.containsKey("title")) { + Object title = settings.get("title"); + if (title instanceof StringProperty titleProperty) + taskLoadingDialog.titleProperty().bind(titleProperty); + else if (title instanceof String titleMessage) + taskLoadingDialog.setTitle(titleMessage); + } + + Controllers.dialog(taskLoadingDialog); + executor.start(); + } else { + TaskExecutorDialogPane pane = new TaskExecutorDialogPane(cancel); + + pane.setTitle(i18n("message.doing")); + if (settings.containsKey("title")) { + Object title = settings.get("title"); + if (title instanceof StringProperty titleProperty) + pane.titleProperty().bind(titleProperty); + else if (title instanceof String titleMessage) + pane.setTitle(titleMessage); + } - }); - } - }); pane.setExecutor(executor); Controllers.dialog(pane); executor.start(); - }); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/TaskCancellationAction.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/TaskCancellationAction.java index 73602876639..f9dc0c94554 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/TaskCancellationAction.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/TaskCancellationAction.java @@ -17,26 +17,26 @@ */ package org.jackhuang.hmcl.util; -import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; +import javafx.scene.Node; import java.util.function.Consumer; public final class TaskCancellationAction { - public static TaskCancellationAction NO_CANCEL = new TaskCancellationAction((Consumer) null); + public static TaskCancellationAction NO_CANCEL = new TaskCancellationAction((Consumer) null); public static TaskCancellationAction NORMAL = new TaskCancellationAction(() -> { }); - private final Consumer cancellationAction; + private final Consumer cancellationAction; public TaskCancellationAction(Runnable cancellationAction) { this.cancellationAction = it -> cancellationAction.run(); } - public TaskCancellationAction(Consumer cancellationAction) { + public TaskCancellationAction(Consumer cancellationAction) { this.cancellationAction = cancellationAction; } - public Consumer getCancellationAction() { + public Consumer getCancellationAction() { return cancellationAction; } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ca60b1b0a01..e54425d4088 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1083,6 +1083,7 @@ modpack.type.server.export=Allows server owner to remotely update the game insta modpack.type.server.malformed=Invalid modpack manifest. Please contact the modpack maker to resolve this problem. modpack.unsupported=Unsupported modpack format modpack.update=Updating modpack +modpack.exporting=Exporting modpack modpack.wizard=Modpack Export Guide modpack.wizard.step.1=Basic Settings modpack.wizard.step.1.title=Some basic informations for the modpack. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5eeafafcb3b..16a9e5bb19a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -892,6 +892,7 @@ modpack.type.server.export=允許伺服器管理員遠端更新遊戲用戶端 modpack.type.server.malformed=伺服器模組包配置格式錯誤,請聯絡伺服器管理員解決此問題 modpack.unsupported=Hello Minecraft! Launcher 不支援該模組包格式 modpack.update=正在升級模組包 +modpack.exporting=正在匯出模組包 modpack.wizard=匯出模組包引導 modpack.wizard.step.1=基本設定 modpack.wizard.step.1.title=設定模組包的主要訊息 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 6fe08134e54..e8f19c97c79 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -898,6 +898,7 @@ modpack.type.server.export=允许服务器管理员远程更新游戏客户端 modpack.type.server.malformed=服务器整合包配置格式错误,请联系服务器管理员解决此问题。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 modpack.unsupported=Hello Minecraft! Launcher 不支持该整合包格式 modpack.update=正在升级整合包 +modpack.exporting=正在导出整合包 modpack.wizard=导出整合包向导 modpack.wizard.step.1=基本设置 modpack.wizard.step.1.title=设置整合包的主要信息