From a2fa7ed5ab1138d9e22415c3ebd0c904dfce6806 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Sun, 8 Nov 2026 05:27:38 +0800 Subject: [PATCH] refactor: remove mod, resource pack, world, data pack and installer pages Co-Authored-By: Claude Sonnet 5.5 --- .../hmcl/ui/download/DownloadPage.java | 76 -- .../hmcl/ui/instances/AddonUpdatesPage.java | 515 ---------- .../hmcl/ui/instances/DataPackListPage.java | 121 --- .../ui/instances/DataPackListPageSkin.java | 370 ------- .../hmcl/ui/instances/DownloadListPage.java | 637 ------------- .../hmcl/ui/instances/DownloadPage.java | 722 -------------- .../hmcl/ui/instances/GameInstancePage.java | 22 +- .../HMCLLocalizedDownloadListPage.java | 108 --- .../hmcl/ui/instances/InstallerListPage.java | 127 --- .../hmcl/ui/instances/ModListPage.java | 900 ------------------ .../ui/instances/ResourcePackListPage.java | 689 -------------- .../hmcl/ui/instances/WorldBackupTask.java | 75 -- .../hmcl/ui/instances/WorldBackupsPage.java | 265 ------ .../hmcl/ui/instances/WorldExportPage.java | 65 -- .../ui/instances/WorldExportPageSkin.java | 72 -- .../hmcl/ui/instances/WorldInfoPage.java | 670 ------------- .../hmcl/ui/instances/WorldListPage.java | 426 --------- .../hmcl/ui/instances/WorldManagePage.java | 250 ----- .../hmcl/ui/instances/WorldManageUIUtils.java | 127 --- 19 files changed, 1 insertion(+), 6236 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPageSkin.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java 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 deleted file mode 100644 index 64da1d4482..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.download; - -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.game.GameInstanceID; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.Validator; -import org.jackhuang.hmcl.util.FileNameSet; -import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.concurrent.CancellationException; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -/// Helpers for downloading a remote addon file into an instance subdirectory. -/// -/// The former free-install / CurseForge-Modrinth browse hub was removed for the -/// server-client launcher; only install-into-folder callbacks remain for update flows. -public final class DownloadPage { - public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_RESOURCE_PACK = - (downloadProvider, repository, version, pack, file) -> download(downloadProvider, repository, version, file, "resourcepacks"); - public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_SHADER = - (downloadProvider, repository, version, shader, file) -> download(downloadProvider, repository, version, file, "shaderpacks"); - - private DownloadPage() { - } - - public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon.Version file, String subdirectoryName) { - @Nullable HMCLGameInstance instance = instanceId != null - ? repository.findInstance(instanceId) - : repository.getSelectedInstance(); - Path runDirectory = instance != null ? instance.getRunDirectory() : repository.getBaseDirectory(); - - var targetPath = runDirectory.resolve(subdirectoryName); - - FileNameSet existingPaths; - try { - existingPaths = FileNameSet.list(targetPath, null); - } catch (Exception e) { - LOG.warning("Failed to list folders in " + targetPath, e); - existingPaths = new FileNameSet(false); - } - - Controllers.prompt(i18n("archive.file.name"), (result, handler) -> { - Path dest = targetPath.resolve(result); - - Controllers.taskDialog(Task.composeAsync(() -> { - var task = new FileDownloadTask(downloadProvider.getDownloadCandidates(file.file().url()), dest); - task.setName(file.name()); - return task; - }).whenComplete(Schedulers.javafx(), exception -> { - if (exception != null) { - if (!(exception instanceof CancellationException)) { - Controllers.dialog(I18n.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR); - } - } else { - Controllers.showToast(i18n("install.success")); - } - }), i18n("message.downloading"), TaskCancellationAction.NORMAL); - handler.resolve(); - }, file.file().filename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("game_directory.already_exists"), existingPaths::notContains)); - } -} 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 deleted file mode 100644 index 731a9aa83c..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java +++ /dev/null @@ -1,515 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXDialogLayout; -import com.jfoenix.controls.JFXListView; - -import javafx.beans.property.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import org.jackhuang.hmcl.addon.update.AddonUpdate; -import org.jackhuang.hmcl.addon.LocalAddonFile; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.mod.LocalModFile; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile; -import org.jackhuang.hmcl.setting.DownloadProviders; -import org.jackhuang.hmcl.setting.GameInstanceIconType; -import org.jackhuang.hmcl.addon.update.AddonUpdateTask; -import org.jackhuang.hmcl.addon.update.AddonUpdates; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -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.DecoratorPage; -import org.jackhuang.hmcl.util.Lazy; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.io.CSVTable; -import org.jackhuang.hmcl.util.io.CompressingUtils; -import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; -import org.jetbrains.annotations.Nullable; - -import java.lang.ref.WeakReference; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import org.jackhuang.hmcl.util.javafx.Subscription; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.stream.Collectors; - -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public class AddonUpdatesPage extends BorderPane implements DecoratorPage { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("addon.check_update"))); - - private final DownloadProvider downloadProvider; - private final Path localAddonDirectory; - private final ObservableList objects; - - private final JFXListView listView; - - public AddonUpdatesPage(DownloadProvider downloadProvider, Path localAddonDirectory, AddonUpdates updates) { - this.downloadProvider = downloadProvider; - this.localAddonDirectory = localAddonDirectory; - - getStyleClass().add("gray-background"); - - var common = new Lazy<>(() -> updates.commonUpdates().stream().map(AddonUpdateObject::new).toList()); - var release = new Lazy<>(() -> updates.releaseUpdates().stream().map(AddonUpdateObject::new).toList()); - - objects = FXCollections.observableArrayList(); - - { - StackPane centerWrapper = new StackPane(); - centerWrapper.setStyle("-fx-padding: 10;"); - - ComponentList centrePane = new ComponentList(); - centrePane.getStyleClass().add("no-padding"); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("addon.check_update.only_preview")); - placeholderContainer.getChildren().add(placeholderLabel); - - listView = new JFXListView<>(); - listView.setItems(objects); - listView.getStyleClass().add("jfx-list-view-float"); - listView.getStyleClass().add("no-horizontal-scrollbar"); - listView.setPlaceholder(placeholderContainer); - - setMargin(listView, new Insets(0, 10, 0, 10)); - VBox.setVgrow(listView, Priority.ALWAYS); - - listView.setCellFactory(AddonUpdateListCell::new); - - ComponentList.setVgrow(listView, Priority.ALWAYS); - - centrePane.getContent().setAll(listView); - - centerWrapper.getChildren().setAll(centrePane); - - setCenter(centerWrapper); - } - - { - HBox actions = new HBox(8); - actions.setPadding(new Insets(8)); - actions.setAlignment(Pos.CENTER_RIGHT); - - JFXCheckBox allEnabledBox = new JFXCheckBox(); - allEnabledBox.setText(i18n("button.select_all")); - FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); - - JFXCheckBox showPreviewCheck = new JFXCheckBox(i18n("addon.check_update.preview")); - { - List subscriptions = new ArrayList<>(); - FXUtils.onChangeAndOperate(showPreviewCheck.selectedProperty(), preview -> { - if (preview) objects.setAll(common.get()); - else objects.setAll(release.get()); - subscriptions.forEach(Subscription::unsubscribe); - subscriptions.clear(); - subscriptions.addAll(FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(AddonUpdateObject::enabledProperty).toArray(BooleanProperty[]::new))); - }); - } - - Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - JFXButton exportListButton = FXUtils.newRaisedButton(i18n("button.export")); - exportListButton.setOnAction(e -> exportList()); - - JFXButton nextButton = FXUtils.newRaisedButton(i18n("addon.check_update.confirm")); - nextButton.setOnAction(e -> updateFiles()); - - JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel")); - cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent())); - onEscPressed(this, cancelButton::fire); - onEscPressed(listView, cancelButton::fire); - - actions.getChildren().setAll(allEnabledBox, showPreviewCheck, spacer, exportListButton, nextButton, cancelButton); - - setBottom(actions); - } - } - - private void updateFiles() { - AddonUpdateTask task = new AddonUpdateTask( - downloadProvider, - localAddonDirectory, - objects.stream() - .filter(AddonUpdateObject::isEnabled) - .map(AddonUpdateObject::getData) - .toList() - ); - Controllers.taskDialog( - task.whenComplete(Schedulers.javafx(), exception -> { - fireEvent(new PageCloseEvent()); - if (!task.getFailedAddons().isEmpty()) { - Controllers.dialog(i18n("addon.check_update.failed_download") + "\n" + - task.getFailedAddons().stream().map(LocalAddonFile::getFileName).collect(Collectors.joining("\n")), - i18n("install.failed"), - MessageDialogPane.MessageType.ERROR); - } - - if (exception == null) { - Controllers.dialog(i18n("install.success")); - } - }), - i18n("addon.check_update"), - TaskCancellationAction.NORMAL); - } - - 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(() -> { - CSVTable csvTable = new CSVTable(); - - csvTable.set(0, 0, "Source File Name"); - csvTable.set(1, 0, "Current Version"); - csvTable.set(2, 0, "Target Version"); - csvTable.set(3, 0, "Update Source"); - - for (int i = 0; i < objects.size(); i++) { - csvTable.set(0, i + 1, objects.get(i).getFileName()); - csvTable.set(1, i + 1, objects.get(i).getCurrentVersion()); - csvTable.set(2, i + 1, objects.get(i).getTargetVersion()); - csvTable.set(3, i + 1, objects.get(i).getSource()); - } - - csvTable.write(path); - - FXUtils.showFileInExplorer(path); - }).whenComplete(Schedulers.javafx(), exception -> { - if (exception == null) { - Controllers.dialog(path.toString(), i18n("message.success")); - } else { - Controllers.dialog("", i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - }), i18n("button.export"), TaskCancellationAction.NORMAL); - } - - @Override - public ReadOnlyObjectWrapper stateProperty() { - return state; - } - - private static final class AddonUpdateObject { - final AddonUpdate data; - final String targetVersionType; - final String source; - String changelog = null; - - final BooleanProperty enabled = new SimpleBooleanProperty(); - - final ItemPropertyAsyncCache iconCache; - - public AddonUpdateObject(AddonUpdate data) { - this.data = data; - - this.targetVersionType = switch (data.targetVersion().versionType()) { - case Release -> i18n("addon.channel.release"); - case Beta -> i18n("addon.channel.beta"); - case Alpha -> i18n("addon.channel.alpha"); - }; - this.source = switch (data.currentVersion().source()) { - case CURSEFORGE -> i18n("addon.curseforge"); - case MODRINTH -> i18n("addon.modrinth"); - }; - - this.enabled.set(!data.localAddonFile().isDisabled()); - - this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); - } - - public AddonUpdate getData() { - return data; - } - - public boolean isEnabled() { - return enabled.get(); - } - - public BooleanProperty enabledProperty() { - return enabled; - } - - public LocalAddonFile getLocalAddonFile() { - return data.localAddonFile(); - } - - public String getFileName() { - return data.localAddonFile().getFileName(); - } - - public String getCurrentVersion() { - return data.currentVersion().version(); - } - - public String getTargetVersion() { - return data.targetVersion().version(); - } - - public String getTargetVersionType() { - return targetVersionType; - } - - public String getSource() { - return source; - } - - private Image getDefaultIcon() { - if (data.localAddonFile() instanceof LocalModFile localModFile) { - return GameInstanceIconType - .getIconType(localModFile.getModLoaderType()) - .getIcon(); - } - - return null; - } - - private Image loadIcon() { - if (data.localAddonFile() instanceof LocalModFile localModFile) { - List iconPaths = new ArrayList<>(); - - if (StringUtils.isNotBlank(localModFile.getLogoPath())) { - iconPaths.add(localModFile.getLogoPath()); - } - - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(localModFile.getFile())) { - - for (String path : iconPaths) { - Path iconPath = fs.getPath(path); - - if (Files.exists(iconPath)) { - Image image = FXUtils.loadImage( - iconPath, - 80, - 80, - true, - true - ); - - if (!image.isError() - && image.getWidth() > 0 - && image.getHeight() > 0 - && Math.abs(image.getWidth() - image.getHeight()) < 1 - ) { - return image; - } - } - } - } catch (Exception e) { - LOG.warning("Failed to load addon icons", e); - } - } - - if (data.localAddonFile() instanceof ResourcePackFile resourcePackFile) { - Image image = resourcePackFile.loadIcon(); - - if (image != null - && !image.isError() - && image.getWidth() > 0 - && image.getHeight() > 0 - && Math.abs(image.getWidth() - image.getHeight()) < 1 - ) { - return image; - } - } - - return getDefaultIcon(); - } - } - - private static class AddonUpdateListCell extends ListCell { - private final StackPane pane = new StackPane(); - private final RipplerContainer ripplerContainer; - - private final JFXCheckBox enabledBox = new JFXCheckBox(); - private final ImageContainer imageContainer = new ImageContainer(40); - private final TwoLineListItem content = new TwoLineListItem(); - private final JFXButton changelogButton = FXUtils.newToggleButton4(SVG.INFO); - - AddonUpdateListCell(ListView listView) { - HBox hbox = new HBox(16); - HBox.setHgrow(content, Priority.ALWAYS); - hbox.setAlignment(Pos.CENTER); - - hbox.getChildren().setAll(enabledBox, imageContainer, content, changelogButton); - - pane.getStyleClass().add("md-list-cell"); - StackPane.setMargin(hbox, new Insets(10, 16, 10, 16)); - - ripplerContainer = new RipplerContainer(hbox); - pane.getChildren().setAll(ripplerContainer); - - enabledBox.setMouseTransparent(false); - imageContainer.setMouseTransparent(true); - - FXUtils.installFastTooltip(changelogButton, i18n("addon.changelog")); - changelogButton.setOnAction(event -> { - AddonUpdateObject item = getItem(); - if (item != null) { - Controllers.dialog(new AddonChangelog(item)); - } - }); - - FXUtils.onClicked(this, () -> { - AddonUpdateObject item = getItem(); - if (item != null) { - item.enabled.set(!item.isEnabled()); - } - }); - - FXUtils.limitCellWidth(listView, this); - } - - /// Updates addon version information and replaces tags together, reusing their labels. - @Override - public void updateItem(@Nullable AddonUpdateObject item, boolean empty) { - @Nullable AddonUpdateObject oldItem = getItem(); - boolean oldEmpty = isEmpty(); - - super.updateItem(item, empty); - - if (oldItem == item && oldEmpty == empty) { - return; - } - - ripplerContainer.resetFeedback(); - - if (oldItem != null) { - enabledBox.selectedProperty().unbindBidirectional(oldItem.enabledProperty()); - } - - if (empty || item == null) { - setGraphic(null); - return; - } - - enabledBox.selectedProperty().bindBidirectional(item.enabledProperty()); - - item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty())); - - LocalAddonFile addonFile = item.getLocalAddonFile(); - - var tags = new ArrayList(3); - if (addonFile instanceof LocalModFile modFile) - tags.add(new Tag(modFile.getId())); - tags.add(new Tag(item.getSource())); - tags.add(new Tag(item.getTargetVersionType())); - content.getTags().setAll(tags); - - if (addonFile instanceof LocalModFile modFile) { - content.setTitle(modFile.getName()); - } else if (addonFile instanceof ResourcePackFile resourcePackFile) { - content.setTitle(resourcePackFile.getFileName()); - } - - content.setSubtitle(item.getCurrentVersion() + " → " + item.getTargetVersion()); - - setGraphic(pane); - } - } - - private static final class AddonChangelog extends JFXDialogLayout { - - public AddonChangelog(AddonUpdateObject object) { - RemoteAddon.Version targetVersion = object.data.targetVersion(); - - this.setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + targetVersion.name()))); - - VBox box = new VBox(8); - box.setPadding(new Insets(8)); - - SpinnerPane spinnerPane = new SpinnerPane(); - ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); - scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - FXUtils.setOverflowHidden(scrollPane, 8); - - loadChangelog(object, spinnerPane, scrollPane); - spinnerPane.setOnFailedAction(e -> loadChangelog(object, spinnerPane, scrollPane)); - - spinnerPane.setContent(scrollPane); - box.getChildren().add(spinnerPane); - VBox.setVgrow(spinnerPane, Priority.SOMETIMES); - - this.setBody(box); - - JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); - versionPageBtn.setDisable(true); - loadVersionPageUrl(object, versionPageBtn); - - JFXButton closeButton = new JFXButton(i18n("button.ok")); - closeButton.getStyleClass().add("dialog-accept"); - closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - - setActions(versionPageBtn, closeButton); - - this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7)); - this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.7)); - - onEscPressed(this, closeButton::fire); - } - - private void loadChangelog(AddonUpdateObject object, SpinnerPane spinnerPane, ScrollPane scrollPane) { - spinnerPane.setLoading(true); - RemoteAddonRepository repo = object.data.targetVersion().source().getRepository(); - Task.supplyAsync(() -> { - if (object.changelog != null) return object.changelog; - RemoteAddon.Version version = object.data.targetVersion(); - return StringUtils.convertToHtml( - repo.getAddonChangelog(DownloadProviders.getDownloadProvider(), version.projectId(), version.versionId()), - "238222".equals(object.data.targetVersion().projectId()) - ); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - object.changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty"); - scrollPane.setContent(FXUtils.renderAddonChangelog(object.changelog, repo.getBaseUrl())); - FXUtils.smoothScrolling(scrollPane); - spinnerPane.setFailedReason(null); - } else { - spinnerPane.setFailedReason(i18n("download.failed.refresh")); - } - spinnerPane.setLoading(false); - }).start(); - } - - private void loadVersionPageUrl(AddonUpdateObject object, JFXHyperlink button) { - Task.supplyAsync(() -> - object.data.targetVersion().source().getRepository().getVersionPageUrl(object.data.targetVersion()) - ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null && StringUtils.isNotBlank(result)) { - button.setExternalLink(result); - button.setDisable(false); - } else { - LOG.warning("Failed to load addon version page url", exception); - } - }).start(); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java deleted file mode 100644 index dbd7a7ac90..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.beans.property.BooleanProperty; -import javafx.collections.ObservableList; -import javafx.scene.control.Skin; -import javafx.stage.FileChooser; -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.addon.datapack.DataPack; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.ListPageBase; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.javafx.MappedObservableList; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class DataPackListPage extends ListPageBase implements WorldManagePage.WorldRefreshable { - private final World world; - private final DataPack dataPack; - final BooleanProperty readOnly; - - public DataPackListPage(WorldManagePage worldManagePage) { - world = worldManagePage.getWorld(); - dataPack = new DataPack(world.getFile().resolve("datapacks")); - setItems(MappedObservableList.create(dataPack.getPacks(), DataPackListPageSkin.DataPackInfoObject::new)); - readOnly = worldManagePage.readOnlyProperty(); - FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), - this::installMultiDataPack, this::refresh); - - refresh(); - } - - private void installMultiDataPack(List dataPackPath) { - dataPackPath.forEach(this::installSingleDataPack); - if (readOnly.get()) { - Controllers.showToast(i18n("datapack.reload.toast")); - } - } - - private void installSingleDataPack(Path dataPack) { - try { - this.dataPack.installPack(dataPack, world.getGameVersion()); - } catch (IOException | IllegalArgumentException e) { - LOG.warning("Unable to parse datapack file " + dataPack, e); - } - } - - @Override - protected Skin createDefaultSkin() { - return new DataPackListPageSkin(this); - } - - public void refresh() { - setLoading(true); - Task.runAsync(dataPack::loadFromDir) - .withRunAsync(Schedulers.javafx(), () -> setLoading(false)) - .start(); - } - - public void add() { - FileChooser chooser = new FileChooser(); - chooser.setTitle(i18n("datapack.add.title")); - chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip")); - List res = Controllers.showOpenMultipleDialog(chooser); - if (res != null) { - installMultiDataPack(res); - } - - dataPack.loadFromDir(); - } - - void removeSelected(ObservableList selectedItems) { - List failed = new ArrayList<>(); - selectedItems.stream() - .map(DataPackListPageSkin.DataPackInfoObject::getPackInfo) - .forEach(pack -> { - try { - dataPack.deletePack(pack); - } catch (IOException e) { - failed.add(pack.getPath().toString()); - LOG.warning("Failed to delete datapack \"" + pack.getId() + "\"", e); - } - }); - if (!failed.isEmpty()) - Controllers.dialog(i18n("datapack.delete.failed") + "\n" + String.join(",\n", failed), - i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - - void setSelectedEnabled(ObservableList selectedItems, boolean enabled) { - List failed = new ArrayList<>(); - selectedItems.stream() - .map(DataPackListPageSkin.DataPackInfoObject::getPackInfo) - .forEach(pack -> { - try { - pack.setActive(enabled); - } catch (IOException e) { - failed.add(pack.getPath().toString()); - LOG.warning("Failed to enable or disable datapack \"" + pack.getId() + "\"", e); - } - }); - if (!failed.isEmpty()) - Controllers.dialog(i18n("datapack.toggle.failed") + "\n" + String.join(",\n", failed), - i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - - void openDataPackFolder() { - FXUtils.openFolder(dataPack.getPath()); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java deleted file mode 100644 index aec84dd601..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java +++ /dev/null @@ -1,370 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXSwitch; -import com.jfoenix.controls.JFXTextField; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.WeakInvalidationListener; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -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.SelectionMode; -import javafx.scene.control.SkinBase; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; -import javafx.util.Duration; -import org.jackhuang.hmcl.addon.datapack.DataPack; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.AnimationUtils; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.CompressingUtils; -import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; -import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.function.Predicate; -import java.util.regex.PatternSyntaxException; - -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -@NotNullByDefault -final class DataPackListPageSkin extends SkinBase { - - private final TransitionPane toolbarPane; - private final HBox searchBar; - private final HBox normalToolbar; - private final HBox selectingToolbar; - - private final JFXListView listView; - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - private final JFXTextField searchField; - - /// Timer for debouncing search input to avoid executing search on every keystroke. - private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); - - DataPackListPageSkin(DataPackListPage skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - listView = new JFXListView<>(); - listView.getStyleClass().add("no-horizontal-scrollbar"); - - // reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker - var selectAllButton = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())); - - ListChangeListener listener = change -> selectAllButton.setDisable(!listView.getItems().isEmpty() - && listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size()); - - { - toolbarPane = new TransitionPane(); - searchBar = new HBox(); - normalToolbar = new HBox(); - selectingToolbar = new HBox(); - - normalToolbar.getChildren().addAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openDataPackFolder), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()), null)); - JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)); - JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)); - removeButton.disableProperty().bind(getSkinnable().readOnly); - enableButton.disableProperty().bind(getSkinnable().readOnly); - disableButton.disableProperty().bind(getSkinnable().readOnly); - - listView.getSelectionModel().getSelectedItems().addListener(listener); - - selectingToolbar.getChildren().addAll( - removeButton, - enableButton, - disableButton, - selectAllButton, - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - searchPause.setOnFinished(e -> search()); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - searchPause.setRate(1); - searchPause.playFromStart(); - } - }); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(normalToolbar); - - searchField.clear(); - searchPause.stop(); - - isSearching.set(false); - Bindings.bindContent(listView.getItems(), skinnable.getItems()); - }); - FXUtils.onEscPressed(searchField, closeSearchBar::fire); - searchBar.getChildren().addAll(searchField, closeSearchBar); - - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> { - if (selectedItem == null) - changeToolbar(isSearching.get() ? searchBar : normalToolbar); - else - changeToolbar(selectingToolbar); - }); - FXUtils.setOverflowHidden(toolbarPane, 8); - root.getContent().add(toolbarPane); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - - listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("datapack.empty")); - placeholderLabel.textProperty().bind( - Bindings.createStringBinding(() -> { - if (isSearching.get()) { - return i18n("search.no_results_found"); - } else { - return i18n("datapack.empty"); - } - }, - isSearching) - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - Bindings.bindContent(listView.getItems(), skinnable.getItems()); - skinnable.getItems().addListener((ListChangeListener) change -> { - if (isSearching.get()) { - search(); - } - }); - listView.getItems().addListener(listener); - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here - FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } - - pane.getChildren().setAll(root); - getChildren().setAll(pane); - } - - /// Filters pack IDs and descriptions using the shared query syntax; invalid regular expressions leave the list empty. - private void search() { - isSearching.set(true); - Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); - - String queryString = searchField.getText(); - if (StringUtils.isBlank(queryString)) { - listView.getItems().setAll(getSkinnable().getItems()); - } else { - listView.getItems().clear(); - - Predicate<@Nullable String> predicate; - try { - predicate = StringUtils.compileQuery(queryString); - } catch (PatternSyntaxException e) { - LOG.warning("Illegal regular expression", e); - return; - } - - for (DataPackInfoObject item : getSkinnable().getItems()) { - if (predicate.test(item.getTitle()) || predicate.test(item.getSubtitle())) { - listView.getItems().add(item); - } - } - } - } - - private void changeToolbar(HBox newToolbar) { - @Nullable Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - // search button click will get focus while searchField request focus, this cause conflict. - // Defer focus request to next pulse avoids this conflict. - Platform.runLater(searchField::requestFocus); - } - } - } - - static class DataPackInfoObject { - private final DataPack.Pack packInfo; - private final ItemPropertyAsyncCache iconCache; - - DataPackInfoObject(DataPack.Pack packInfo) { - this.packInfo = packInfo; - this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); - } - - String getTitle() { - return packInfo.getId(); - } - - String getSubtitle() { - return packInfo.getDescription().toString(); - } - - DataPack.Pack getPackInfo() { - return packInfo; - } - - private Image getDefaultIcon() { - return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); - } - - private Image loadIcon() { - Image image = null; - Path imagePath; - if (this.getPackInfo().isDirectory()) { - imagePath = getPackInfo().getPath().resolve("pack.png"); - try { - image = FXUtils.loadImage(imagePath, 64, 64, true, true); - } catch (Exception e) { - LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e); - } - } else { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(getPackInfo().getPath())) { - imagePath = fs.getPath("/pack.png"); - if (Files.exists(imagePath)) { - image = FXUtils.loadImage(imagePath, 64, 64, true, true); - } - } catch (Exception e) { - LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e); - } - } - - if (image != null && !image.isError() && image.getWidth() > 0 && image.getHeight() > 0 && - Math.abs(image.getWidth() - image.getHeight()) < 1) { - return image; - } else { - return getDefaultIcon(); - } - } - } - - private static final class DataPackInfoListCell extends MDListCell { - private final JFXSwitch activeSwitch = new JFXSwitch(); - ImageContainer imageContainer = new ImageContainer(32); - final TwoLineListItem content = new TwoLineListItem(); - - private final InvalidationListener activeListener = obs -> activeSwitch.setSelected(getItem().getPackInfo().isActive()); - private final WeakInvalidationListener weakActiveListener = new WeakInvalidationListener(activeListener); - private @Nullable ReadOnlyBooleanProperty observedActiveProperty; - - /// Creates a selectable data pack row whose switch follows the read-only state. - DataPackInfoListCell(JFXListView listView, BooleanProperty isReadOnlyProperty) { - super(listView); - - HBox container = new HBox(8); - container.setPickOnBounds(false); - container.setAlignment(Pos.CENTER_LEFT); - container.setPadding(new Insets(0, 8, 0, 8)); - HBox.setHgrow(content, Priority.ALWAYS); - setSelectable(); - - activeSwitch.disableProperty().bind(isReadOnlyProperty); - activeSwitch.setSize(20); - activeSwitch.setMinSize(30, 30); - activeSwitch.setPrefSize(30, 30); - activeSwitch.setMaxSize(30, 30); - HBox.setMargin(activeSwitch, new Insets(0, 0, 0, 6)); - - imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png")); - - StackPane.setMargin(container, new Insets(8)); - container.getChildren().setAll(imageContainer, content, activeSwitch); - getContainer().getChildren().setAll(container); - } - - @Override - protected void updateControl(@Nullable DataPackInfoObject dataItem, boolean empty) { - if (observedActiveProperty != null) - observedActiveProperty.removeListener(weakActiveListener); - if (empty || dataItem == null) return; - content.setTitle(dataItem.getTitle()); - content.setSubtitle(dataItem.getSubtitle()); - - DataPack.Pack packInfo = dataItem.getPackInfo(); - (observedActiveProperty = packInfo.activeProperty()).addListener(weakActiveListener); - activeSwitch.setDisableAnimation(true); - activeSwitch.setSelected(packInfo.isActive()); - activeSwitch.setOnAction(event -> { - try { - packInfo.setActive(activeSwitch.isSelected()); - } catch (IOException e) { - LOG.warning("Unable to change state of datapack " + packInfo.getPath(), e); - Controllers.showToast(i18n("datapack.toggle.failed")); - } finally { - activeSwitch.setSelected(packInfo.isActive()); - } - }); - activeSwitch.setDisableAnimation(!AnimationUtils.isAnimationEnabled()); - dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty())); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java deleted file mode 100644 index fca50e264b..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java +++ /dev/null @@ -1,637 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXComboBox; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXTextField; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.ObjectBinding; -import javafx.beans.property.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.*; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.AddonProjectInfo; -import org.jackhuang.hmcl.addon.AddonProjectInfoRegistry; -import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; -import org.jackhuang.hmcl.download.DownloadCandidates; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.setting.DownloadProviders; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.*; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.javafx.BindingMapping; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.stream.Collectors; - -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; -import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; - -public class DownloadListPage extends Control implements DecoratorPage { - protected final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); - private final BooleanProperty loading = new SimpleBooleanProperty(false); - private final BooleanProperty failed = new SimpleBooleanProperty(false); - private final boolean instanceSelection; - private final ObjectProperty instanceReference = new SimpleObjectProperty<>(); - private final IntegerProperty pageOffset = new SimpleIntegerProperty(0); - private final IntegerProperty pageCount = new SimpleIntegerProperty(-1); - private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); - private final ObservableList instances = FXCollections.observableArrayList(); - private final ObjectProperty selectedInstance = new SimpleObjectProperty<>(); - private final DownloadPage.DownloadCallback callback; - private boolean searchInitialized = false; - protected final BooleanProperty supportChinese = new SimpleBooleanProperty(); - private final ObservableList actions = FXCollections.observableArrayList(); - protected final ListProperty downloadSources = new SimpleListProperty<>(this, "downloadSources", FXCollections.observableArrayList()); - protected final StringProperty downloadSource = new SimpleStringProperty(); - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private int searchID = 0; - protected final RemoteAddonRepository repository; - protected final @NotNull RemoteAddon.Type type; - private final DownloadProvider downloadProvider; - - private Runnable retrySearch; - - public DownloadListPage(@NotNull RemoteAddon.Type type, RemoteAddonRepository repository) { - this(type, repository, null, false); - } - - public DownloadListPage(@NotNull RemoteAddon.Type type, RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean instanceSelection) { - this.type = Objects.requireNonNull(type); - this.repository = repository; - this.callback = callback; - this.instanceSelection = instanceSelection; - this.downloadProvider = DownloadProviders.getDownloadProvider(); - } - - public DownloadProvider getDownloadProvider() { - return downloadProvider; - } - - public ObservableList getActions() { - return actions; - } - - public void loadInstance(HMCLGameInstance.Optional instance) { - this.instanceReference.set(instance); - - setLoading(false); - setFailed(false); - - if (!searchInitialized) { - searchInitialized = true; - search("", null, 0, "", RemoteAddonRepository.SortType.RELEVANCY); - } - - if (instanceSelection) { - HMCLGameRepository repository = instance.repository(); - instances.setAll(repository.getDisplayInstances() - .map(DefaultGameInstance::getId) - .toList()); - @Nullable HMCLGameInstance repositorySelection = repository.getSelectedInstance(); - selectedInstance.set(repositorySelection != null ? repositorySelection.getId() : null); - } - } - - public boolean isFailed() { - return failed.get(); - } - - public BooleanProperty failedProperty() { - return failed; - } - - public void setFailed(boolean failed) { - this.failed.set(failed); - } - - public boolean isLoading() { - return loading.get(); - } - - public BooleanProperty loadingProperty() { - return loading; - } - - public void setLoading(boolean loading) { - this.loading.set(loading); - } - - public void selectInstance(GameInstanceID instanceId) { - FXUtils.runInFX(() -> selectedInstance.set(instanceId)); - } - - public DownloadPage.DownloadCallback getCallback() { - return callback; - } - - private void search(String userGameVersion, RemoteAddonRepository.Category category, int pageOffset, String searchFilter, RemoteAddonRepository.SortType sort) { - retrySearch = null; - setLoading(true); - setFailed(false); - - int currentSearchID = searchID = searchID + 1; - Task.supplyAsync(() -> { - HMCLGameInstance.Optional instanceReference = this.instanceReference.get(); - @Nullable HMCLGameInstance instance = instanceReference.instance(); - if (instance == null) { - return userGameVersion; - } else { - GameVersionNumber version = instance.getVersion(); - return version != GameVersionNumber.unknown() ? version.toString() : ""; - } - }).thenApplyAsync( - gameVersion -> repository.search(downloadProvider, type, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteAddonRepository.SortOrder.DESC) - ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (searchID != currentSearchID) { - return; - } - - setLoading(false); - if (exception == null) { - items.setAll(result.results().collect(Collectors.toList())); - pageCount.set(result.totalPages()); - failed.set(false); - } else { - failed.set(true); - pageCount.set(-1); - retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort); - } - }).executor(true); - } - - protected String getLocalizedCategory(String category, Object self) { - return repository instanceof ModrinthRemoteAddonRepository - ? i18n("modrinth.category." + category) - : i18n("curse.category." + category); - } - - protected boolean shouldDisplayCategory(String category) { - return !"minecraft".equals(category); - } - - private String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) { - return StringUtils.repeats(' ', category.indent * 4) + - (category.category() == null - ? i18n("curse.category.0") - : getLocalizedCategory(category.category().id(), category.category().self())); - } - - protected String getLocalizedOfficialPage() { - if (repository instanceof ModrinthRemoteAddonRepository) { - return i18n("addon.modrinth"); - } else { - return i18n("addon.curseforge"); - } - } - - protected HMCLGameInstance.Optional getInstanceOptional() { - if (instanceSelection) { - @Nullable GameInstanceID instanceId = selectedInstance.get(); - return HMCLGameInstance.Optional.of(instanceReference.get().repository(), instanceId); - } else { - return instanceReference.get(); - } - } - - @Override - public ReadOnlyObjectProperty stateProperty() { - return state.getReadOnlyProperty(); - } - - @Override - protected Skin createDefaultSkin() { - return new ModDownloadListPageSkin(this); - } - - private static class ModDownloadListPageSkin extends SkinBase { - private final JFXListView listView = new JFXListView<>(); - private final RemoteImageLoader iconLoader; - - protected ModDownloadListPageSkin(DownloadListPage control) { - super(control); - - listView.getStyleClass().add("no-horizontal-scrollbar"); - - iconLoader = new RemoteImageLoader(control.downloadProvider) { - @Override - protected @NotNull Task createLoadTask(@NotNull DownloadCandidates urls) { - return FXUtils.getRemoteImageTask(urls, 80, 80, true, true); - } - }; - - BorderPane pane = new BorderPane(); - - GridPane searchPane = new GridPane(); - pane.setTop(searchPane); - searchPane.getStyleClass().addAll("card"); - BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10)); - - ColumnConstraints nameColumn = new ColumnConstraints(); - nameColumn.setMinWidth(USE_PREF_SIZE); - ColumnConstraints column1 = new ColumnConstraints(); - column1.setHgrow(Priority.ALWAYS); - ColumnConstraints column2 = new ColumnConstraints(); - column2.setHgrow(Priority.ALWAYS); - searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2); - - searchPane.setHgap(16); - searchPane.setVgap(10); - - { - int rowIndex = 0; - - if (control.instanceSelection || !control.downloadSources.isEmpty()) { - searchPane.addRow(rowIndex); - int columns = 0; - Node lastNode = null; - if (control.instanceSelection) { - JFXComboBox instancesComboBox = new JFXComboBox<>(); - instancesComboBox.setMaxWidth(Double.MAX_VALUE); - Bindings.bindContent(instancesComboBox.getItems(), control.instances); - selectedItemPropertyFor(instancesComboBox).bindBidirectional(control.selectedInstance); - - searchPane.add(new Label(i18n("instance")), columns++, rowIndex); - searchPane.add(lastNode = instancesComboBox, columns++, rowIndex); - } - - if (control.downloadSources.getSize() > 1) { - JFXComboBox downloadSourceComboBox = new JFXComboBox<>(); - downloadSourceComboBox.setMaxWidth(Double.MAX_VALUE); - downloadSourceComboBox.getItems().setAll(control.downloadSources.get()); - downloadSourceComboBox.setConverter(stringConverter(I18n::i18n)); - selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource); - - searchPane.add(new Label(i18n("settings.launcher.download_source")), columns++, rowIndex); - searchPane.add(lastNode = downloadSourceComboBox, columns++, rowIndex); - } - - if (columns == 2) { - GridPane.setColumnSpan(lastNode, 3); - } - - rowIndex++; - } - - JFXTextField nameField = new JFXTextField(); - nameField.setPromptText(getSkinnable().supportChinese.get() ? i18n("search.hint.chinese") : i18n("search.hint.english")); - if (getSkinnable().supportChinese.get()) { - FXUtils.installFastTooltip(nameField, i18n("search.hint.chinese")); - } else { - FXUtils.installFastTooltip(nameField, i18n("search.hint.english")); - } - - JFXComboBox gameVersionField = new JFXComboBox<>(); - gameVersionField.setMaxWidth(Double.MAX_VALUE); - gameVersionField.setEditable(true); - gameVersionField.getItems().setAll(GameVersionNumber.getDefaultGameVersions()); - Label lblGameVersion = new Label(i18n("world.game_version")); - searchPane.addRow(rowIndex++, new Label(i18n("mods.name")), nameField, lblGameVersion, gameVersionField); - - ObjectBinding hasVersion = BindingMapping.of(getSkinnable().instanceReference) - .map(instanceReference -> instanceReference.instanceId() == null); - lblGameVersion.managedProperty().bind(hasVersion); - lblGameVersion.visibleProperty().bind(hasVersion); - gameVersionField.managedProperty().bind(hasVersion); - gameVersionField.visibleProperty().bind(hasVersion); - FXUtils.installFastTooltip(gameVersionField, i18n("search.enter")); - - FXUtils.onChangeAndOperate(getSkinnable().instanceReference, instanceReference -> { - if (instanceReference.instanceId() != null) { - GridPane.setColumnSpan(nameField, 3); - } else { - GridPane.setColumnSpan(nameField, 1); - } - }); - - StackPane categoryStackPane = new StackPane(); - JFXComboBox categoryComboBox = new JFXComboBox<>(); - categoryComboBox.getItems().setAll(CategoryIndented.ALL); - categoryStackPane.getChildren().setAll(categoryComboBox); - categoryComboBox.prefWidthProperty().bind(categoryStackPane.widthProperty()); - categoryComboBox.getStyleClass().add("fit-width"); - categoryComboBox.setPromptText(i18n("addon.category")); - categoryComboBox.getSelectionModel().select(0); - categoryComboBox.setConverter(stringConverter(getSkinnable()::getLocalizedCategoryIndent)); - FXUtils.onChangeAndOperate(getSkinnable().downloadSource, downloadSource -> { - categoryComboBox.getItems().setAll(CategoryIndented.ALL); - categoryComboBox.getSelectionModel().select(0); - - Task.supplyAsync(() -> getSkinnable().repository.getCategories(getSkinnable().type)) - .thenAcceptAsync(Schedulers.javafx(), categories -> { - if (!Objects.equals(getSkinnable().downloadSource.get(), downloadSource)) { - return; - } - - List result = new ArrayList<>(); - result.add(CategoryIndented.ALL); - for (RemoteAddonRepository.Category category : Lang.toIterable(categories)) { - resolveCategory(category, 0, result); - } - categoryComboBox.getItems().setAll(result); - categoryComboBox.getSelectionModel().select(0); - }).start(); - }); - - StackPane sortStackPane = new StackPane(); - JFXComboBox sortComboBox = new JFXComboBox<>(); - sortStackPane.getChildren().setAll(sortComboBox); - sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty()); - sortComboBox.getStyleClass().add("fit-width"); - sortComboBox.setConverter(stringConverter(sortType -> i18n("addon.sort." + sortType.name().toLowerCase(Locale.ROOT)))); - sortComboBox.getItems().setAll(RemoteAddonRepository.SortType.values()); - sortComboBox.getSelectionModel().select(0); - searchPane.addRow(rowIndex++, new Label(i18n("addon.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane); - - IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0); - IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1); - EventHandler searchAction = e -> { - iconLoader.clearInvalidCache(); - if (currentFilterID.get() != -1 && currentFilterID.get() != filterID.get()) { - control.pageOffset.set(0); - } - currentFilterID.set(filterID.get()); - - int pageOffset = control.pageOffset.get(); - getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(), - Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem()) - .map(CategoryIndented::category) - .orElse(null), - pageOffset == -1 ? 0 : pageOffset, - nameField.getText(), - sortComboBox.getSelectionModel().getSelectedItem()); - }; - - control.listenerHolder.add(FXUtils.observeWeak( - () -> filterID.set(filterID.get() + 1), - - control.downloadSource, - gameVersionField.getSelectionModel().selectedItemProperty(), - categoryComboBox.getSelectionModel().selectedItemProperty(), - nameField.textProperty(), - sortComboBox.getSelectionModel().selectedItemProperty() - )); - - HBox actionsBox = new HBox(8); - GridPane.setColumnSpan(actionsBox, 4); - actionsBox.setAlignment(Pos.CENTER); - { - AggregatedObservableList actions = new AggregatedObservableList<>(); - - Holder changeButton = new Holder<>(); - - JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page")); - firstPageButton.setOnAction(event -> { - control.pageOffset.set(0); - searchAction.handle(event); - changeButton.value.run(); - }); - - JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page")); - previousPageButton.setOnAction(event -> { - int pageOffset = control.pageOffset.get(); - if (pageOffset > 0) { - control.pageOffset.set(pageOffset - 1); - searchAction.handle(event); - changeButton.value.run(); - } - }); - - Label pageDescription = new Label(); - pageDescription.textProperty().bind(Bindings.createStringBinding(() -> { - int pageCount = control.pageCount.get(); - return i18n("search.page_n", control.pageOffset.get() + 1, pageCount == -1 ? "-" : String.valueOf(pageCount)); - }, control.pageOffset, control.pageCount)); - - JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page")); - nextPageButton.setOnAction(event -> { - int nv = control.pageOffset.get() + 1; - if (nv < control.pageCount.get()) { - control.pageOffset.set(nv); - searchAction.handle(event); - changeButton.value.run(); - } - }); - - JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page")); - lastPageButton.setOnAction(event -> { - control.pageOffset.set(control.pageCount.get() - 1); - searchAction.handle(event); - changeButton.value.run(); - }); - - firstPageButton.setDisable(true); - previousPageButton.setDisable(true); - lastPageButton.setDisable(true); - nextPageButton.setDisable(true); - - changeButton.value = () -> { - int pageOffset = control.pageOffset.get(); - int pageCount = control.pageCount.get(); - - boolean disableAll = pageCount >= -1 && pageCount <= 1; - - boolean disablePrevious = disableAll || pageOffset == 0; - firstPageButton.setDisable(disablePrevious); - previousPageButton.setDisable(disablePrevious); - - boolean disableNext = disableAll || pageOffset == pageCount - 1; - nextPageButton.setDisable(disableNext); - lastPageButton.setDisable(disableNext); - - listView.scrollTo(0); - }; - - FXUtils.onChange(control.pageCount, pageCountN -> { - int pageCount = pageCountN.intValue(); - - if (pageCount != -1) { - if (control.pageOffset.get() + 1 >= pageCount) { - control.pageOffset.set(pageCount - 1); - } - } - - changeButton.value.run(); - }); - - FXUtils.onChange(control.pageOffset, pageOffsetN -> { - changeButton.value.run(); - }); - - Pane placeholder = new Pane(); - HBox.setHgrow(placeholder, Priority.SOMETIMES); - - JFXButton searchButton = FXUtils.newRaisedButton(i18n("search")); - searchButton.setOnAction(searchAction); - - actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageDescription, nextPageButton, lastPageButton, placeholder, searchButton)); - actions.appendList(control.actions); - Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList()); - } - - searchPane.addRow(rowIndex++, actionsBox); - - FXUtils.onChange(control.downloadSource, v -> searchAction.handle(null)); - nameField.setOnAction(searchAction); - gameVersionField.setOnAction(searchAction); - categoryComboBox.setOnAction(searchAction); - sortComboBox.setOnAction(searchAction); - } - - SpinnerPane spinnerPane = new SpinnerPane(); - pane.setCenter(spinnerPane); - { - spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty()); - spinnerPane.failedReasonProperty().bind( - Bindings.createStringBinding(() -> { - if (getSkinnable().isFailed()) { - return i18n("download.failed.refresh"); - } else if (!getSkinnable().isLoading() && getSkinnable().pageCount.get() >= 0 && getSkinnable().items.isEmpty()) { - return i18n("search.no_results_found"); - } else { - return null; - } - }, - getSkinnable().failedProperty(), - getSkinnable().loadingProperty(), - getSkinnable().pageCount, - getSkinnable().items) - ); - spinnerPane.setOnFailedAction(e -> { - if (getSkinnable().isFailed() && getSkinnable().retrySearch != null) { - getSkinnable().retrySearch.run(); - } - }); - - spinnerPane.setContent(listView); - Bindings.bindContent(listView.getItems(), getSkinnable().items); - listView.setSelectionModel(new NoneMultipleSelectionModel<>()); - // ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - listView.setCellFactory(x -> new DownloadListCell(getSkinnable(), listView, iconLoader)); - } - - this.getChildren().setAll(pane); - } - - private record CategoryIndented(int indent, RemoteAddonRepository.Category category) { - private static final CategoryIndented ALL = new CategoryIndented(0, null); - } - - private static void resolveCategory(RemoteAddonRepository.Category category, int indent, List result) { - result.add(new CategoryIndented(indent, category)); - for (RemoteAddonRepository.Category subcategory : category.subcategories()) { - resolveCategory(subcategory, indent + 1, result); - } - } - } - - private static final class DownloadListCell extends ListCell { - private final DownloadListPage page; - private final JFXListView listView; - private final RemoteImageLoader iconLoader; - - private static final Insets PADDING = new Insets(9, 9, 0, 9); - private static final Insets LAST_PADDING = new Insets(9, 9, 9, 9); - - private final RipplerContainer graphic; - private final StackPane wrapper = new StackPane(); - - private final TwoLineListItem content = new TwoLineListItem(); - private final ImageContainer imageContainer = new ImageContainer(40); - - /// Creates a download card with its icon and text laid out by the same row. - private DownloadListCell(DownloadListPage page, JFXListView listView, RemoteImageLoader iconLoader) { - this.page = page; - this.listView = listView; - this.iconLoader = iconLoader; - - setPadding(PADDING); - - content.setPadding(new Insets(8)); - content.setCursor(Cursor.HAND); - TwoLineListItem.setMargin(imageContainer, new Insets(0, 8, 0, 0)); - imageContainer.setMouseTransparent(true); - content.setLeading(imageContainer); - - this.graphic = new RipplerContainer(content); - wrapper.getChildren().setAll(this.graphic); - wrapper.getStyleClass().add("card-no-padding"); - - FXUtils.onClicked(wrapper, () -> { - RemoteAddon item = getItem(); - if (item != null) { - Controllers.navigate(new DownloadPage(page, item, page.getInstanceOptional(), page.callback)); - } - }); - - setPrefWidth(0); - - FXUtils.limitCellWidth(listView, this); - } - - /// Updates addon text and replaces category tags together so their labels can be reused. - @Override - protected void updateItem(@Nullable RemoteAddon item, boolean empty) { - @Nullable RemoteAddon oldItem = getItem(); - boolean oldEmpty = isEmpty(); - - super.updateItem(item, empty); - - if (oldItem == item && oldEmpty == empty) return; - - this.graphic.resetFeedback(); - - if (empty || item == null) { - setGraphic(null); - } else { - setPadding( - getIndex() == listView.getItems().size() - 1 - ? LAST_PADDING - : PADDING - ); - - @Nullable AddonProjectInfoRegistry registry = AddonProjectInfoRegistry.getByAddonType(page.type); - @Nullable AddonProjectInfo mod = registry != null ? registry.getInfoByCurseForgeId(item.slug()) : null; - content.setTitle(mod != null && I18n.isUseChinese() ? mod.displayName() : item.title()); - @Nullable String description = item.description(); - if (description != null) { - description = description.replaceAll("\\R", " "); - } - content.setSubtitle(description); - content.getTags().setAll(item.categories().stream() - .filter(page::shouldDisplayCategory) - .map(category -> new Tag(page.getLocalizedCategory(category, null))) - .toList()); - iconLoader.load(imageContainer.imageProperty(), item.iconUrl()); - setGraphic(wrapper); - } - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java deleted file mode 100644 index 039b64b1c5..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java +++ /dev/null @@ -1,722 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXDialogLayout; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.control.*; -import javafx.scene.layout.*; -import javafx.stage.FileChooser; -import org.jackhuang.hmcl.addon.AddonLoader; -import org.jackhuang.hmcl.addon.AddonProjectInfo; -import org.jackhuang.hmcl.addon.AddonProjectInfoRegistry; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.addon.mod.ModLoaderType; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.SVGContainer; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.*; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; - -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public class DownloadPage extends Control implements DecoratorPage { - - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); - private final BooleanProperty loaded = new SimpleBooleanProperty(false); - private final BooleanProperty loading = new SimpleBooleanProperty(false); - private final BooleanProperty failed = new SimpleBooleanProperty(false); - private final RemoteAddonRepository repository; - private final RemoteAddon addon; - /// Localized project metadata, or null when no registry or matching project is available. - private final @Nullable AddonProjectInfo mod; - private final HMCLGameInstance.Optional instanceReference; - private final DownloadCallback callback; - private final DownloadListPage page; - private final RemoteAddon.Type type; - - private SimpleMultimap> versions; - - public DownloadPage(DownloadListPage page, RemoteAddon addon, HMCLGameInstance.Optional instanceReference, @Nullable DownloadCallback callback) { - this.page = page; - this.repository = page.repository; - this.addon = addon; - this.type = Objects.requireNonNullElse(addon.type(), page.type); - @Nullable AddonProjectInfoRegistry registry = AddonProjectInfoRegistry.getByAddonType(this.type); - this.mod = registry != null ? registry.getInfoByCurseForgeId(addon.slug()) : null; - this.instanceReference = instanceReference; - this.callback = callback; - loadAddonVersions(); - - this.state.set(State.fromTitle(addon.title())); - } - - private void loadAddonVersions() { - setLoading(true); - setFailed(false); - - Task.supplyAsync(() -> { - Stream versions = addon.data().loadVersions(repository, page.getDownloadProvider()); - return sortVersions(versions); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - this.versions = result; - - loaded.set(true); - setFailed(false); - } else { - setFailed(true); - } - setLoading(false); - }).start(); - } - - private SimpleMultimap> sortVersions(Stream versions) { - SimpleMultimap> classifiedVersions - = new SimpleMultimap<>(HashMap::new, ArrayList::new); - versions.forEach(version -> { - for (String gameVersion : version.gameVersions()) { - classifiedVersions.put(gameVersion, version); - } - }); - - for (String gameVersion : classifiedVersions.keys()) { - List versionList = classifiedVersions.get(gameVersion); - versionList.sort(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()); - } - return classifiedVersions; - } - - public RemoteAddon getAddon() { - return addon; - } - - public HMCLGameInstance.Optional getInstanceOptional() { - return instanceReference; - } - - public boolean isLoading() { - return loading.get(); - } - - public BooleanProperty loadingProperty() { - return loading; - } - - public void setLoading(boolean loading) { - this.loading.set(loading); - } - - public boolean isFailed() { - return failed.get(); - } - - public BooleanProperty failedProperty() { - return failed; - } - - public void setFailed(boolean failed) { - this.failed.set(failed); - } - - public void download(RemoteAddon addon, RemoteAddon.Version file) { - if (this.callback == null) { - saveAs(file); - } else { - this.callback.download(page.getDownloadProvider(), instanceReference.repository(), instanceReference.instanceId(), addon, file); - } - } - - public void saveAs(RemoteAddon.Version file) { - String extension = StringUtils.substringAfterLast(file.file().filename(), '.'); - - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(i18n("button.save_as")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension)); - fileChooser.setInitialFileName(file.file().filename()); - Path dest = Controllers.showSaveDialog(fileChooser); - if (dest == null) { - return; - } - - Controllers.taskDialog( - Task.composeAsync(() -> { - var task = new FileDownloadTask(file.file().url(), dest, file.file().getIntegrityCheck()); - task.setName(file.name()); - return task; - }), - i18n("message.downloading"), - TaskCancellationAction.NORMAL); - } - - @Override - public ReadOnlyObjectProperty stateProperty() { - return state.getReadOnlyProperty(); - } - - @Override - protected Skin createDefaultSkin() { - return new DownloadPageSkin(this); - } - - private static class DownloadPageSkin extends SkinBase { - - protected DownloadPageSkin(DownloadPage control) { - super(control); - - VBox pane = new VBox(8); - pane.getStyleClass().add("gray-background"); - pane.setPadding(new Insets(10)); - - HBox descriptionPane = new HBox(8); - descriptionPane.setMinHeight(Region.USE_PREF_SIZE); - descriptionPane.setAlignment(Pos.CENTER); - pane.getChildren().add(descriptionPane); - descriptionPane.getStyleClass().add("card-non-transparent"); - { - var imageContainer = new ImageContainer(40); - if (StringUtils.isNotBlank(getSkinnable().addon.iconUrl())) { - imageContainer.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.iconUrl(), 80, 80, true, true)); - } - descriptionPane.getChildren().add(imageContainer); - - TwoLineListItem content = new TwoLineListItem(); - HBox.setHgrow(content, Priority.ALWAYS); - @Nullable AddonProjectInfo mod = getSkinnable().mod; - content.setTitle(mod != null && I18n.isUseChinese() ? mod.displayName() : getSkinnable().addon.title()); - content.setSubtitle(getSkinnable().addon.description()); - content.getSubtitleLabel().setWrapText(true); - getSkinnable().addon.categories().stream() - .filter(category -> getSkinnable().page.shouldDisplayCategory(category)) - .map(category -> getSkinnable().page.getLocalizedCategory(category, null)) - .forEach(content::addTag); - descriptionPane.getChildren().add(content); - - @Nullable String mcmodUrl = mod != null ? mod.mcmodUrl() : null; - if (mcmodUrl != null) { - JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod")); - openMcmodButton.setExternalLink(mcmodUrl); - descriptionPane.getChildren().add(openMcmodButton); - openMcmodButton.setMinWidth(Region.USE_PREF_SIZE); - } - - JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage()); - openUrlButton.setExternalLink(getSkinnable().addon.pageUrl()); - descriptionPane.getChildren().add(openUrlButton); - openUrlButton.setMinWidth(Region.USE_PREF_SIZE); - } - - SpinnerPane spinnerPane = new SpinnerPane(); - VBox.setVgrow(spinnerPane, Priority.ALWAYS); - pane.getChildren().add(spinnerPane); - { - spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty()); - spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> { - if (getSkinnable().isFailed()) { - return i18n("download.failed.refresh"); - } else { - return null; - } - }, getSkinnable().failedProperty())); - spinnerPane.setOnFailedAction(e -> getSkinnable().loadAddonVersions()); - - ComponentList list = new ComponentList(); - ScrollPane scrollPane = new ScrollPane(list); - FXUtils.smoothScrolling(scrollPane); - scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - FXUtils.setOverflowHidden(scrollPane, 8); - StackPane.setAlignment(scrollPane, Pos.TOP_CENTER); - spinnerPane.setContent(scrollPane); - - FXUtils.onChangeAndOperate(control.loaded, loaded -> { - if (control.versions == null) return; - - @Nullable HMCLGameInstance instance = control.instanceReference.instance(); - if (instance != null) { - String gameVersion = instance.getVersion().toString(); - if (!GameVersionNumber.unknown().equals(instance.getVersion()) - && control.versions.containsKey(gameVersion)) { - List addonVersions = control.versions.get(gameVersion); - if (addonVersions != null && !addonVersions.isEmpty()) { - Set targetLoaders = instance.getModLoaders(); - - resolve: - for (RemoteAddon.Version addonVersion : addonVersions) { - if (getSkinnable().type == RemoteAddon.Type.MOD) { - for (AddonLoader loader : addonVersion.loaders()) { - if (loader.type() instanceof ModLoaderType modLoaderType && targetLoaders.contains(modLoaderType)) { - list.getContent().addAll( - ComponentList.createComponentListTitle(i18n("addon.download.recommend", gameVersion)), - new AddonItem(control.addon, addonVersion, control) - ); - break resolve; - } - } - } else { - list.getContent().addAll( - ComponentList.createComponentListTitle(i18n("addon.download.recommend", gameVersion)), - new AddonItem(control.addon, addonVersion, control) - ); - break; - } - } - } - } - } else if (getSkinnable().type == RemoteAddon.Type.MODPACK) { - control.versions - .keys() - .stream() - .max(Comparator.comparing(GameVersionNumber::asGameVersion)) - .flatMap(gameVersion -> - control.versions.get(gameVersion) - .stream() - .max(Comparator.comparing(RemoteAddon.Version::datePublished)) - .map(v -> Pair.pair(gameVersion, v)) - ).ifPresent(pair -> - list.getContent().addAll( - ComponentList.createComponentListTitle(i18n("addon.download.recommend", pair.key())), - new AddonItem(control.addon, pair.value(), control) - ) - ); - } - - final class Versions { - static ComponentSublist createSublist(DownloadPage control, String title, List target) { - var sublist = new ComponentSublist(() -> { - ArrayList items = new ArrayList<>(); - for (String gv : target) { - List versions = control.versions.get(gv); - if (versions != null) { - for (RemoteAddon.Version v : versions) { - items.add(new AddonItem(control.addon, v, control)); - } - } - } - return items; - }); - - sublist.setTitle(title); - sublist.getStyleClass().add("no-padding"); - - return sublist; - } - - final List releases = new ArrayList<>(); - final List snapshots = new ArrayList<>(); - } - - TreeMap map = new TreeMap<>(Collections.reverseOrder()); - for (String version : control.versions.keys()) { - GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(version); - GameVersionNumber releaseOfSnapshot = GameVersionNumber.getReleaseOfSnapshot(gameVersion); - GameVersionNumber releaseVersion = Objects.requireNonNullElse(releaseOfSnapshot, gameVersion); - - Versions lists = map.computeIfAbsent(releaseVersion, v -> new Versions()); - - if (releaseOfSnapshot == null) { - lists.releases.add(version); - } else { - lists.snapshots.add(version); - } - } - - for (Map.Entry entry : map.entrySet()) { - GameVersionNumber gameVersion = entry.getKey(); - Versions versions = entry.getValue(); - - if (!versions.releases.isEmpty()) - list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.release", gameVersion), versions.releases)); - - if (!versions.snapshots.isEmpty()) - list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.snapshot", gameVersion), versions.snapshots)); - } - }); - } - - getChildren().setAll(pane); - } - } - - /// Displays an addon dependency and opens its download page when activated. - private static final class DependencyAddonItem extends LineButton { - /// Returns the localized dependency type, or its lowercase enum name for other types. - public static String translateDependencyType(RemoteAddon.DependencyType dependencyType) { - return switch (dependencyType) { - case EMBEDDED -> i18n("addon.dependency.embedded"); - case OPTIONAL -> i18n("addon.dependency.optional"); - case REQUIRED -> i18n("addon.dependency.required"); - default -> dependencyType.name().toLowerCase(Locale.ROOT); - }; - } - - /// The dependency represented by this row. - public final RemoteAddon addon; - - /// Creates a dependency row that opens the addon's download page. - DependencyAddonItem(DownloadListPage page, RemoteAddon addon, HMCLGameInstance.Optional instanceReference) { - this.addon = addon; - - container.setPadding(new Insets(10, 24, 10, 24)); - container.getTitleLabel().setMinWidth(0); - setLargeTitle(true); - var imageView = new ImageContainer(40); - imageView.setMouseTransparent(true); - setMargin(imageView, new Insets(0, 8, 0, 0)); - setLeading(imageView); - FXUtils.setLimitHeight(this, 60); - - RemoteAddon.@Nullable Type type = addon.type(); - @Nullable DownloadCallback callback = type == null ? null : switch (type) { - case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK; - case SHADER_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; - default -> null; // Mods, modpacks, worlds, and custom types are not downloadable from this UI - }; - setOnAction((e) -> { - fireEvent(new DialogCloseEvent()); - Controllers.navigate(new DownloadPage(page, addon, instanceReference, callback)); - }); - @Nullable AddonProjectInfoRegistry registry = AddonProjectInfoRegistry.getByAddonType(type); - @Nullable AddonProjectInfo mod = registry != null ? registry.getInfoByCurseForgeId(addon.slug()) : null; - setTitle(mod != null && I18n.isUseChinese() ? mod.displayName() : addon.title()); - setSubtitle(addon.description()); - container.getSubtitleLabel().setWrapText(false); - for (String category : addon.categories()) { - if (page.shouldDisplayCategory(category)) - getTags().add(new Tag(page.getLocalizedCategory(category, null), Tag.Type.NORMAL)); - } - if (StringUtils.isNotBlank(addon.iconUrl())) { - imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true)); - } - } - } - - private static final class AddonItem extends StackPane { - - /// Creates a version summary that opens its details when clicked. - AddonItem(RemoteAddon mod, RemoteAddon.Version dataItem, DownloadPage selfPage) { - this(mod, dataItem, selfPage, true); - } - - /// Creates a version summary, optionally enabling navigation to its details. - AddonItem(RemoteAddon mod, RemoteAddon.Version dataItem, DownloadPage selfPage, boolean interactive) { - VBox pane = new VBox(8); - pane.setPadding(new Insets(8, 0, 8, 0)); - - { - HBox descPane = new HBox(8); - descPane.setPadding(new Insets(0, 8, 0, 8)); - descPane.setAlignment(Pos.CENTER_LEFT); - descPane.setPickOnBounds(false); - - { - StackPane graphicPane = new StackPane(); - graphicPane.setMouseTransparent(true); - TwoLineListItem content = new TwoLineListItem(); - HBox.setHgrow(content, Priority.ALWAYS); - content.setTitle(dataItem.name()); - content.setSubtitle(I18n.formatDateTime(dataItem.datePublished())); - - @Nullable SVG svg = null; - @Nullable String styleClass = null; - if (dataItem.versionType() != null) { - switch (dataItem.versionType()) { - case Alpha: - content.addTag(i18n("addon.channel.alpha")); - svg = SVG.ALPHA_CIRCLE; - styleClass = "alpha"; - break; - case Beta: - content.addTag(i18n("addon.channel.beta")); - svg = SVG.BETA_CIRCLE; - styleClass = "beta"; - break; - case Release: - content.addTag(i18n("addon.channel.release")); - svg = SVG.RELEASE_CIRCLE; - styleClass = "release"; - break; - default: - styleClass = ""; - break; - } - } - if (svg != null) { - SVGContainer icon = svg.createIcon(24); - icon.getStyleClass().addAll("addon-channel-icon", styleClass); - graphicPane.getChildren().setAll(icon); - } - - Set tags = new LinkedHashSet<>(); - for (AddonLoader loader : dataItem.loaders()) { - tags.add(I18n.translateLoaderName(loader)); - } - content.addTags(tags); - - descPane.getChildren().setAll(graphicPane, content); - } - - pane.getChildren().add(descPane); - } - - RipplerContainer container = new RipplerContainer(pane); - container.setPressFeedbackEnabled(interactive); - if (interactive) - FXUtils.onClicked(container, () -> Controllers.dialog(new AddonVersion(mod, dataItem, selfPage))); - getChildren().setAll(container); - - // Workaround for https://github.com/HMCL-dev/HMCL/issues/2129 - this.setMinHeight(50); - } - } - - private static final class AddonVersion extends JFXDialogLayout { - /// Creates version details with dependency downloads and a non-navigating summary. - public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage selfPage) { - RemoteAddon.Type type = selfPage.type; - - String title = switch (type) { - case RESOURCE_PACK -> "resourcepack.download.title"; - case SHADER_PACK -> "shaderpack.download.title"; - default -> "mods.download.title"; - }; - this.setHeading(new HBox(new Label(i18n(title, version.name())))); - - VBox box = new VBox(8); - box.setPadding(new Insets(8)); - var addonItem = new AddonItem(mod, version, selfPage, false); - box.getChildren().setAll(addonItem); - - JFXHyperlink changelogButton = new JFXHyperlink(i18n("addon.changelog")); - changelogButton.setOnAction(__ -> Controllers.dialog(new AddonChangelog(version, selfPage.repository, selfPage.page.getDownloadProvider()))); - - JFXHyperlink versionPageBtn = new JFXHyperlink(i18n("mods.url")); - versionPageBtn.setDisable(true); - loadVersionPageUrl(version, selfPage.repository, versionPageBtn); - - HBox additionalBox = new HBox(changelogButton, versionPageBtn); - box.getChildren().add(additionalBox); - - SpinnerPane spinnerPane = new SpinnerPane(); - ScrollPane scrollPane = new ScrollPane(); - ComponentList dependenciesList = new ComponentList(); - loadDependencies(version, selfPage, spinnerPane, dependenciesList); - spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList)); - - scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scrollPane.setContent(dependenciesList); - scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - FXUtils.onChangeAndOperate(scrollPane.widthProperty(), d -> FXUtils.setLimitWidth(dependenciesList, d.doubleValue())); - FXUtils.smoothScrolling(scrollPane); - FXUtils.setOverflowHidden(scrollPane, 8); - spinnerPane.setContent(scrollPane); - box.getChildren().add(spinnerPane); - VBox.setVgrow(spinnerPane, Priority.SOMETIMES); - - this.setBody(box); - - JFXButton downloadButton = null; - if (selfPage.callback != null) { - downloadButton = new JFXButton(i18n("mods.install")); - downloadButton.getStyleClass().add("dialog-accept"); - downloadButton.setOnAction(e -> { - if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { - fireEvent(new DialogCloseEvent()); - } - selfPage.download(mod, version); - }); - } - - JFXButton saveAsButton = new JFXButton(i18n("mods.save_as")); - saveAsButton.getStyleClass().add("dialog-accept"); - saveAsButton.setOnAction(e -> { - if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { - fireEvent(new DialogCloseEvent()); - } - selfPage.saveAs(version); - }); - - JFXButton cancelButton = new JFXButton(i18n("button.cancel")); - cancelButton.getStyleClass().add("dialog-cancel"); - cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - - if (downloadButton == null) { - this.setActions(saveAsButton, cancelButton); - } else { - this.setActions(downloadButton, saveAsButton, cancelButton); - } - - this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7)); - this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.8)); - - onEscPressed(this, cancelButton::fire); - } - - private void loadDependencies(RemoteAddon.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) { - spinnerPane.setLoading(true); - Task.composeAsync(() -> { - // TODO: Massive tasks may cause OOM. - EnumMap>> dependencies = new EnumMap<>(RemoteAddon.DependencyType.class); - AtomicBoolean hasBroken = new AtomicBoolean(false); - List> queue = new ArrayList<>(version.dependencies().size()); - for (RemoteAddon.Dependency dependency : version.dependencies()) { - if (dependency.getType() == RemoteAddon.DependencyType.INCOMPATIBLE) continue; - if (dependency.getType() == RemoteAddon.DependencyType.BROKEN) { - hasBroken.set(true); - continue; - } - - if (!dependencies.containsKey(dependency.getType())) { - Label title = new Label(DependencyAddonItem.translateDependencyType(dependency.getType())); - title.setPadding(new Insets(0, 8, 0, 8)); - List list = new ArrayList<>(); - dependencies.put(dependency.getType(), Pair.pair(title, list)); - } - - queue.add(Task.supplyAsync(Schedulers.io(), () -> dependency.load(selfPage.page.getDownloadProvider())) - .setSignificance(Task.TaskSignificance.MINOR) - .thenAcceptAsync(Schedulers.javafx(), dep -> { - if (dep == RemoteAddon.BROKEN) { - hasBroken.set(true); - return; - } - DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dep, selfPage.instanceReference); - var listener = FXUtils.onWeakChangeAndOperate(dependenciesList.widthProperty(), d -> FXUtils.setLimitWidth(dependencyAddonItem, d.doubleValue())); - dependencyAddonItem.getProperties().put("DependencyAddonItem.width", listener); - dependencies.get(dependency.getType()).value().add(dependencyAddonItem); - }) - .setSignificance(Task.TaskSignificance.MINOR)); - } - - return Task.allOf(queue).thenSupplyAsync(() -> { - var dependenciesStream = dependencies.values().stream().flatMap(types -> { - if (types.value().isEmpty()) return Stream.empty(); - return Stream.concat( - Stream.of(types.key()), - types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER))); - }); - if (!hasBroken.get()) { - return dependenciesStream; - } else { - Label warning = new Label(i18n("addon.dependencies.has_broken")); - warning.setWrapText(true); - warning.setPadding(new Insets(0, 8, 0, 8)); - return Stream.concat( - Stream.of(warning), - dependenciesStream - ); - } - }); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - dependenciesList.getContent().setAll(result.toList()); - spinnerPane.setFailedReason(null); - } else { - dependenciesList.getContent().setAll(); - spinnerPane.setFailedReason(i18n("download.failed.refresh")); - } - spinnerPane.setLoading(false); - }).start(); - } - - private void loadVersionPageUrl(RemoteAddon.Version version, RemoteAddonRepository repo, JFXHyperlink button) { - Task.supplyAsync(() -> repo.getVersionPageUrl(version)) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null && StringUtils.isNotBlank(result)) { - button.setExternalLink(result); - button.setDisable(false); - } else { - LOG.warning("Failed to load addon version page url", exception); - } - }) - .start(); - } - } - - private static final class AddonChangelog extends JFXDialogLayout { - - public AddonChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider) { - setHeading(new HBox(new Label(i18n("addon.changelog") + " - " + version.name()))); - - VBox box = new VBox(8); - box.setPadding(new Insets(8)); - - SpinnerPane spinnerPane = new SpinnerPane(); - ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); - scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - FXUtils.setOverflowHidden(scrollPane, 8); - - loadChangelog(version, repo, provider, spinnerPane, scrollPane); - spinnerPane.setOnFailedAction(e -> loadChangelog(version, repo, provider, spinnerPane, scrollPane)); - - spinnerPane.setContent(scrollPane); - box.getChildren().add(spinnerPane); - VBox.setVgrow(spinnerPane, Priority.SOMETIMES); - - this.setBody(box); - - JFXButton closeButton = new JFXButton(i18n("button.ok")); - closeButton.getStyleClass().add("dialog-accept"); - closeButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - - setActions(closeButton); - - this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7)); - this.prefHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.8)); - - onEscPressed(this, closeButton::fire); - } - - private void loadChangelog(RemoteAddon.Version version, RemoteAddonRepository repo, DownloadProvider provider, SpinnerPane spinnerPane, ScrollPane scrollPane) { - spinnerPane.setLoading(true); - Task.supplyAsync(() -> - StringUtils.convertToHtml( - repo.getAddonChangelog(provider, version.projectId(), version.versionId()), - "238222".equals(version.projectId()) // https://github.com/HMCL-dev/HMCL/pull/4828#issuecomment-3791068569 FUCK YOU JEI, FUCK YOU CURSEFORGE - ) - ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - String changelog = StringUtils.isNotBlank(result) ? result : i18n("addon.changelog.empty"); - scrollPane.setContent(FXUtils.renderAddonChangelog(changelog, repo.getBaseUrl())); - FXUtils.smoothScrolling(scrollPane); - spinnerPane.setFailedReason(null); - } else { - spinnerPane.setFailedReason(i18n("download.failed.refresh")); - } - spinnerPane.setLoading(false); - }).start(); - } - } - - @FunctionalInterface - public interface DownloadCallback { - void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon addon, RemoteAddon.Version file); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index 673c2fc04f..21a7b0ccb4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -41,14 +41,6 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator private final TabController tabController; /// The settings tab bound to the current game instance. private final TabItem> gameSettingsTab; - /// The component management tab bound to the current game instance. - private final TabItem installerListTab; - /// The installed mods tab bound to the current game instance. - private final TabItem modListTab; - /// The saved worlds tab bound to the current game instance. - private final TabItem worldListTab; - /// The installed resource packs tab bound to the current game instance. - private final TabItem resourcePackTab; /// The screenshots tab bound to the current game instance. private final TabItem screenshotsTab; private final TransitionPane transitionPane = new TransitionPane(); @@ -79,18 +71,10 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator public GameInstancePage() { // Child tabs subscribe to instanceProperty() themselves and reload on change. this.gameSettingsTab = new TabItem<>(() -> new GameSettingsPage<>(GameSettings.Instance.class, instance)); - this.installerListTab = new TabItem<>(() -> new InstallerListPage(instance)); - this.modListTab = new TabItem<>(() -> new ModListPage(instance)); - this.resourcePackTab = new TabItem<>(() -> new ResourcePackListPage(instance)); - this.worldListTab = new TabItem<>(() -> new WorldListPage(instance)); this.screenshotsTab = new TabItem<>(() -> new ScreenshotsPage(instance)); tabController = new TabController(transitionPane, gameSettingsTab, - installerListTab, - modListTab, - resourcePackTab, - worldListTab, screenshotsTab); tabController.selectTab(gameSettingsTab); @@ -118,11 +102,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator })); getSidebar() - .addNavigationDrawerTab(tabController, gameSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL) - .addNavigationDrawerTab(tabController, installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL) - .addNavigationDrawerTab(tabController, modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) - .addNavigationDrawerTab(tabController, resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) - .addNavigationDrawerTab(tabController, worldListTab, i18n("world.manage"), SVG.PUBLIC); + .addNavigationDrawerTab(tabController, gameSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL); PopupMenu browseList = new PopupMenu(); JFXPopup browsePopup = new JFXPopup(browseList); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java deleted file mode 100644 index fb553768d2..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.game.LocalizedRemoteAddonRepository; -import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; -import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; -import org.jackhuang.hmcl.util.i18n.I18n; - -import java.util.MissingResourceException; - -import static org.jackhuang.hmcl.setting.SettingsManager.settings; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class HMCLLocalizedDownloadListPage extends DownloadListPage { - - public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { - return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, true, true); - } - - public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { - var page = new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.SHADER_PACK, true, true); - page.supportChinese.set(false); - return page; - } - - private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean instanceSelection, RemoteAddon.Type type, boolean curseForge, boolean modrinth) { - super(type, new Repository(), callback, instanceSelection); - ((Repository) repository).downloadSource.bind(downloadSource); - - supportChinese.set(true); - - boolean supportedCurseForge = curseForge && CurseForgeRemoteAddonRepository.isAvailable(); - - downloadSources.setAll("addon.modrinth"); - if (supportedCurseForge) { - downloadSources.add("addon.curseforge"); - } - - if ("curseforge".equalsIgnoreCase(settings().defaultAddonSourceProperty().get())) { - if (supportedCurseForge) { - downloadSource.set("addon.curseforge"); - } else if (modrinth) { - downloadSource.set("addon.modrinth"); - } else { - throw new AssertionError("Should not be here."); - } - } else { - if (modrinth) { - downloadSource.set("addon.modrinth"); - } else if (supportedCurseForge) { - downloadSource.set("addon.curseforge"); - } else { - throw new AssertionError("Should not be here."); - } - } - } - - private static class Repository extends LocalizedRemoteAddonRepository { - - public Repository() { - } - - public final StringProperty downloadSource = new SimpleStringProperty(); - - @Override - protected RemoteAddonRepository getBackedRemoteModRepository() { - if ("addon.modrinth".equals(downloadSource.get())) { - return ModrinthRemoteAddonRepository.getInstance(); - } else { - return CurseForgeRemoteAddonRepository.getInstance(); - } - } - - } - - @Override - protected String getLocalizedCategory(String category, Object self) { - if (category.isEmpty()) { - return ""; - } - - String key = ("addon.modrinth".equals(downloadSource.get()) ? "modrinth" : "curse") + ".category." + category; - try { - return I18n.getResourceBundle().getString(key); - } catch (MissingResourceException e) { - LOG.warning("Cannot find key " + key + " in resource bundle"); - if (self instanceof CurseForgeRemoteAddonRepository.Category curseCategory) { - return curseCategory.getName(); - } - return category; - } - } - - @Override - protected String getLocalizedOfficialPage() { - if ("addon.modrinth".equals(downloadSource.get())) { - return i18n("addon.modrinth"); - } else { - return i18n("addon.curseforge"); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java deleted file mode 100644 index e780a63b8b..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.beans.value.ObservableValue; -import javafx.scene.Node; -import javafx.scene.control.Skin; -import org.jackhuang.hmcl.game.GameComponentAnalyzer; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.ui.*; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -public class InstallerListPage extends ListPageBase { - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private @Nullable HMCLGameInstance gameInstance; - - /// Creates an installer list that reloads when `instanceContext` changes. - /// - /// @param instanceContext the parent page's instance property - public InstallerListPage(ObservableValue instanceContext) { - Objects.requireNonNull(instanceContext, "instanceContext"); - - listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> { - if (current != null) { - loadInstance(current); - } - })); - } - - @Override - protected Skin createDefaultSkin() { - return new InstallerListPageSkin(); - } - - /// Rebuilds the component controls for the selected instance, or clears them if none is selected. - /// Minecraft editing follows the stored manifest's modifiability; other installed components - /// require a recognized patch to expose editing controls. - public void loadInstance(HMCLGameInstance.Optional instance) { - @Nullable HMCLGameInstance gameInstance = instance.instance(); - this.gameInstance = gameInstance; - if (gameInstance == null) { - itemsProperty().clear(); - return; - } - - HMCLGameRepository repository = gameInstance.getRepository(); - - itemsProperty().clear(); - InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameInstance.getVersion(), InstallerItem.Style.LIST_ITEM); - - // Conventional libraries: game, fabric, legacyfabric, forge, cleanroom, neoforge, liteloader, optifine - for (InstallerItem component : group.getComponents()) { - - // Skip fabric-api and quilt-api and legacyfabric-api - if (component.getComponentType().getPatchId().endsWith("-api")) { - continue; - } - - @Nullable String libraryVersion = gameInstance.getComponentVersion(component.getComponentType()); - - if (libraryVersion != null) { - component.versionProperty().set(new InstallerItem.InstalledState( - libraryVersion, - component.getComponentType() == GameComponentType.GAME - ? !gameInstance.getManifest().isModifiable() - : !gameInstance.getAnalyzer().isClear(component.getComponentType()), - false - )); - } else { - component.versionProperty().set(null); - } - - component.setOnRemove(() -> repository.getDependency().updateInstanceAsync( - gameInstance.getId(), - publishedInstance -> repository.getDependency().removeComponentAsync( - publishedInstance, - component.getComponentType())) - .withRunAsync(Schedulers.javafx(), this::reloadCurrentInstance) - .start()); - - itemsProperty().add(component); - } - - var currentItems = getItems().stream().map(InstallerItem::getComponentType).toList(); - // other third-party libraries which are unable to manage. - for (GameComponentAnalyzer.Mark mark : gameInstance.getAnalyzer()) { - if (currentItems.contains(mark.componentType())) continue; - - // we have done this library above. - - InstallerItem installerItem = new InstallerItem(mark.componentType(), InstallerItem.Style.LIST_ITEM); - installerItem.versionProperty().set(new InstallerItem.InstalledState(mark.version(), false, false)); - installerItem.setOnRemove(() -> repository.getDependency().updateInstanceAsync( - gameInstance.getId(), - publishedInstance -> repository.getDependency().removeComponentAsync( - publishedInstance, - mark.componentType())) - .withRunAsync(Schedulers.javafx(), this::reloadCurrentInstance) - .start()); - - itemsProperty().add(installerItem); - } - } - - private void reloadCurrentInstance() { - if (gameInstance != null) { - loadInstance(HMCLGameInstance.Optional.of(gameInstance.getRepository(), gameInstance.getId())); - } - } - - private class InstallerListPageSkin extends ToolbarListPageSkin { - - InstallerListPageSkin() { - super(InstallerListPage.this); - } - - @Override - protected List initializeToolbar(InstallerListPage skinnable) { - return Collections.emptyList(); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java deleted file mode 100644 index 86bbc73fa7..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java +++ /dev/null @@ -1,900 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.*; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.WeakInvalidationListener; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.DoubleBinding; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ObservableValue; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.css.PseudoClass; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; -import javafx.stage.FileChooser; -import javafx.util.Duration; -import org.jackhuang.hmcl.addon.AddonLoader; -import org.jackhuang.hmcl.addon.AddonProjectInfoRegistry; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.mod.LocalModFile; -import org.jackhuang.hmcl.addon.mod.ModLoaderType; -import org.jackhuang.hmcl.addon.mod.ModManager; -import org.jackhuang.hmcl.addon.mod.ModProjectInfo; -import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; -import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; -import org.jackhuang.hmcl.addon.update.AddonCheckUpdatesTask; -import org.jackhuang.hmcl.game.GameInstanceID; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.setting.DownloadProviders; -import org.jackhuang.hmcl.setting.GameDirectory; -import org.jackhuang.hmcl.setting.GameInstanceIconType; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.animation.AnimationUtils; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.Lazy; -import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.io.CompressingUtils; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.NetworkUtils; -import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.ref.WeakReference; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; - -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; -import static org.jackhuang.hmcl.util.Lang.mapOf; -import static org.jackhuang.hmcl.util.Pair.pair; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class ModListPage extends ListPageBase implements PageAware { - private final ReentrantLock lock = new ReentrantLock(); - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - - private ModManager modManager; - private @Nullable HMCLGameInstance gameInstance; - - final EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); - - private final BooleanProperty isLoadingMods = new SimpleBooleanProperty(false); - private long modLoadRequestId; - - /// Creates a mod list that reloads when `instanceContext` changes. - /// - /// @param instanceContext the parent page's instance property - public ModListPage(ObservableValue instanceContext) { - Objects.requireNonNull(instanceContext, "instanceContext"); - FXUtils.applyDragListener(this, it -> ModManager.MOD_EXTENSIONS.contains(FileUtils.getExtension(it).toLowerCase(Locale.ROOT)), mods -> { - mods.forEach(it -> { - try { - modManager.addMod(it); - } catch (IOException | IllegalArgumentException e) { - LOG.warning("Unable to parse mod file " + it, e); - } - }); - loadMods(modManager); - }); - - listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> { - if (current != null) { - loadInstance(current); - } - })); - } - - @Override - protected Skin createDefaultSkin() { - return new ModListPageSkin(this); - } - - public void refresh() { - if (isLoadingMods.get()) { - return; - } - - loadMods(modManager); - } - - public void loadInstance(HMCLGameInstance.Optional instance) { - this.gameInstance = instance.instance(); - if (gameInstance == null) { - return; - } - - loadMods(gameInstance.getModManager()); - } - - private void loadMods(ModManager modManager) { - long requestId = ++modLoadRequestId; - - setLoading(true); - isLoadingMods.set(true); - - if (this.modManager != modManager) { - getItems().clear(); - } - this.modManager = modManager; - - CompletableFuture.supplyAsync(() -> { - lock.lock(); - try { - modManager.refresh(); - return modManager.getLocalFiles().stream().map(ModInfoObject::new).toList(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } finally { - lock.unlock(); - } - }, Schedulers.io()).whenCompleteAsync((list, exception) -> { - if (this.modManager != modManager || requestId != modLoadRequestId) { - return; - } - - try { - supportedLoaders.clear(); - supportedLoaders.addAll(modManager.getSupportedLoaders()); - - if (exception == null) { - getItems().setAll(list); - } else { - LOG.warning("Failed to load mods", exception); - getItems().clear(); - } - } finally { - isLoadingMods.set(false); - setLoading(false); - } - }, Schedulers.javafx()); - } - - public void add() { - FileChooser chooser = new FileChooser(); - chooser.setTitle(i18n("mods.add.title")); - chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod")); - List res = Controllers.showOpenMultipleDialog(chooser); - - if (res == null) return; - - // It's guaranteed that succeeded and failed are thread safe here. - List succeeded = new ArrayList<>(res.size()); - List failed = new ArrayList<>(); - - Task.runAsync(() -> { - for (Path file : res) { - try { - modManager.addMod(file); - succeeded.add(FileUtils.getName(file)); - } catch (Exception e) { - LOG.warning("Unable to add mod " + file, e); - failed.add(FileUtils.getName(file)); - - // Actually addMod will not throw exceptions because FileChooser has already filtered files. - } - } - }).withRunAsync(Schedulers.javafx(), () -> { - List prompt = new ArrayList<>(1); - if (!succeeded.isEmpty()) - prompt.add(i18n("mods.add.success", String.join(", ", succeeded))); - if (!failed.isEmpty()) - prompt.add(i18n("mods.add.failed", String.join(", ", failed))); - Controllers.dialog(String.join("\n", prompt), i18n("mods.add")); - loadMods(modManager); - }).start(); - } - - void removeSelected(ObservableList selectedItems) { - List failed = new ArrayList<>(); - selectedItems.stream() - .filter(Objects::nonNull) - .map(ModInfoObject::getModInfo) - .forEach(info -> { - try { - info.delete(); - } catch (IOException e) { - failed.add(info.getFile().toString()); - LOG.warning("Failed to delete mod \"" + info.getId() + "\"", e); - } - }); - loadMods(modManager); - if (!failed.isEmpty()) - Controllers.dialog(i18n("mods.delete.failed") + "\n" + String.join(",\n", failed), - i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - - void setSelectedEnabled(ObservableList selectedItems, boolean enabled) { - List failed = new ArrayList<>(); - selectedItems.stream() - .filter(Objects::nonNull) - .map(ModInfoObject::getModInfo) - .forEach(info -> { - try { - info.setActive(enabled); - } catch (IOException e) { - failed.add(info.getFile().toString()); - } - }); - if (!failed.isEmpty()) - Controllers.dialog(i18n("mods.toggle.failed") + "\n" + String.join(",\n", failed), - i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - - public void openModFolder() { - if (gameInstance != null) { - FXUtils.openFolder(gameInstance.getModsDirectory()); - } - } - - /// Checks the supplied mods for updates and opens the results for the current instance. - /// The target directory is retained even if this page switches instances during the check. - /// Does nothing while the list is loading or no instance is selected. - /// - /// @param mods the mods to check - public void checkUpdates(Collection mods) { - Objects.requireNonNull(mods); - @Nullable HMCLGameInstance gameInstance = this.gameInstance; - if (isLoading() || gameInstance == null) { - return; - } - - Path modDirectory = gameInstance.getModManager().getDirectory(); - Runnable action = () -> Controllers.taskDialog(Task - .composeAsync(() -> { - GameVersionNumber version = gameInstance.getVersion(); - if (version != GameVersionNumber.unknown()) { - return new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), mods); - } else { - LOG.warning("Failed to check for updates, due to unable to get instance game version"); - return null; - } - }) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception instanceof CancellationException) return; - if (exception != null || result == null) { - Controllers.dialog(i18n("addon.check_update.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); - if (exception != null) LOG.warning("Failed to check for updates", exception); - } else if (result.commonUpdates().isEmpty()) { - Controllers.dialog(i18n("addon.check_update.empty")); - } else { - Controllers.navigateForward(new AddonUpdatesPage(DownloadProviders.getDownloadProvider(), modDirectory, result)); - } - }) - .withStagesHints("update.checking"), - i18n("addon.check_update"), TaskCancellationAction.NORMAL); - - if (gameInstance.isModpack()) { - Controllers.confirm( - i18n("mods.update_modpack_mod.warning"), null, - MessageDialogPane.MessageType.WARNING, - action, null); - } else { - action.run(); - } - } - - public void rollback(LocalModFile from, LocalModFile to) { - try { - modManager.rollback(from, to); - refresh(); - } catch (IOException ex) { - Controllers.showToast(i18n("message.failed")); - } - } - - public GameDirectory getGameDirectory() { - return gameInstance != null ? gameInstance.getRepository().getGameDirectory() : null; - } - - public HMCLGameRepository getRepository() { - return gameInstance != null ? gameInstance.getRepository() : null; - } - - public GameInstanceID getInstanceId() { - return gameInstance != null ? gameInstance.getId() : null; - } - - @NotNullByDefault - private static final class ModListPageSkin extends SkinBase { - - private final TransitionPane toolbarPane; - private final HBox searchBar; - private final HBox toolbarNormal; - private final HBox toolbarSelecting; - - private final JFXListView listView; - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - private final JFXTextField searchField; - - /// Timer for debouncing search input to avoid executing search on every keystroke. - private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); - - public ModListPageSkin(ModListPage skinnable) { - super(skinnable); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - pane.getChildren().setAll(root); - root.getStyleClass().add("no-padding"); - listView = new JFXListView<>(); - listView.getStyleClass().add("no-horizontal-scrollbar"); - - { - toolbarPane = new TransitionPane(); - - searchBar = new HBox(); - toolbarNormal = new HBox(); - toolbarSelecting = new HBox(); - - // Search Bar - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField = new JFXTextField(); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - searchPause.setOnFinished(e -> search()); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - searchPause.setRate(1); - searchPause.playFromStart(); - } - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); - - searchField.clear(); - searchPause.stop(); - - isSearching.set(false); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - JFXButton refreshButton = createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh); - refreshButton.disableProperty().bind(skinnable.isLoadingMods); - - // Toolbar Normal - toolbarNormal.getChildren().setAll( - refreshButton, - createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getItems().stream() - .map(ModListPage.ModInfoObject::getModInfo) - .toList() - ) - ), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - // Toolbar Selecting - - // reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker - var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())); - - ListChangeListener listener = change -> { - selectAll.setDisable(!listView.getItems().isEmpty() - && listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size()); - }; - - listView.getSelectionModel().getSelectedItems().addListener(listener); - listView.getItems().addListener(listener); - - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }), - createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), - createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - skinnable.checkUpdates( - listView.getSelectionModel().getSelectedItems().stream() - .map(ModListPage.ModInfoObject::getModInfo) - .toList() - ) - ), - selectAll, - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> { - if (selectedItem == null) - changeToolbar(isSearching.get() ? searchBar : toolbarNormal); - else - changeToolbar(toolbarSelecting); - }); - - FXUtils.setOverflowHidden(toolbarPane, 8); - - root.getContent().add(toolbarPane); - - // Clear selection when pressing ESC - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(skinnable.loadingProperty()); - - listView.setCellFactory(x -> new ModListPage.ModInfoListCell(listView, skinnable)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("mods.empty")); - placeholderLabel.textProperty().bind( - Bindings.createStringBinding(() -> { - if (isSearching.get()) { - return i18n("search.no_results_found"); - } else { - return i18n("mods.empty"); - } - }, - isSearching) - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - Bindings.bindContent(listView.getItems(), skinnable.getItems()); - skinnable.getItems().addListener((ListChangeListener) c -> { - if (isSearching.get()) { - search(); - } - }); - - listView.setOnContextMenuRequested(event -> { - ModListPage.ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); - if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { - listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ModListPage.ModInfoDialog(selectedItem)); - } - }); - - // ListViewBehavior would consume ESC pressed event, preventing us from handling it - // So we ignore it here - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } - - getChildren().setAll(pane); - } - - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); - } - } - } - - private void search() { - isSearching.set(true); - - Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); - - String queryString = searchField.getText(); - if (StringUtils.isBlank(queryString)) { - listView.getItems().setAll(getSkinnable().getItems()); - } else { - listView.getItems().clear(); - - Predicate<@Nullable String> predicate; - try { - predicate = StringUtils.compileQuery(queryString); - } catch (Throwable e) { - LOG.warning("Illegal regular expression", e); - return; - } - - // Do we need to search in the background thread? - for (ModListPage.ModInfoObject item : getSkinnable().getItems()) { - LocalModFile modInfo = item.getModInfo(); - if (predicate.test(modInfo.getFileName()) - || predicate.test(modInfo.getName()) - || predicate.test(modInfo.getVersion()) - || predicate.test(modInfo.getGameVersion()) - || predicate.test(modInfo.getId()) - || predicate.test(Objects.toString(modInfo.getModLoaderType())) - || predicate.test((item.getProjectInfo() != null ? item.getProjectInfo().displayName() : null))) { - listView.getItems().add(item); - } - } - } - } - - } - - public static final class ModInfoObject { - private final LocalModFile localModFile; - /// Matching project metadata, or null if the local mod cannot be identified. - private final @Nullable ModProjectInfo projectInfo; - - private final ItemPropertyAsyncCache iconCache; - - ModInfoObject(LocalModFile localModFile) { - this.localModFile = localModFile; - this.projectInfo = AddonProjectInfoRegistry.MOD.getInfo(localModFile.getId(), localModFile.getName()); - - this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); - } - - public LocalModFile getModInfo() { - return localModFile; - } - - /// Returns matching project metadata, or null if no project was found. - public @Nullable ModProjectInfo getProjectInfo() { - return projectInfo; - } - - private Image getDefaultIcon() { - return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon(); - } - - private Image loadIcon() { - List iconPaths = new ArrayList<>(); - - if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) { - iconPaths.add(this.localModFile.getLogoPath()); - } - - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) { - for (String path : iconPaths) { - Path iconPath = fs.getPath(path); - if (Files.exists(iconPath)) { - Image image = FXUtils.loadImage(iconPath, 80, 80, true, true); - if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 && - Math.abs(image.getWidth() - image.getHeight()) < 1) { - return image; - } - } - } - } catch (Exception e) { - LOG.warning("Failed to load mod icons", e); - } - - return getDefaultIcon(); - } - } - - private static final class ModInfoDialog extends JFXDialogLayout { - - ModInfoDialog(ModInfoObject modInfo) { - HBox titleContainer = new HBox(); - titleContainer.setSpacing(8); - titleContainer.setPadding(new Insets(0, 0, 12, 0)); - - DoubleBinding widthBinding = Controllers.getDecorator().contentWidthProperty().multiply(0.7); - prefWidthProperty().bind(widthBinding); - maxWidthProperty().bind(widthBinding); - - var imageContainer = new ImageContainer(40); - titleContainer.setAlignment(Pos.CENTER_LEFT); - modInfo.iconCache.attachValue(imageContainer.imageProperty(), null); - - TwoLineListItem title = new TwoLineListItem(); - title.getTitleLabel().setWrapText(true); - String displayName = modInfo.getModInfo().getName(); - if (modInfo.getProjectInfo() != null && I18n.isUseChinese()) { - String chineseName = modInfo.getProjectInfo().mainName(); - if (StringUtils.containsChinese(chineseName)) { - if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) - displayName = chineseName + " (" + displayName + ")"; - } - } - title.setTitle(displayName); - - StringJoiner subtitle = new StringJoiner("\n"); - subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile())); - if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) { - subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion()); - } - if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) { - subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion()); - } - if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) { - subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors()); - } - title.setSubtitle(subtitle.toString()); - - titleContainer.getChildren().setAll(imageContainer, title); - setHeading(titleContainer); - - Label description = new Label(modInfo.getModInfo().getDescription().toString()); - description.setWrapText(true); - FXUtils.copyOnDoubleClick(description); - - ScrollPane descriptionPane = new ScrollPane(description); - FXUtils.smoothScrolling(descriptionPane); - descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - descriptionPane.setFitToWidth(true); - description.heightProperty().addListener((obs, oldVal, newVal) -> { - double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5; - double targetHeight = Math.min(newVal.doubleValue(), maxHeight); - descriptionPane.setPrefViewportHeight(targetHeight); - }); - - setBody(descriptionPane); - - if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) { - for (Pair item : Arrays.asList( - pair("addon.curseforge", CurseForgeRemoteAddonRepository.getInstance()), - pair("addon.modrinth", ModrinthRemoteAddonRepository.getInstance()) - )) { - RemoteAddonRepository repository = item.getValue(); - JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); - Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo().getFile()); - if (versionOptional.isPresent()) { - RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId()); - FXUtils.runInFX(() -> { - Set tags = new LinkedHashSet<>(); - for (AddonLoader loader : versionOptional.get().loaders()) { - String tag = I18n.translateLoaderName(loader); - if (tag != null) tags.add(tag); - } - title.addTagsIfNotExist(tags); - - button.setExternalLink(remoteAddon.pageUrl()); - button.setDisable(false); - }); - } - }).start(); - button.setDisable(true); - getActions().add(button); - } - } - - if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) { - JFXHyperlink officialPageButton = new JFXHyperlink(i18n("mods.url")); - officialPageButton.setExternalLink(modInfo.getModInfo().getUrl()); - getActions().add(officialPageButton); - } - - @Nullable String mcmodUrl = modInfo.getProjectInfo() != null ? modInfo.getProjectInfo().mcmodUrl() : null; - if (mcmodUrl == null) { - JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search")); - searchButton.setExternalLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf( - pair("key", modInfo.getModInfo().getName()), - pair("site", "all"), - pair("filter", "0") - ))); - getActions().add(searchButton); - } else { - JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page")); - mcmodButton.setExternalLink(mcmodUrl); - getActions().add(mcmodButton); - } - - JFXButton okButton = new JFXButton(); - okButton.getStyleClass().add("dialog-accept"); - okButton.setText(i18n("button.ok")); - okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - getActions().add(okButton); - - onEscPressed(this, okButton::fire); - } - } - - private static final Lazy menu = new Lazy<>(PopupMenu::new); - private static final Lazy popup = new Lazy<>(() -> new JFXPopup(menu.get())); - - private static final class ModInfoListCell extends MDListCell { - private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); - - private final ModListPage page; - - private final ImageContainer imageContainer = new ImageContainer(32); - private final TwoLineListItem content = new TwoLineListItem(); - private final JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE); - private final JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER); - private final JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO); - private final JFXSwitch activeSwitch = new JFXSwitch(); - - private final InvalidationListener activeListener = obs -> activeSwitch.setSelected(getItem().getModInfo().isActive()); - private final WeakInvalidationListener weakActiveListener = new WeakInvalidationListener(activeListener); - private @Nullable ReadOnlyBooleanProperty observedActiveProperty; - - private Tooltip warningTooltip; - - /// Creates a selectable mod row with file actions for the supplied page. - ModInfoListCell(JFXListView listView, ModListPage page) { - super(listView); - this.page = page; - - this.getStyleClass().add("mod-info-list-cell"); - - HBox container = new HBox(8); - container.setPickOnBounds(false); - container.setAlignment(Pos.CENTER_LEFT); - container.setPadding(new Insets(0, 8, 0, 8)); - HBox.setHgrow(content, Priority.ALWAYS); - setSelectable(); - - imageContainer.setImage(GameInstanceIconType.COMMAND.getIcon()); - - FXUtils.installFastTooltip(restoreButton, i18n("mods.restore")); - - // free up space when not visible - restoreButton.managedProperty().bind(restoreButton.visibleProperty()); - - HBox actions = new HBox(); - actions.setAlignment(Pos.CENTER); - actions.getChildren().setAll(restoreButton, revealButton, infoButton, activeSwitch); - - activeSwitch.setSize(20); - activeSwitch.setMinSize(30, 30); - activeSwitch.setPrefSize(30, 30); - activeSwitch.setMaxSize(30, 30); - HBox.setMargin(activeSwitch, new Insets(0, 0, 0, 6)); - - container.getChildren().setAll(imageContainer, content, actions); - - StackPane.setMargin(container, new Insets(8)); - getContainer().getChildren().setAll(container); - } - - /// Updates mod details, warnings, and tags while retaining reusable tag labels. - @Override - protected void updateControl(@Nullable ModInfoObject dataItem, boolean empty) { - if (observedActiveProperty != null) - observedActiveProperty.removeListener(weakActiveListener); - - pseudoClassStateChanged(WARNING, false); - if (warningTooltip != null) { - Tooltip.uninstall(this, warningTooltip); - warningTooltip = null; - } - - if (empty || dataItem == null) return; - - List warning = new ArrayList<>(); - - var tags = new ArrayList(2); - - LocalModFile modInfo = dataItem.getModInfo(); - @Nullable ModProjectInfo projectInfo = dataItem.getProjectInfo(); - - ModLoaderType modLoaderType = modInfo.getModLoaderType(); - - dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty())); - - String displayName = modInfo.getName(); - if (projectInfo != null && I18n.isUseChinese()) { - String chineseName = projectInfo.mainName(); - if (StringUtils.containsChinese(chineseName)) { - if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) - displayName = chineseName + " (" + displayName + ")"; - } - } - content.setTitle(displayName); - - StringJoiner joiner = new StringJoiner(" | "); - if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId())) - joiner.add(modInfo.getId()); - - joiner.add(FileUtils.getName(modInfo.getFile())); - - content.setSubtitle(joiner.toString()); - - if (modLoaderType == ModLoaderType.UNKNOWN) { - tags.add(Tag.warning(i18n("mods.unknown"))); - } else if (!page.supportedLoaders.contains(modLoaderType)) { - warning.add(i18n("mods.warning.loader_mismatch")); - tags.add(Tag.warning(I18n.translateLoaderType(dataItem.getModInfo().getModLoaderType()))); - } - - String modVersion = modInfo.getVersion(); - if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) { - tags.add(new Tag(modVersion)); - } - - content.getTags().setAll(tags); - - (observedActiveProperty = modInfo.activeProperty()).addListener(weakActiveListener); - activeSwitch.setDisableAnimation(true); - activeSwitch.setSelected(modInfo.isActive()); - activeSwitch.setOnAction(event -> { - try { - modInfo.setActive(activeSwitch.isSelected()); - } catch (IOException e) { - LOG.warning("Unable to change state of mod file " + modInfo.getFile(), e); - Controllers.showToast(i18n("mods.toggle.failed")); - } finally { - activeSwitch.setSelected(modInfo.isActive()); - } - }); - activeSwitch.setDisableAnimation(!AnimationUtils.isAnimationEnabled()); - restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty()); - restoreButton.setOnAction(e -> { - menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream() - .map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(), - () -> page.rollback(modInfo, localModFile), - popup.get())) - .toList() - ); - - popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight()); - }); - revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile())); - infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem))); - - if (!warning.isEmpty()) { - pseudoClassStateChanged(WARNING, true); - - //noinspection ConstantValue - this.warningTooltip = warning.size() == 1 - ? new Tooltip(warning.get(0)) - : new Tooltip(String.join("\n", warning)); - FXUtils.installFastTooltip(this, warningTooltip); - } - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java deleted file mode 100644 index ea9289de18..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java +++ /dev/null @@ -1,689 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.*; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ObservableValue; -import javafx.css.PseudoClass; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; -import javafx.stage.FileChooser; -import javafx.util.Duration; -import org.jackhuang.hmcl.addon.LocalAddonFile; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; -import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager; -import org.jackhuang.hmcl.addon.update.AddonCheckUpdatesTask; -import org.jackhuang.hmcl.game.HMCLGameInstance; -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.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.ListPageBase; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.*; -import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; -import java.util.stream.Stream; - -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; -import static org.jackhuang.hmcl.util.Pair.pair; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class ResourcePackListPage extends ListPageBase { - - private static final String TIP_KEY = "resourcePackWarning"; - private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) { - return switch (compatibility) { - case TOO_NEW -> i18n("resourcepack.warning.too_new"); - case TOO_OLD -> i18n("resourcepack.warning.too_old"); - case INVALID -> i18n("resourcepack.warning.invalid"); - case MISSING_PACK_META -> i18n("resourcepack.warning.missing_pack_meta"); - case MISSING_GAME_META -> i18n("resourcepack.warning.missing_game_meta"); - default -> null; - }; - } - - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private @Nullable HMCLGameInstance gameInstance; - - private Path resourcePackDirectory; - private ResourcePackManager resourcePackManager; - - private final ReentrantLock lock = new ReentrantLock(); - - /// Creates a resource-pack list that reloads when `instanceContext` changes. - /// - /// @param instanceContext the parent page's instance property - public ResourcePackListPage(ObservableValue instanceContext) { - Objects.requireNonNull(instanceContext, "instanceContext"); - FXUtils.applyDragListener(this, ResourcePackFile::isFileResourcePack, this::addFiles); - - listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> { - if (current != null) { - loadInstance(current); - } - })); - } - - @Override - protected Skin createDefaultSkin() { - return new ResourcePackListPageSkin(this); - } - - public void loadInstance(HMCLGameInstance.Optional instance) { - this.gameInstance = instance.instance(); - if (gameInstance == null) { - this.resourcePackManager = null; - this.resourcePackDirectory = null; - getItems().clear(); - return; - } - - this.resourcePackManager = gameInstance.getResourcePackManager(); - this.resourcePackDirectory = this.resourcePackManager.getDirectory(); - - refresh(); - } - - public void refresh() { - if (resourcePackManager == null) return; - setDisable(false); - setLoading(true); - Task.supplyAsync(Schedulers.io(), () -> { - lock.lock(); - try { - if (!ResourcePackManager.isMcVersionSupported(resourcePackManager.getMinecraftVersion())) return null; - resourcePackManager.refresh(); - return resourcePackManager.arePacksEnabled(resourcePackManager.getLocalFiles().stream()) - .map(ResourcePackInfoObject::new) - .toList(); - } finally { - lock.unlock(); - } - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - if (result != null) { - getItems().setAll(result); - } else { // Unsupported mc version - getItems().clear(); - setDisable(true); - } - } else { - LOG.warning("Failed to load resource packs", exception); - getItems().clear(); - } - setLoading(false); - }).start(); - } - - public void addFiles(List files) { - if (resourcePackManager == null) return; - - List failures = new ArrayList<>(); - Task.runAsync(() -> { - for (Path file : files) { - try { - resourcePackManager.importResourcePack(file); - } catch (Exception e) { - LOG.warning("Failed to add resource pack", e); - failures.add(file); - } - } - }).withRunAsync(Schedulers.javafx(), () -> { - if (!failures.isEmpty()) { - StringBuilder failure = new StringBuilder(i18n("resourcepack.add.failed")); - for (Path file: failures) { - failure.append("\n").append(file.toString()); - } - Controllers.dialog(failure.toString(), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - refresh(); - }).start(); - - } - - public void onAddFiles() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(i18n("resourcepack.add")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); - List files = Controllers.showOpenMultipleDialog(fileChooser); - if (files != null && !files.isEmpty()) { - addFiles(files); - } - } - - private void onOpenFolder() { - if (resourcePackDirectory != null) { - FXUtils.openFolder(resourcePackDirectory); - } - } - - private void setSelectedEnabled(List selectedItems, boolean enabled) { - if (!Boolean.TRUE.equals(SettingsManager.state().getShownTips().get(TIP_KEY)) && enabled && !selectedItems.stream().map(ResourcePackInfoObject::getFile).allMatch(ResourcePackFile::isCompatible)) { - Controllers.confirm( - i18n("resourcepack.warning.manipulate"), - i18n("message.warning"), - MessageDialogPane.MessageType.WARNING, - () -> { - SettingsManager.state().getShownTips().put(TIP_KEY, true); - setSelectedEnabled(selectedItems, true); - }, null); - } else { - if (resourcePackManager == null) return; - if (enabled) { - resourcePackManager.enableResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList()); - } else { - resourcePackManager.disableResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList()); - } - for (ResourcePackInfoObject item : selectedItems) { - item.enabledProperty().set(enabled); - } - } - } - - private void removeSelected(List selectedItems) { - try { - if (resourcePackManager != null) { - if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { - refresh(); - } - } - } catch (IOException e) { - Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resource packs", e); - } - } - - /// Checks the supplied resource packs for updates and opens the results for the current instance. - /// The target directory is retained even if this page switches instances during the check. - /// Does nothing when no instance is selected. - /// - /// @param resourcePacks the resource packs to check - public void checkUpdates(Collection resourcePacks) { - @Nullable HMCLGameInstance gameInstance = this.gameInstance; - if (gameInstance == null) { - return; - } - - Path resourcePackDirectory = gameInstance.getResourcePackManager().getDirectory(); - Runnable action = () -> Controllers.taskDialog(Task - .composeAsync(() -> { - GameVersionNumber version = gameInstance.getVersion(); - if (version != GameVersionNumber.unknown()) { - return new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), resourcePacks); - } else { - LOG.warning("Failed to check for updates, due to unable to get instance game version"); - return null; - } - }) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null || result == null) { - Controllers.dialog(i18n("addon.check_update.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); - } else if (result.commonUpdates().isEmpty()) { - Controllers.dialog(i18n("addon.check_update.empty")); - } else { - Controllers.navigateForward(new AddonUpdatesPage(DownloadProviders.getDownloadProvider(), resourcePackDirectory, result)); - } - }) - .withStagesHints("update.checking"), - i18n("addon.check_update"), TaskCancellationAction.NORMAL); - - if (gameInstance.isModpack()) { - Controllers.confirm( - i18n("resourcepack.update_in_modpack.warning"), null, - MessageDialogPane.MessageType.WARNING, - action, null); - } else { - action.run(); - } - } - - @NotNullByDefault - private static final class ResourcePackListPageSkin extends SkinBase { - private final JFXListView listView; - private final JFXTextField searchField = new JFXTextField(); - - private final TransitionPane toolbarPane = new TransitionPane(); - private final HBox searchBar = new HBox(); - private final HBox toolbarNormal = new HBox(); - private final HBox toolbarSelecting = new HBox(); - - /// Whether the search mechanism is currently active. - private final BooleanProperty isSearching = new SimpleBooleanProperty(false); - - /// Timer for debouncing search input to avoid executing search on every keystroke. - private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); - - private ResourcePackListPageSkin(ResourcePackListPage control) { - super(control); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - - listView = new JFXListView<>(); - - { - - // Toolbar Selecting - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - control.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }), - createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), - createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - control.checkUpdates( - listView.getSelectionModel().getSelectedItems().stream().map(ResourcePackInfoObject::getFile).toList() - ) - ), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> - listView.getSelectionModel().selectAll()), - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - // Search Bar - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - searchPause.setOnFinished(e -> search()); - FXUtils.onChange(searchField.textProperty(), newValue -> { - if (isSearching.get() || !StringUtils.isBlank(newValue)) { - searchPause.setRate(1); - searchPause.playFromStart(); - } - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); - - searchField.clear(); - searchPause.stop(); - - isSearching.set(false); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - // Toolbar Normal - toolbarNormal.setAlignment(Pos.CENTER_LEFT); - toolbarNormal.setPickOnBounds(false); - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), - createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), - createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> - control.checkUpdates(listView.getItems().stream().map(ResourcePackInfoObject::getFile).toList()) - ), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> { - if (selectedItem == null) - changeToolbar(isSearching.get() ? searchBar : toolbarNormal); - else - changeToolbar(toolbarSelecting); - }); - FXUtils.setOverflowHidden(toolbarPane, 8); - root.getContent().add(toolbarPane); - - // Clear selection when pressing ESC - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.loadingProperty().bind(control.loadingProperty()); - - listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("resourcepack.empty")); - placeholderLabel.textProperty().bind( - Bindings.createStringBinding(() -> { - if (isSearching.get()) { - return i18n("search.no_results_found"); - } else { - return i18n("resourcepack.empty"); - } - }, - isSearching) - ); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - - Bindings.bindContent(listView.getItems(), control.getItems()); - - listView.setOnContextMenuRequested(event -> { - ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); - if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { - listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ResourcePackInfoDialog(selectedItem)); - } - }); - - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - listView.getStyleClass().add("no-horizontal-scrollbar"); - - center.setContent(listView); - root.getContent().add(center); - } - - pane.getChildren().setAll(root); - getChildren().setAll(pane); - } - - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); - } - } - } - - private void search() { - isSearching.set(true); - - Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); - - String queryString = searchField.getText(); - if (StringUtils.isBlank(queryString)) { - listView.getItems().setAll(getSkinnable().getItems()); - } else { - listView.getItems().clear(); - - Predicate<@Nullable String> predicate; - try { - predicate = StringUtils.compileQuery(queryString); - } catch (Throwable e) { - LOG.warning("Illegal regular expression", e); - return; - } - - // Do we need to search in the background thread? - for (ResourcePackInfoObject item : getSkinnable().getItems()) { - ResourcePackFile resourcePack = item.getFile(); - LocalAddonFile.Description description = resourcePack.getDescription(); - Stream descriptionParts = description == null - ? Stream.empty() - : description.parts().stream().map(LocalAddonFile.Description.Part::text); - if (predicate.test(resourcePack.getFileNameWithExtension()) - || predicate.test(resourcePack.getFileName()) - || descriptionParts.anyMatch(predicate)) { - listView.getItems().add(item); - } - } - } - } - } - - public static final class ResourcePackInfoObject { - - private final ResourcePackFile file; - private final BooleanProperty enabled; - - private final ItemPropertyAsyncCache iconCache; - - public ResourcePackInfoObject(Pair pair) { - this.file = pair.key(); - this.enabled = new SimpleBooleanProperty(this, "enabled", pair.value()); - - this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); - } - - public ResourcePackFile getFile() { - return file; - } - - public BooleanProperty enabledProperty() { - return enabled; - } - - private Image getDefaultIcon() { - return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); - } - - private Image loadIcon() { - Image icon = file.loadIcon(); - if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) { - return icon; - } - return getDefaultIcon(); - } - } - - private static final class ResourcePackListCell extends MDListCell { - private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); - - private final ResourcePackListPage page; - - private final ImageContainer imageContainer = new ImageContainer(32); - private final TwoLineListItem content = new TwoLineListItem(); - private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER); - private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO); - private final JFXSwitch activeSwitch; - - private BooleanProperty booleanProperty = null; - - /// Creates a selectable pack row with compatibility confirmation and file actions. - public ResourcePackListCell(JFXListView listView, ResourcePackListPage page) { - super(listView); - this.page = page; - - getStyleClass().add("resource-pack-list-cell"); - - HBox root = new HBox(8); - root.setPickOnBounds(false); - root.setAlignment(Pos.CENTER_LEFT); - root.setPadding(new Insets(0, 8, 0, 8)); - - activeSwitch = new JFXSwitch() { - @Override - public void fire() { - if (!Boolean.TRUE.equals(SettingsManager.state().getShownTips().get(TIP_KEY)) && !isSelected() && getItem() != null && !getItem().getFile().isCompatible()) { - Controllers.confirm( - i18n("resourcepack.warning.manipulate"), - i18n("message.info"), - MessageDialogPane.MessageType.INFO, - () -> { - super.fire(); - SettingsManager.state().getShownTips().put(TIP_KEY, true); - }, null); - } else { - super.fire(); - } - } - }; - activeSwitch.setOnAction(e -> { - if (getItem() != null) getItem().getFile().setEnabled(activeSwitch.isSelected()); - }); - activeSwitch.setSize(20); - activeSwitch.setMinSize(30, 30); - activeSwitch.setPrefSize(30, 30); - activeSwitch.setMaxSize(30, 30); - HBox.setMargin(activeSwitch, new Insets(0, 0, 0, 6)); - - HBox.setHgrow(content, Priority.ALWAYS); - - HBox actions = new HBox(); - actions.setAlignment(Pos.CENTER); - actions.getChildren().setAll(btnReveal, btnInfo, activeSwitch); - - root.getChildren().setAll(imageContainer, content, actions); - - setSelectable(); - - StackPane.setMargin(root, new Insets(8)); - getContainer().getChildren().add(root); - } - - /// Updates pack details and reuses the compatibility tag when a warning remains. - @Override - protected void updateControl(@Nullable ResourcePackInfoObject item, boolean empty) { - pseudoClassStateChanged(WARNING, false); - - if (empty || item == null) return; - - ResourcePackFile file = item.getFile(); - item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(itemProperty())); - - content.setTitle(file.getFileName()); - { - var description = file.getDescription(); - content.setSubtitle(description != null ? description.toStringSingleLine() : ""); - } - - FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); - btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getFile())); - - btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(item))); - - if (booleanProperty != null) { - activeSwitch.selectedProperty().unbindBidirectional(booleanProperty); - } - activeSwitch.selectedProperty().bindBidirectional(booleanProperty = item.enabledProperty()); - - { - var compatibility = file.getCompatibility(); - if (compatibility != ResourcePackFile.Compatibility.COMPATIBLE) { - pseudoClassStateChanged(WARNING, true); - content.getTags().setAll(Tag.warning(getWarning(compatibility))); - } else { - content.getTags().clear(); - } - } - } - } - - private static final class ResourcePackInfoDialog extends JFXDialogLayout { - - ResourcePackInfoDialog(ResourcePackInfoObject packInfoObject) { - ResourcePackFile pack = packInfoObject.getFile(); - - HBox titleContainer = new HBox(); - titleContainer.setSpacing(8); - - maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7)); - - ImageContainer imageContainer = new ImageContainer(40); - packInfoObject.iconCache.attachValue(imageContainer.imageProperty(), null); - - TwoLineListItem title = new TwoLineListItem(); - title.setTitle(pack.getFileName()); - title.setSubtitle(pack.getFileNameWithExtension()); - var compatibility = pack.getCompatibility(); - if (compatibility != ResourcePackFile.Compatibility.COMPATIBLE) { - title.addTagWarning(getWarning(compatibility)); - } - - titleContainer.getChildren().setAll(imageContainer, title); - setHeading(titleContainer); - - Label description = new Label(Objects.requireNonNullElse(pack.getDescription(), "").toString()); - description.setWrapText(true); - FXUtils.copyOnDoubleClick(description); - - ScrollPane descriptionPane = new ScrollPane(description); - FXUtils.smoothScrolling(descriptionPane); - descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - descriptionPane.setFitToWidth(true); - FXUtils.onChange(description.heightProperty(), newVal -> { - double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5; - double targetHeight = Math.min(newVal.doubleValue(), maxHeight); - descriptionPane.setPrefViewportHeight(targetHeight); - }); - - setBody(descriptionPane); - - for (Pair item : Arrays.asList( - pair("addon.curseforge", CurseForgeRemoteAddonRepository.getInstance()), - pair("addon.modrinth", ModrinthRemoteAddonRepository.getInstance()) - )) { - RemoteAddonRepository repository = item.getValue(); - JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); - Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile()); - if (versionOptional.isPresent()) { - RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId()); - FXUtils.runInFX(() -> { - button.setExternalLink(remoteAddon.pageUrl()); - button.setDisable(false); - }); - } - }).start(); - button.setDisable(true); - getActions().add(button); - } - - JFXButton okButton = new JFXButton(); - okButton.getStyleClass().add("dialog-accept"); - okButton.setText(i18n("button.ok")); - okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - getActions().add(okButton); - - onEscPressed(this, okButton::fire); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java deleted file mode 100644 index 38dbc79650..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.task.Task; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.channels.FileChannel; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.time.LocalDateTime; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** - * @author Glavo - */ -public final class WorldBackupTask extends Task { - - private final World world; - private final Path backupsDir; - private final boolean needLock; - - public WorldBackupTask(World world, Path backupsDir, boolean needLock) { - this.world = world; - this.backupsDir = backupsDir; - this.needLock = needLock; - } - - @Override - public void execute() throws Exception { - try (FileChannel lockChannel = needLock ? world.lock() : null) { - Files.createDirectories(backupsDir); - String time = LocalDateTime.now().format(WorldBackupsPage.TIME_FORMATTER); - String baseName = time + "_" + world.getFileName(); - Path backupFile = null; - OutputStream outputStream = null; - - int count; - for (count = 0; count < 256; count++) { - try { - backupFile = backupsDir.resolve(baseName + (count == 0 ? "" : " " + count) + ".zip").toAbsolutePath(); - outputStream = Files.newOutputStream(backupFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); - break; - } catch (FileAlreadyExistsException ignored) { - } - } - - if (outputStream == null) - throw new IOException("Too many attempts"); - - try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(outputStream))) { - String rootName = world.getFileName(); - Path rootDir = this.world.getFile(); - Files.walkFileTree(this.world.getFile(), new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { - if (path.endsWith("session.lock")) { - return FileVisitResult.CONTINUE; - } - zipOutputStream.putNextEntry(new ZipEntry(rootName + "/" + rootDir.relativize(path).toString().replace('\\', '/'))); - Files.copy(path, zipOutputStream); - zipOutputStream.closeEntry(); - return FileVisitResult.CONTINUE; - } - }); - } - - setResult(backupFile); - } - } -} 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 deleted file mode 100644 index 18afa131c9..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import javafx.beans.property.BooleanProperty; -import javafx.collections.FXCollections; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.game.WorldLockedException; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.construct.ImageContainer; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -import org.jackhuang.hmcl.ui.construct.RipplerContainer; -import org.jackhuang.hmcl.ui.construct.TwoLineListItem; -import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jetbrains.annotations.NotNull; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; -import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -/** - * @author Glavo - */ -public final class WorldBackupsPage extends ListPageBase implements WorldManagePage.WorldRefreshable { - static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); - - private final World world; - private final Path backupsDir; - private final BooleanProperty readOnly; - private final Pattern backupFileNamePattern; - - public WorldBackupsPage(WorldManagePage worldManagePage) { - this.world = worldManagePage.getWorld(); - this.backupsDir = worldManagePage.getBackupsDir(); - this.readOnly = worldManagePage.readOnlyProperty(); - this.backupFileNamePattern = Pattern.compile("(?[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?[0-9]+))?\\.zip"); - - refresh(); - } - - public void refresh() { - setLoading(true); - Task.supplyAsync(() -> { - if (Files.isDirectory(backupsDir)) { - try (Stream paths = Files.list(backupsDir)) { - ArrayList result = new ArrayList<>(); - - paths.forEach(path -> { - if (Files.isRegularFile(path)) { - try { - Matcher matcher = backupFileNamePattern.matcher(path.getFileName().toString()); - if (matcher.matches()) { - LocalDateTime time = LocalDateTime.parse(matcher.group("datetime"), TIME_FORMATTER); - int count = 0; - - if (matcher.group("count") != null) { - count = Integer.parseInt(matcher.group("count")); - } - - result.add(new BackupInfo(path, new World(path), time, count)); - } - } catch (Throwable e) { - LOG.warning("Failed to load backup file " + path, e); - } - } - }); - - result.sort(Comparator.naturalOrder()); - return result; - } - } else { - return new ArrayList(); - } - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - this.setLoading(false); - if (exception == null) { - this.setItems(FXCollections.observableList(result)); - } else { - LOG.warning("Failed to load backups", exception); - } - }).start(); - } - - @Override - protected Skin createDefaultSkin() { - return new WorldBackupsPageSkin(); - } - - void createBackup() { - Controllers.taskDialog(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); - } - - LocalDateTime time = LocalDateTime.parse(matcher.group("datetime"), TIME_FORMATTER); - int count = 0; - - if (matcher.group("count") != null) { - count = Integer.parseInt(matcher.group("count")); - } - - return Pair.pair(path, new BackupInfo(path, new World(path), time, count)); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - WorldBackupsPage.this.getItems().add(result.getValue()); - WorldBackupsPage.this.getItems().sort(Comparator.naturalOrder()); - Controllers.dialog(i18n("world.backup.create.success", result.getKey()), null, MessageDialogPane.MessageType.INFO); - } else if (exception instanceof WorldLockedException) { - Controllers.dialog(i18n("world.locked.failed"), null, MessageDialogPane.MessageType.WARNING); - } else { - 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); - } - - private final class WorldBackupsPageSkin extends ToolbarListPageSkin { - - WorldBackupsPageSkin() { - super(WorldBackupsPage.this); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("world.backup.empty")); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - } - - @Override - protected List initializeToolbar(WorldBackupsPage skinnable) { - JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup); - createBackup.disableProperty().bind(getSkinnable().readOnly); - - return Arrays.asList( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createBackup - ); - } - } - - public final class BackupInfo extends Control implements Comparable { - private final Path file; - private final World backupWorld; - private final LocalDateTime backupTime; - private final int count; - - public BackupInfo(Path file, World backupWorld, LocalDateTime backupTime, int count) { - this.file = file; - this.backupWorld = backupWorld; - this.backupTime = backupTime; - this.count = count; - } - - public World getBackupWorld() { - return backupWorld; - } - - public LocalDateTime getBackupTime() { - return backupTime; - } - - @Override - protected Skin createDefaultSkin() { - return new BackupInfoSkin(this); - } - - void onReveal() { - FXUtils.showFileInExplorer(file); - } - - void onDelete() { - WorldBackupsPage.this.getItems().remove(this); - Task.runAsync(() -> Files.delete(file)).start(); - } - - @Override - public int compareTo(@NotNull WorldBackupsPage.BackupInfo that) { - int c = this.backupTime.compareTo(that.backupTime); - return c != 0 ? c : Integer.compare(this.count, that.count); - } - } - - private static final class BackupInfoSkin extends SkinBase { - - BackupInfoSkin(BackupInfo skinnable) { - super(skinnable); - - World world = skinnable.getBackupWorld(); - - BorderPane root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); - root.setPadding(new Insets(8)); - - { - StackPane left = new StackPane(); - root.setLeft(left); - left.setPadding(new Insets(0, 8, 0, 0)); - - var imageView = new ImageContainer(32); - left.getChildren().add(imageView); - imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); - } - - { - TwoLineListItem item = new TwoLineListItem(); - root.setCenter(item); - - if (skinnable.getBackupWorld().getWorldName() != null) - item.setTitle(parseColorEscapes(skinnable.getBackupWorld().getWorldName())); - item.setSubtitle(formatDateTime(skinnable.getBackupTime()) + (skinnable.count == 0 ? "" : " (" + skinnable.count + ")")); - - if (world.getGameVersion() != null) - item.addTag(I18n.getDisplayVersion(world.getGameVersion())); - } - - { - HBox right = new HBox(8); - root.setRight(right); - right.setAlignment(Pos.CENTER_RIGHT); - - JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN); - right.getChildren().add(btnReveal); - FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); - btnReveal.setOnAction(event -> skinnable.onReveal()); - - JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER); - right.getChildren().add(btnDelete); - FXUtils.installFastTooltip(btnDelete, i18n("world.backup.delete")); - btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::onDelete, null)); - } - - getChildren().setAll(new RipplerContainer(root)); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java deleted file mode 100644 index e9e59bd370..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.scene.control.Skin; -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.wizard.WizardSinglePage; -import org.jackhuang.hmcl.util.i18n.I18n; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class WorldExportPage extends WizardSinglePage { - private final StringProperty path = new SimpleStringProperty(); - private final StringProperty gameVersion = new SimpleStringProperty(); - private final StringProperty worldName = new SimpleStringProperty(); - private final World world; - - public WorldExportPage(World world, Path export, Runnable onFinish) { - super(onFinish); - - this.world = world; - - path.set(export.toString()); - if (world.getGameVersion() != null) - gameVersion.set(I18n.getDisplayVersion(world.getGameVersion())); - worldName.set(world.getWorldName()); - } - - @Override - protected Skin createDefaultSkin() { - return new WorldExportPageSkin(this); - } - - public StringProperty pathProperty() { - return path; - } - - public StringProperty gameVersionProperty() { - return gameVersion; - } - - public StringProperty worldNameProperty() { - return worldName; - } - - public void export() { - onFinish.run(); - } - - @Override - public String getTitle() { - return i18n("world.export.wizard", world.getFileName()); - } - - @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/instances/WorldExportPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPageSkin.java deleted file mode 100644 index d7a472d55d..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPageSkin.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXTextField; -import javafx.beans.binding.Bindings; -import javafx.geometry.Pos; -import javafx.scene.control.SkinBase; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.stage.FileChooser; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.ComponentList; -import org.jackhuang.hmcl.ui.construct.LineFileChooserButton; -import org.jackhuang.hmcl.ui.construct.LinePane; -import org.jackhuang.hmcl.ui.construct.LineTextPane; - -import java.nio.file.Files; -import java.nio.file.Paths; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class WorldExportPageSkin extends SkinBase { - - public WorldExportPageSkin(WorldExportPage skinnable) { - super(skinnable); - - VBox container = new VBox(); - container.setSpacing(16); - container.setAlignment(Pos.CENTER); - FXUtils.setLimitWidth(container, 500); - - ComponentList list = new ComponentList(); - - var chooseFileButton = new LineFileChooserButton(); - chooseFileButton.setTitle(i18n("world.export.location")); - chooseFileButton.setType(LineFileChooserButton.Type.SAVE_FILE); - chooseFileButton.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip")); - chooseFileButton.locationProperty().bindBidirectional(skinnable.pathProperty()); - - var worldNamePane = new LinePane(); - worldNamePane.setTitle(i18n("world.name")); - JFXTextField txtWorldName = new JFXTextField(); - txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty()); - txtWorldName.setPrefWidth(300); - worldNamePane.setRight(txtWorldName); - - LineTextPane gameVersionPane = new LineTextPane(); - gameVersionPane.setTitle(i18n("world.game_version")); - gameVersionPane.textProperty().bind(skinnable.gameVersionProperty()); - - list.getContent().setAll(chooseFileButton, worldNamePane, gameVersionPane); - - container.getChildren().setAll( - ComponentList.createComponentListTitle(i18n("world.export")), - list - ); - - JFXButton btnExport = FXUtils.newRaisedButton(i18n("button.export")); - btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(chooseFileButton.getLocation())), - txtWorldName.textProperty().isEmpty(), chooseFileButton.locationProperty())); - btnExport.setOnAction(e -> skinnable.export()); - HBox bottom = new HBox(); - bottom.setAlignment(Pos.CENTER_RIGHT); - bottom.getChildren().setAll(btnExport); - container.getChildren().add(bottom); - - getChildren().setAll(container); - } - -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java deleted file mode 100644 index b738cd2afa..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java +++ /dev/null @@ -1,670 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXTextField; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.effect.BoxBlur; -import javafx.scene.image.Image; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.stage.FileChooser; -import org.glavo.nbt.tag.*; -import org.glavo.nbt.tag.Tag; -import org.jackhuang.hmcl.game.World; -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.util.Lang; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.text.DecimalFormat; -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.Locale; - -import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -/** - * @author Glavo - */ -public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.WorldRefreshable { - private final WorldManagePage worldManagePage; - private boolean isReadOnly; - private final World world; - private CompoundTag levelData; - private CompoundTag playerData; - - private final ImageContainer iconImageView = new ImageContainer(32); - - public WorldInfoPage(WorldManagePage worldManagePage) { - this.worldManagePage = worldManagePage; - this.world = worldManagePage.getWorld(); - refresh(); - } - - private void updateControls() { - CompoundTag dataTag = (CompoundTag) levelData.get("Data"); - CompoundTag playerTag = playerData; - - ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToHeight(true); - scrollPane.setFitToWidth(true); - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS); - setContent(scrollPane); - - VBox rootPane = new VBox(); - rootPane.setFillWidth(true); - scrollPane.setContent(rootPane); - FXUtils.smoothScrolling(scrollPane); - rootPane.getStyleClass().add("card-list"); - - ComponentList worldInfo = new ComponentList(); - { - var worldNamePane = new LinePane(); - { - worldNamePane.setTitle(i18n("world.name")); - JFXTextField worldNameField = new JFXTextField(); - setRightTextField(worldNamePane, worldNameField, 200); - - if (dataTag.get("LevelName") instanceof StringTag worldNameTag) { - var worldName = new SimpleStringProperty(worldNameTag.get()); - FXUtils.bindString(worldNameField, worldName); - worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName); - worldName.addListener((observable, oldValue, newValue) -> { - if (StringUtils.isNotBlank(newValue)) { - try { - world.setWorldName(newValue); - worldManagePage.setTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName()))); - } catch (Exception e) { - LOG.warning("Failed to set world name", e); - } - } - }); - } else { - worldNameField.setDisable(true); - } - } - - var gameVersionPane = new LineTextPane(); - { - gameVersionPane.setTitle(i18n("world.info.game_version")); - gameVersionPane.setText(world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString()); - } - - var iconPane = new LinePane(); - { - iconPane.setTitle(i18n("world.icon")); - - { - iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); - } - - JFXButton editIconButton = FXUtils.newToggleButton4(SVG.EDIT, 20); - JFXButton resetIconButton = FXUtils.newToggleButton4(SVG.RESTORE, 20); - { - editIconButton.setDisable(isReadOnly); - editIconButton.setOnAction(event -> Controllers.confirm( - I18n.i18n("world.icon.change.tip"), - I18n.i18n("world.icon.change"), - MessageDialogPane.MessageType.INFO, - this::changeWorldIcon, - null - )); - FXUtils.installFastTooltip(editIconButton, i18n("button.edit")); - - resetIconButton.setDisable(isReadOnly); - resetIconButton.setOnAction(event -> this.clearWorldIcon()); - FXUtils.installFastTooltip(resetIconButton, i18n("button.reset")); - } - - HBox hBox = new HBox(8); - hBox.setAlignment(Pos.CENTER_LEFT); - hBox.getChildren().addAll(iconImageView, editIconButton, resetIconButton); - - iconPane.setRight(hBox); - } - - var seedPane = new LinePane(); - { - seedPane.setTitle(i18n("world.info.random_seed")); - - SimpleBooleanProperty visibility = new SimpleBooleanProperty(); - JFXButton visibilityButton = FXUtils.newToggleButton4(SVG.VISIBILITY_OFF); - { - visibilityButton.setAlignment(Pos.CENTER); - visibilityButton.setOnAction((event) -> visibility.set(!visibility.get())); - } - - Label seedLabel = new Label(); - { - FXUtils.copyOnDoubleClick(seedLabel); - seedLabel.setAlignment(Pos.CENTER_RIGHT); - - seedLabel.setText(world.getSeed() != null ? world.getSeed().toString() : ""); - - BoxBlur blur = new BoxBlur(); - blur.setIterations(3); - FXUtils.onChangeAndOperate(visibility, isVisibility -> { - SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF; - visibilityButton.setGraphic(icon.createIcon(16)); - seedLabel.setEffect(isVisibility ? null : blur); - }); - } - - HBox right = new HBox(8); - { - right.setAlignment(Pos.CENTER_RIGHT); - right.getChildren().setAll(visibilityButton, seedLabel); - seedPane.setRight(right); - } - } - - var worldSpawnPoint = new LineTextPane(); - { - worldSpawnPoint.setTitle(i18n("world.info.spawn")); - - String value; - // Valid after 1.21.9-pre1 - if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) { - value = (spawnTag.get("dimension") instanceof StringTag dimensionTag) - ? Dimension.of(dimensionTag).formatPosition(posTag) - : Dimension.OVERWORLD.formatPosition(posTag); - } - // Valid before 1.21.9-pre1 - else if (dataTag.get("SpawnX") instanceof IntTag intX - && dataTag.get("SpawnY") instanceof IntTag intY - && dataTag.get("SpawnZ") instanceof IntTag intZ) { - value = Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); - } else { - value = null; - } - - worldSpawnPoint.setText(value); - } - - var lastPlayedPane = new LineTextPane(); - { - lastPlayedPane.setTitle(i18n("world.info.last_played")); - lastPlayedPane.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); - } - - var timePane = new LineTextPane(); - { - timePane.setTitle(i18n("world.info.time")); - if (dataTag.get("Time") instanceof LongTag timeTag) { - Duration duration = Duration.ofSeconds(timeTag.getValue() / 20); - timePane.setText(i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart())); - } - } - - var allowCheatsButton = new LineSwitch(); - { - allowCheatsButton.setTitle(i18n("world.info.allow_cheats")); - allowCheatsButton.setDisable(isReadOnly); - - bindTagAndSwitch(dataTag.get("allowCommands"), allowCheatsButton); - } - - var generateFeaturesButton = new LineSwitch(); - { - generateFeaturesButton.setTitle(i18n("world.info.generate_features")); - generateFeaturesButton.setDisable(isReadOnly); - // Valid before (1.16)20w20a - if (dataTag.get("MapFeatures") instanceof ByteTag generateFeaturesTag) { - bindTagAndSwitch(generateFeaturesTag, generateFeaturesButton); - } - // Valid after (1.16)20w20a - else if (world.getNormalizedWorldGenSettingsData() != null) { - CompoundTag unifiedWorldGenSettingsDataTag = world.getNormalizedWorldGenSettingsData(); - Tag tag = unifiedWorldGenSettingsDataTag.get("generate_features"); // Valid between (1.16)20w20a and 26.1-snapshot-6 - if (tag == null) - tag = unifiedWorldGenSettingsDataTag.get("generate_structures"); // Valid after 26.1-snapshot-6 - bindTagAndSwitch(tag, generateFeaturesButton); - } else { - generateFeaturesButton.setDisable(true); - } - } - - var difficultyButton = new LineSelectButton(); - { - difficultyButton.setTitle(i18n("world.info.difficulty")); - difficultyButton.setDisable(isReadOnly); - difficultyButton.setItems(Difficulty.items); - - Difficulty difficulty; - // Valid before 26.1-snapshot-6 - if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag - && (difficulty = Difficulty.of(difficultyTag.getValue())) != null) { - difficultyButton.setValue(difficulty); - difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - difficultyTag.setValue((byte) newValue.ordinal()); - saveWorldData(); - } - }); - } - // Valid after 26.1-snapshot-6 - else if (dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingTag - && difficultySettingTag.get("difficulty") instanceof StringTag difficultyTag - && (difficulty = Difficulty.of(difficultyTag.getValue())) != null) { - difficultyButton.setValue(difficulty); - difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - difficultyTag.setValue(newValue.getTagStringValue()); - saveWorldData(); - } - }); - } else { - difficultyButton.setDisable(true); - } - } - - var difficultyLockPane = new LineSwitch(); - { - difficultyLockPane.setTitle(i18n("world.info.difficulty_lock")); - difficultyLockPane.setDisable(isReadOnly); - // Valid before 26.1-snapshot-6 - if (dataTag.get("DifficultyLocked") instanceof ByteTag difficultyLockedTag) { - bindTagAndSwitch(difficultyLockedTag, difficultyLockPane); - } - // Valid after 26.1-snapshot-6 - else if (dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingTag - && difficultySettingTag.get("locked") instanceof ByteTag lockedTag) { - bindTagAndSwitch(lockedTag, difficultyLockPane); - } else { - difficultyLockPane.setDisable(true); - } - } - - worldInfo.getContent().setAll( - worldNamePane, gameVersionPane, iconPane, seedPane, worldSpawnPoint, lastPlayedPane, timePane, - allowCheatsButton, generateFeaturesButton, difficultyButton, difficultyLockPane); - - rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo); - } - - if (playerTag != null) { - ComponentList playerInfo = new ComponentList(); - - var locationPane = new LineTextPane(); - { - locationPane.setTitle(i18n("world.info.player.location")); - Dimension dimension = Dimension.of(playerTag.get("Dimension")); - if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) { - locationPane.setText(dimension.formatPosition(posTag)); - } - } - - var lastDeathLocationPane = new LineTextPane(); - { - lastDeathLocationPane.setTitle(i18n("world.info.player.last_death_location")); - // Valid after 22w14a; prior to this version, the game did not record the last death location data. - if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) { - Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension")); - if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) { - lastDeathLocationPane.setText(dimension.formatPosition(posTag)); - } - } - } - - var spawnPane = new LineTextPane(); - { - spawnPane.setTitle(i18n("world.info.player.spawn")); - if (playerTag.get("respawn") instanceof CompoundTag respawnTag - && respawnTag.get("dimension") instanceof StringTag dimensionTag - && respawnTag.get("pos") instanceof IntArrayTag intArrayTag - && intArrayTag.size() >= 3) { // Valid after 25w07a - spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag)); - } else if (playerTag.get("SpawnX") instanceof IntTag intX - && playerTag.get("SpawnY") instanceof IntTag intY - && playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a - // SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. - spawnPane.setText((playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD) - .formatPosition(intX.getValue(), intY.getValue(), intZ.getValue())); - } - } - - var playerGameTypePane = new LineSelectButton(); - { - playerGameTypePane.setTitle(i18n("world.info.player.game_type")); - playerGameTypePane.setDisable(worldManagePage.isReadOnly()); - playerGameTypePane.setItems(GameType.items); - - // Valid before 26.1-snapshot-6 - Tag rawHardcoreTag = dataTag.get("hardcore"); - // Valid after 26.1-snapshot-6 - if (rawHardcoreTag == null && dataTag.get("difficulty_settings") instanceof CompoundTag difficultySettingsTag) { - rawHardcoreTag = difficultySettingsTag.get("hardcore"); - } - - if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag && rawHardcoreTag instanceof ByteTag hardcoreTag) { - boolean isHardcore = hardcoreTag.getValue() == 1; - GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore); - if (gameType != null) { - playerGameTypePane.setValue(gameType); - playerGameTypePane.valueProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - if (newValue == GameType.HARDCORE) { - playerGameTypeTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag) - hardcoreTag.setValue((byte) 1); - } else { - playerGameTypeTag.setValue(newValue.ordinal()); - hardcoreTag.setValue((byte) 0); - } - saveWorldData(); - } - }); - } else { - playerGameTypePane.setDisable(true); - } - } else { - playerGameTypePane.setDisable(true); - } - } - - var healthPane = new LinePane(); - { - healthPane.setTitle(i18n("world.info.player.health")); - setRightTextField(healthPane, 50, playerTag.get("Health")); - } - - var foodLevelPane = new LinePane(); - { - foodLevelPane.setTitle(i18n("world.info.player.food_level")); - setRightTextField(foodLevelPane, 50, playerTag.get("foodLevel")); - } - - var foodSaturationPane = new LinePane(); - { - foodSaturationPane.setTitle(i18n("world.info.player.food_saturation_level")); - setRightTextField(foodSaturationPane, 50, playerTag.get("foodSaturationLevel")); - } - - var xpLevelPane = new LinePane(); - { - xpLevelPane.setTitle(i18n("world.info.player.xp_level")); - setRightTextField(xpLevelPane, 50, playerTag.get("XpLevel")); - } - - playerInfo.getContent().setAll( - locationPane, lastDeathLocationPane, spawnPane, playerGameTypePane, - healthPane, foodLevelPane, foodSaturationPane, xpLevelPane - ); - - rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo); - } - } - - private void setRightTextField(LinePane linePane, int perfWidth, Tag tag) { - JFXTextField textField = new JFXTextField(); - setRightTextField(linePane, textField, perfWidth); - if (tag instanceof IntTag intTag) { - bindTagAndTextField(intTag, textField); - } else if (tag instanceof FloatTag floatTag) { - bindTagAndTextField(floatTag, textField); - } else { - textField.setDisable(true); - } - } - - private void setRightTextField(LinePane linePane, JFXTextField textField, int perfWidth) { - textField.setDisable(isReadOnly); - textField.setPrefWidth(perfWidth); - linePane.setRight(textField); - } - - /// Saves boolean byte-tag edits through the switch, disabling it for missing or invalid tags. - private void bindTagAndSwitch(@Nullable Tag tag, LineSwitch switchControl) { - if (tag instanceof ByteTag byteTag) { - byte value = byteTag.getValue(); - if (value == 0 || value == 1) { - switchControl.setSelected(value == 1); - switchControl.selectedProperty().addListener((o, oldValue, newValue) -> { - try { - byteTag.setValue((byte) (newValue ? 1 : 0)); - saveWorldData(); - } catch (Exception e) { - switchControl.setSelected(oldValue); - LOG.warning("Exception happened when saving world info", e); - } - }); - } else { - switchControl.setDisable(true); - } - } else { - switchControl.setDisable(true); - } - } - - private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) { - jfxTextField.setText(intTag.getValue().toString()); - - jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - try { - Integer integer = Lang.toIntOrNull(newValue); - if (integer != null) { - intTag.setValue(integer); - saveWorldData(); - } - } catch (Exception e) { - jfxTextField.setText(oldValue); - LOG.warning("Exception happened when saving world data", e); - } - } - }); - FXUtils.setValidateWhileTextChanged(jfxTextField, true); - jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true)); - } - - private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { - jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue())); - - jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - try { - Float floatValue = Lang.toFloatOrNull(newValue); - if (floatValue != null) { - floatTag.setValue(floatValue); - saveWorldData(); - } - } catch (Exception e) { - jfxTextField.setText(oldValue); - LOG.warning("Exception happened when saving world data", e); - } - } - }); - FXUtils.setValidateWhileTextChanged(jfxTextField, true); - jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true)); - } - - private void saveWorldData() { - LOG.info("Saving data of world " + world.getWorldName()); - try { - this.world.writeWorldData(); - } catch (IOException e) { - LOG.warning("Failed to save world data", e); - } - } - - @Override - public void refresh() { - setFailedReason(null); - try { - this.isReadOnly = worldManagePage.isReadOnly(); - this.levelData = world.getLevelData(); - this.playerData = world.getPlayerData(); - updateControls(); - } catch (Exception e) { - LOG.warning("Failed to refresh world info", e); - setFailedReason(i18n("world.info.failed")); - } - } - - private record Dimension(String name) { - static final Dimension OVERWORLD = new Dimension(null); - static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether")); - static final Dimension THE_END = new Dimension(i18n("world.info.dimension.the_end")); - - static Dimension of(Tag tag) { - if (tag instanceof IntTag intTag) { - return switch (intTag.get()) { - case 0 -> OVERWORLD; - case -1 -> THE_NETHER; - case 1 -> THE_END; - default -> null; - }; - } else if (tag instanceof StringTag stringTag) { - String id = stringTag.getValue(); - return switch (id) { - case "overworld", "minecraft:overworld" -> OVERWORLD; - case "the_nether", "minecraft:the_nether" -> THE_NETHER; - case "the_end", "minecraft:the_end" -> THE_END; - default -> new Dimension(id); - }; - } else { - return null; - } - } - - String formatPosition(Tag tag) { - if (tag instanceof ListTag listTag && listTag.size() == 3) { - - if (listTag.getTag(0) instanceof DoubleTag x - && listTag.getTag(1) instanceof DoubleTag y - && listTag.getTag(2) instanceof DoubleTag z) { - return formatPosition(x.get(), y.get(), z.get()); - } - - return null; - } - - if (tag instanceof IntArrayTag intArrayTag) { - int x = intArrayTag.get(0); - int y = intArrayTag.get(1); - int z = intArrayTag.get(2); - - return formatPosition(x, y, z); - } - - return null; - } - - String formatPosition(int x, int y, int z) { - return this == OVERWORLD - ? "(%d, %d, %d)".formatted(x, y, z) - : "%s (%d, %d, %d)".formatted(name, x, y, z); - } - - String formatPosition(double x, double y, double z) { - return this == OVERWORLD - ? "(%.2f, %.2f, %.2f)".formatted(x, y, z) - : "%s (%.2f, %.2f, %.2f)".formatted(name, x, y, z); - } - } - - private enum Difficulty { - PEACEFUL, EASY, NORMAL, HARD; - - static final ObservableList items = FXCollections.observableList(Arrays.asList(values())); - - static Difficulty of(int d) { - return (d >= 0 && d < items.size()) ? items.get(d) : null; - } - - static Difficulty of(String name) { - for (Difficulty d : items) if (d.name().toLowerCase(Locale.ROOT).equals(name)) return d; - return null; - } - - String getTagStringValue() { - return this.name().toLowerCase(Locale.ROOT); - } - - @Override - public String toString() { - return i18n("world.info.difficulty." + name().toLowerCase(Locale.ROOT)); - } - } - - private enum GameType { - SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR, HARDCORE; - - static final ObservableList items = FXCollections.observableList(Arrays.asList(values())); - - static GameType of(int d, boolean hardcore) { - if (hardcore && d == 0) return HARDCORE; // hardcore + survival - return d >= 0 && d < 4 ? items.get(d) : null; - } - - @Override - public String toString() { - return i18n("world.info.player.game_type." + name().toLowerCase(Locale.ROOT)); - } - } - - private void changeWorldIcon() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(i18n("world.icon.choose.title")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png")); - fileChooser.setInitialFileName("icon.png"); - - Path iconPath = Controllers.showOpenDialog(fileChooser); - if (iconPath == null) return; - - Image image; - try { - image = FXUtils.loadImage(iconPath); - } catch (Exception e) { - LOG.warning("Failed to load image", e); - Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR); - return; - } - if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) { - Path output = world.getFile().resolve("icon.png"); - saveWorldIcon(iconPath, image, output); - } else { - Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR); - } - } - - private void saveWorldIcon(Path sourcePath, Image image, Path targetPath) { - Image oldImage = iconImageView.getImage(); - try { - FileUtils.copyFile(sourcePath, targetPath); - iconImageView.setImage(image); - Controllers.showToast(i18n("world.icon.change.succeed.toast")); - } catch (IOException e) { - LOG.warning("Failed to save world icon " + e.getMessage()); - iconImageView.setImage(oldImage); - } - } - - private void clearWorldIcon() { - Path output = world.getFile().resolve("icon.png"); - try { - Files.deleteIfExists(output); - iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png")); - } catch (IOException e) { - LOG.warning("Failed to delete world icon ", e); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java deleted file mode 100644 index d49749d032..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java +++ /dev/null @@ -1,426 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXPopup; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ObservableValue; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.Skin; -import javafx.scene.control.Tooltip; -import javafx.scene.input.MouseButton; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import javafx.stage.FileChooser; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.*; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.ChunkBaseApp; -import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; -import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; -import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class WorldListPage extends ListPageBase { - private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - - private Path savesDir; - private List worlds; - private @Nullable HMCLGameInstance gameInstance; - private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false); - - private int refreshCount = 0; - - /// Creates a world list that reloads when `instanceContext` changes. - /// - /// @param instanceContext the parent page's instance property - public WorldListPage(ObservableValue instanceContext) { - Objects.requireNonNull(instanceContext, "instanceContext"); - FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { - installWorld(modpacks.get(0)); - }); - - showAll.addListener(e -> updateWorldList()); - - listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> { - if (current != null) { - loadInstance(current); - } - })); - } - - @Override - protected Skin createDefaultSkin() { - return new WorldListPageSkin(); - } - - public void loadInstance(HMCLGameInstance.Optional instance) { - this.gameInstance = instance.instance(); - this.savesDir = gameInstance != null ? gameInstance.getSavesDirectory() : null; - refresh(); - } - - private void updateWorldList() { - if (worlds == null || gameInstance == null) { - getItems().clear(); - } else if (showAll.get()) { - getItems().setAll(worlds); - } else { - GameVersionNumber gameVersion = gameInstance.getVersion(); - getItems().setAll(worlds.stream() - .filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion)) - .toList()); - } - } - - public void refresh() { - if (gameInstance == null || savesDir == null) - return; - - int currentRefresh = ++refreshCount; - HMCLGameInstance gameInstance = this.gameInstance; - - setLoading(true); - Task.supplyAsync(Schedulers.io(), () -> { - // Ensure the game version number is parsed - gameInstance.getVersion(); - return World.getWorlds(savesDir); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (refreshCount != currentRefresh) { - // A newer refresh task is running, discard this result - return; - } - - supportQuickPlay.set(World.supportQuickPlay(gameInstance.getVersion())); - - worlds = result; - updateWorldList(); - - if (exception != null) - LOG.warning("Failed to load world list page", exception); - - setLoading(false); - }).start(); - } - - public void openSavesFolder() { - if (gameInstance != null) { - FXUtils.openFolder(gameInstance.getSavesDirectory()); - } - } - - public void add() { - FileChooser chooser = new FileChooser(); - chooser.setTitle(i18n("world.add.title")); - chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.world"), "*.zip")); - List res = Controllers.showOpenMultipleDialog(chooser); - - if (res == null || res.isEmpty()) return; - installWorld(res.get(0)); - } - - private void installWorld(Path zipFile) { - // Only accept one world file because user is required to confirm the new world name - // Or too many input dialogs are popped. - Task.supplyAsync(() -> new World(zipFile)) - .whenComplete(Schedulers.javafx(), world -> { - Controllers.prompt(i18n("world.name.enter"), (name, handler) -> { - Task.runAsync(() -> world.install(savesDir, name)) - .whenComplete(Schedulers.javafx(), () -> { - handler.resolve(); - refresh(); - }, e -> { - if (e instanceof FileAlreadyExistsException) - handler.reject(i18n("world.add.failed", i18n("world.add.already_exists"))); - else if (e instanceof IOException && e.getCause() instanceof InvalidPathException) - handler.reject(i18n("world.add.failed", i18n("install.new_game.malformed"))); - else - handler.reject(i18n("world.add.failed", e.getClass().getName() + ": " + e.getLocalizedMessage())); - }).start(); - }, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); - }, e -> { - LOG.warning("Unable to parse world file " + zipFile, e); - Controllers.dialog(i18n("world.add.invalid")); - }).start(); - } - - private void showManagePage(World world) { - if (gameInstance != null) { - Controllers.navigate(new WorldManagePage(world, gameInstance)); - } - } - - public void export(World world) { - WorldManageUIUtils.export(world); - } - - public void delete(World world) { - WorldManageUIUtils.delete(world, this::refresh); - } - - public void copy(World world) { - WorldManageUIUtils.copyWorld(world, this::refresh); - } - - public void reveal(World world) { - FXUtils.openFolder(world.getFile()); - } - - public void launch(World world) { - if (gameInstance != null) { - Instances.launchAndEnterWorld(gameInstance, world.getFileName()); - } - } - - public void generateLaunchScript(World world) { - if (gameInstance != null) { - Instances.generateLaunchScriptForQuickEnterWorld(gameInstance, world.getFileName()); - } - } - - public BooleanProperty showAllProperty() { - return showAll; - } - - public ReadOnlyBooleanProperty supportQuickPlayProperty() { - return supportQuickPlay; - } - - private final class WorldListPageSkin extends ToolbarListPageSkin { - - WorldListPageSkin() { - super(WorldListPage.this); - - StackPane placeholderContainer = new StackPane(); - placeholderContainer.getStyleClass().add("notice-pane"); - Label placeholderLabel = new Label(i18n("world.empty")); - placeholderContainer.getChildren().add(placeholderLabel); - listView.setPlaceholder(placeholderContainer); - } - - @Override - protected List initializeToolbar(WorldListPage skinnable) { - JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all")); - chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty()); - - return Arrays.asList( - chkShowAll, - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openSavesFolder) - ); - } - - @Override - protected ListCell createListCell(JFXListView listView) { - return new WorldListCell(getSkinnable()); - } - } - - private static final class WorldListCell extends ListCell { - - private final WorldListPage page; - - private final RipplerContainer graphic; - private final ImageContainer imageView; - private final Tooltip leftTooltip; - private final TwoLineListItem content; - private final JFXButton btnLaunch; - - /// Creates a world row with launch and management actions for the supplied page. - public WorldListCell(WorldListPage page) { - this.page = page; - - var root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); - root.setPadding(new Insets(8)); - - { - StackPane left = new StackPane(); - this.leftTooltip = new Tooltip(); - FXUtils.installSlowTooltip(left, leftTooltip); - root.setLeft(left); - left.setPadding(new Insets(0, 8, 0, 0)); - - this.imageView = new ImageContainer(32); - left.getChildren().add(imageView); - } - - { - this.content = new TwoLineListItem(); - root.setCenter(content); - } - - { - HBox right = new HBox(8); - root.setRight(right); - right.setAlignment(Pos.CENTER_RIGHT); - - btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); - btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty()); - btnLaunch.managedProperty().bind(btnLaunch.visibleProperty()); - right.getChildren().add(btnLaunch); - FXUtils.installFastTooltip(btnLaunch, i18n("instance.launch")); - btnLaunch.setOnAction(event -> { - World world = getItem(); - if (world != null) - page.launch(world); - }); - - JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); - right.getChildren().add(btnMore); - btnMore.setOnAction(event -> { - World world = getItem(); - if (world != null) - showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); - }); - } - - this.graphic = new RipplerContainer(root); - graphic.setOnMouseClicked(event -> { - if (event.getClickCount() != 1) - return; - - World world = getItem(); - if (world == null) - return; - - if (event.getButton() == MouseButton.PRIMARY) - page.showManagePage(world); - else if (event.getButton() == MouseButton.SECONDARY) - showPopupMenu(world, page.supportQuickPlayProperty().get(), JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); - }); - } - - /// Updates world details and replaces version and lock tags together. - @Override - protected void updateItem(@Nullable World world, boolean empty) { - @Nullable World oldWorld = getItem(); - boolean oldEmpty = isEmpty(); - - super.updateItem(world, empty); - - if (oldWorld == world && oldEmpty == empty) return; - - this.graphic.resetFeedback(); - - if (empty || world == null) { - content.getTags().clear(); - setGraphic(null); - imageView.setImage(null); - leftTooltip.setText(""); - content.setTitle(""); - content.setSubtitle(""); - } else { - imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); - leftTooltip.setText(world.getFile().toString()); - content.setTitle(world.getWorldName() != null ? parseColorEscapes(world.getWorldName()) : ""); - - var tags = new ArrayList(2); - if (world.getGameVersion() != null) - tags.add(new Tag(I18n.getDisplayVersion(world.getGameVersion()))); - if (world.isLocked()) { - tags.add(new Tag(i18n("world.locked"))); - btnLaunch.setDisable(true); - } else { - btnLaunch.setDisable(false); - } - - content.getTags().setAll(tags); - content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())))); - - setGraphic(graphic); - } - } - - // Popup Menu - - public void showPopupMenu(World world, boolean supportQuickPlay, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { - boolean worldLocked = world.isLocked(); - - PopupMenu popupMenu = new PopupMenu(); - JFXPopup popup = new JFXPopup(popupMenu); - - if (supportQuickPlay) { - - IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_enter_world"), () -> page.launch(world), popup); - launchItem.setDisable(worldLocked); - - popupMenu.getContent().addAll( - launchItem, - new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> page.generateLaunchScript(world), popup), - new MenuSeparator() - ); - } - - popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), () -> page.showManagePage(world), popup)); - - if (ChunkBaseApp.isSupported(world)) { - popupMenu.getContent().addAll( - new MenuSeparator(), - new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup), - new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup), - new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup) - ); - - if (ChunkBaseApp.supportEndCity(world)) { - popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup)); - } - } - - IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup); - exportMenuItem.setDisable(worldLocked); - - IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE_FOREVER, i18n("world.delete"), () -> page.delete(world), popup); - deleteMenuItem.setDisable(worldLocked); - - IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup); - duplicateMenuItem.setDisable(worldLocked); - - popupMenu.getContent().addAll( - new MenuSeparator(), - exportMenuItem, - deleteMenuItem, - duplicateMenuItem - ); - - popupMenu.getContent().addAll( - new MenuSeparator(), - new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), () -> page.reveal(world), popup) - ); - - JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup); - popup.show(this, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY); - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java deleted file mode 100644 index cab26df1a0..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java +++ /dev/null @@ -1,250 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import com.jfoenix.controls.JFXPopup; -import javafx.application.Platform; -import javafx.beans.property.*; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; -import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.ChunkBaseApp; -import org.jackhuang.hmcl.util.StringUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.nio.file.Path; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -/// Displays world information, backups, and management actions. -public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage { - - private final World world; - private final Path backupsDir; - private final HMCLGameInstance gameInstance; - private final boolean supportQuickPlay; - /// The active world lock, or null when the world is read-only. - private @Nullable FileChannel sessionLockChannel; - - private final ObjectProperty state; - private boolean isFirstNavigation = true; - private final BooleanProperty refreshable = new SimpleBooleanProperty(true); - private final BooleanProperty readOnly = new SimpleBooleanProperty(false); - - private final TransitionPane transitionPane = new TransitionPane(); - /// Manages tab selection and content transitions independently of the sidebar. - private final TabController tabController = new TabController(transitionPane); - /// The tab displaying information about this world. - private final TabItem worldInfoTab; - /// The tab for managing backups of this world. - private final TabItem worldBackupsTab; - /// The tab for managing data packs in this world. - private final TabItem dataPackTab; - - /// Loads world metadata and creates its tabs and sidebar actions. - public WorldManagePage(World world, HMCLGameInstance gameInstance) { - this.world = world; - this.gameInstance = gameInstance; - this.backupsDir = gameInstance.getBackupsDirectory(); - - updateSessionLockChannel(); - - try { - this.world.reloadWorldData(); - } catch (IOException e) { - LOG.warning("Can not load world data of world: " + this.world.getFile(), e); - this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail()); - } - - this.worldInfoTab = new TabItem<>(() -> new WorldInfoPage(this)); - this.worldBackupsTab = new TabItem<>(() -> new WorldBackupsPage(this)); - this.dataPackTab = new TabItem<>(() -> new DataPackListPage(this)); - - this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true)); - - supportQuickPlay = World.supportQuickPlay(gameInstance.getVersion()); - - this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited); - this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); - - setContent(transitionPane); - initializeSidebar(); - setSidebarBottom(createToolBar()); - } - - /// Reloads world metadata and refreshes only tab pages whose content is already initialized. - @Override - public void refresh() { - updateSessionLockChannel(); - try { - world.reloadWorldData(); - } catch (IOException e) { - LOG.warning("Can not load world data of world: " + world.getFile(), e); - closePageForLoadingFail(); - return; - } - - for (var tab : tabController.getTabs()) { - if (tab.isInitialized() && tab.getNode() instanceof WorldRefreshable r) { - r.refresh(); - } - } - } - - private void closePageForLoadingFail() { - Platform.runLater(() -> { - fireEvent(new PageCloseEvent()); - Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR); - }); - } - - private void updateSessionLockChannel() { - if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { - sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - readOnly.set(sessionLockChannel == null); - } - } - - private void onNavigated(Navigator.NavigationEvent event) { - if (isFirstNavigation) - isFirstNavigation = false; - else - refresh(); - } - - public void onExited(Navigator.NavigationEvent event) { - try { - WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel); - } catch (IOException ignored) { - } - } - - public void launch() { - fireEvent(new PageCloseEvent()); - Instances.launchAndEnterWorld(gameInstance, world.getFileName()); - } - - public void generateLaunchScript() { - Instances.generateLaunchScriptForQuickEnterWorld(gameInstance, world.getFileName()); - } - - @Override - public ReadOnlyObjectProperty stateProperty() { - return state; - } - - public void setTitle(String title) { - this.state.set(new DecoratorPage.State(title, null, true, true, true)); - } - - public World getWorld() { - return world; - } - - public Path getBackupsDir() { - return backupsDir; - } - - public boolean isReadOnly() { - return readOnly.get(); - } - - public BooleanProperty readOnlyProperty() { - return readOnly; - } - - @Override - public BooleanProperty refreshableProperty() { - return refreshable; - } - - /// Registers supported world tabs and selects the information tab. - private void initializeSidebar() { - tabController.getTabs().addAll(worldInfoTab, worldBackupsTab); - tabController.selectTab(worldInfoTab); - - getSidebar().addNavigationDrawerTab(tabController, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL) - .addNavigationDrawerTab(tabController, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL); - - if (world.supportDataPacks()) { - tabController.getTabs().add(dataPackTab); - getSidebar().addNavigationDrawerTab(tabController, dataPackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL); - } - } - - /// Creates world actions and binds write operations to the lock state. - private AdvancedListBox createToolBar() { - AdvancedListBox toolbar = new AdvancedListBox(); - { - if (supportQuickPlay) { - toolbar.addNavigationDrawerItem(i18n("instance.launch"), SVG.ROCKET_LAUNCH, () -> launch(), advancedListItem -> advancedListItem.disableProperty().bind(readOnlyProperty())); - } - - if (ChunkBaseApp.isSupported(world)) { - PopupMenu chunkBasePopupMenu = new PopupMenu(); - JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu); - - chunkBasePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup), - new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), chunkBasePopup), - new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), chunkBasePopup) - ); - - if (ChunkBaseApp.supportEndCity(world)) { - chunkBasePopupMenu.getContent().add( - new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup)); - } - - toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem -> - chunkBaseMenuItem.setOnAction(e -> - chunkBasePopup.show(chunkBaseMenuItem, - JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, - chunkBaseMenuItem.getWidth(), 0))); - } - - toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile())); - - { - PopupMenu managePopupMenu = new PopupMenu(); - JFXPopup managePopup = new JFXPopup(managePopupMenu); - - if (supportQuickPlay) { - managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch"), () -> launch(), managePopup), - new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> generateLaunchScript(), managePopup), - new MenuSeparator() - ); - } - - managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), - new IconedMenuItem(SVG.DELETE_FOREVER, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null), managePopup) - ); - - toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> - { - managePopupMenuItem.setOnAction(e -> - managePopup.show(managePopupMenuItem, - JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, - managePopupMenuItem.getWidth(), 0)); - managePopupMenuItem.disableProperty().bind(readOnlyProperty()); - }); - } - } - return toolbar; - } - - public interface WorldRefreshable { - void refresh(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java deleted file mode 100644 index b264f3e32d..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors -// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL -package org.jackhuang.hmcl.ui.instances; - -import javafx.stage.FileChooser; -import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.game.WorldLockedException; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; -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.wizard.SinglePageWizardProvider; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.FileUtils; - -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class WorldManageUIUtils { - private WorldManageUIUtils() { - } - - public static void delete(World world, Runnable runnable) { - delete(world, runnable, null); - } - - public static void delete(World world, Runnable runnable, FileChannel sessionLockChannel) { - Controllers.confirm( - i18n("button.remove.confirm"), - i18n("world.delete"), - () -> Task.runAsync(() -> closeSessionLockChannel(world, sessionLockChannel)) - .thenRunAsync(world::delete) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - runnable.run(); - } else if (exception instanceof WorldLockedException) { - Controllers.dialog(i18n("world.locked.failed"), null, MessageDialogPane.MessageType.WARNING); - } else { - Controllers.dialog(i18n("world.delete.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING); - } - }).start(), - null - ); - } - - public static void export(World world) { - export(world, null); - } - - public static void export(World world, FileChannel sessionLockChannel) { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(i18n("world.export.title")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip")); - fileChooser.setInitialFileName(world.getWorldName() + ".zip"); - Path file = Controllers.showSaveDialog(fileChooser); - if (file == null) { - return; - } - - try { - closeSessionLockChannel(world, sessionLockChannel); - } catch (IOException e) { - return; - } - - Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file, controller::onFinish))); - } - - public static void copyWorld(World world, Runnable runnable) { - Path worldPath = world.getFile(); - Controllers.prompt( - i18n("world.duplicate.prompt"), - (result, handler) -> { - Path targetDir = worldPath.resolveSibling(result); - if (Files.exists(targetDir)) { - handler.reject(i18n("world.duplicate.failed.already_exists")); - return; - } - - Task.runAsync(Schedulers.io(), () -> world.copy(result)) - .thenAcceptAsync(Schedulers.javafx(), (Void) -> Controllers.showToast(i18n("world.duplicate.success.toast"))) - .thenAcceptAsync(Schedulers.javafx(), (Void) -> { - if (runnable != null) { - runnable.run(); - } - } - ).whenComplete(Schedulers.javafx(), (throwable) -> { - if (throwable == null) { - handler.resolve(); - } else { - handler.reject(i18n("world.duplicate.failed")); - LOG.warning("Failed to duplicate world " + world.getFile(), throwable); - } - }) - .start(); - }, "", new RequiredValidator(), new Validator(i18n("world.duplicate.failed.invalid_name"), FileUtils::isNameValid)); - } - - public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { - if (sessionLockChannel != null) { - try { - sessionLockChannel.close(); - LOG.info("Closed session lock channel of the world " + world.getFileName()); - } catch (IOException e) { - throw new IOException("Failed to close session lock channel of the world " + world.getFile(), e); - } - } - } - - public static FileChannel getSessionLockChannel(World world) { - try { - FileChannel lock = world.lock(); - LOG.info("Acquired lock on world " + world.getFileName()); - return lock; - } catch (WorldLockedException ignored) { - return null; - } - } - -}