diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java index 8050beed9d..da573cfae7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java @@ -20,6 +20,8 @@ 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.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -27,28 +29,41 @@ 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.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.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 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; @@ -70,98 +85,89 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage { private final Path localAddonDirectory; private final ObservableList objects; - @SuppressWarnings("unchecked") + private final JFXListView listView; + public AddonUpdatesPage(DownloadProvider downloadProvider, Path localAddonDirectory, AddonUpdates updates) { this.downloadProvider = downloadProvider; this.localAddonDirectory = localAddonDirectory; getStyleClass().add("gray-background"); - TableColumn enabledColumn = new TableColumn<>(); - var allEnabledBox = new JFXCheckBox(); - enabledColumn.setStyle("-fx-alignment: CENTER;"); - enabledColumn.setGraphic(allEnabledBox); - enabledColumn.setCellFactory(JFXCheckBoxTableCell.forTableColumn(enabledColumn)); - setupCellValueFactory(enabledColumn, AddonUpdateObject::enabledProperty); - enabledColumn.setEditable(true); - enabledColumn.setMaxWidth(40); - enabledColumn.setMinWidth(40); - - TableColumn fileNameColumn = new TableColumn<>(i18n("addon.check_update.file")); - fileNameColumn.setPrefWidth(180); - setupCellValueFactory(fileNameColumn, AddonUpdateObject::fileNameProperty); - - TableColumn currentVersionColumn = new TableColumn<>(i18n("addon.check_update.current_version")); - currentVersionColumn.setPrefWidth(180); - setupCellValueFactory(currentVersionColumn, AddonUpdateObject::currentVersionProperty); - - TableColumn targetVersionColumn = new TableColumn<>(i18n("addon.check_update.target_version")); - targetVersionColumn.setPrefWidth(180); - setupCellValueFactory(targetVersionColumn, AddonUpdateObject::targetVersionProperty); - - TableColumn targetVersionTypeColumn = new TableColumn<>(i18n("addon.version_type")); - setupCellValueFactory(targetVersionTypeColumn, AddonUpdateObject::targetVersionTypeProperty); - - TableColumn sourceColumn = new TableColumn<>(i18n("addon.check_update.source")); - setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty); - - TableColumn changelogColumn = new TableColumn<>(i18n("addon.changelog")); - { - var oldCellFactory = changelogColumn.getCellFactory(); - changelogColumn.setCellFactory(param -> { - TableCell cell = oldCellFactory.call(param); - cell.getStyleClass().add("addon-changelog-table-cell"); - cell.setOnMouseClicked(event -> { - AddonUpdateObject object; - if (cell.isEmpty() || cell.getTableRow() == null || (object = cell.getTableRow().getItem()) == null) return; - Controllers.dialog(new AddonChangelog(object)); - }); - return cell; - }); - changelogColumn.setCellValueFactory(__ -> new SimpleStringProperty(i18n("button.view"))); - } - 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(); - TableView table = new TableView<>(objects); - table.setEditable(true); - table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, targetVersionTypeColumn, sourceColumn, changelogColumn); - setMargin(table, new Insets(10, 10, 5, 10)); - - setCenter(table); - - HBox actions = new HBox(8); - actions.setPadding(new Insets(8)); - actions.setAlignment(Pos.CENTER_RIGHT); - - 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))); - }); + 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.setPlaceholder(placeholderContainer); + + setMargin(listView, new Insets(0, 10, 0, 10)); + VBox.setVgrow(listView, Priority.ALWAYS); + + listView.setCellFactory(listView -> new AddonUpdateListCell()); + + ComponentList.setVgrow(listView, Priority.ALWAYS); + + centrePane.getContent().setAll(listView); + + centerWrapper.getChildren().setAll(centrePane); + + setCenter(centerWrapper); } - JFXButton exportListButton = FXUtils.newRaisedButton(i18n("button.export")); - exportListButton.setOnAction(e -> exportList()); + { + HBox actions = new HBox(8); + actions.setPadding(new Insets(8)); + actions.setAlignment(Pos.CENTER_RIGHT); - JFXButton nextButton = FXUtils.newRaisedButton(i18n("addon.check_update.confirm")); - nextButton.setOnAction(e -> updateFiles()); + JFXCheckBox allEnabledBox = new JFXCheckBox(); + allEnabledBox.setText(i18n("button.select_all")); + FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new)); - JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel")); - cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent())); - onEscPressed(this, cancelButton::fire); - onEscPressed(table, cancelButton::fire); + 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))); + }); + } - actions.getChildren().setAll(showPreviewCheck, exportListButton, nextButton, cancelButton); - setBottom(actions); + 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 setupCellValueFactory(TableColumn column, Function> mapper) { @@ -240,6 +246,8 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage { final StringProperty source = new SimpleStringProperty(); String changelog = null; + final ItemPropertyAsyncCache iconCache; + public AddonUpdateObject(AddonUpdate data) { this.data = data; @@ -252,6 +260,12 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage { case CURSEFORGE -> i18n("addon.curseforge"); case MODRINTH -> i18n("addon.modrinth"); }); + + iconCache = new ItemPropertyAsyncCache.Soft<>( + this, + this::loadIcon, + this::getDefaultIcon + ); } public AddonUpdate getData() { @@ -270,6 +284,18 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage { return fileName; } + public void setFileName(String fileName) { + this.fileName.set(fileName); + } + + public LocalAddonFile getLocalAddonFile() { + return data.localAddonFile(); + } + + public String getCurrentVersion() { + return currentVersion.get(); + } + public StringProperty currentVersionProperty() { return currentVersion; } @@ -286,6 +312,163 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage { return source; } + public void setSource(String source) { + this.source.set(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() { + HBox hbox = new HBox(16); + HBox.setHgrow(content, Priority.ALWAYS); + content.setAlignment(Pos.CENTER); + 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()); + } + }); + } + + @Override + public void updateItem(AddonUpdateObject item, boolean empty) { + AddonUpdateObject oldItem = getItem(); + + if (oldItem != null) { + enabledBox.selectedProperty().unbindBidirectional(oldItem.enabledProperty()); + } + + super.updateItem(item, empty); + + 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(); + + if (oldItem != item) { + content.getTags().clear(); + + if (addonFile instanceof LocalModFile modFile) { + content.addTag(modFile.getId()); + content.addTag(item.source.get()); + } else if (addonFile instanceof ResourcePackFile) { + content.addTag(item.source.get()); + } + } + + if (addonFile instanceof LocalModFile modFile) { + content.setTitle(modFile.getName()); + } else if (addonFile instanceof ResourcePackFile resourcePackFile) { + content.setTitle(resourcePackFile.getFileName()); + } + + content.setSubtitle( + item.getCurrentVersion() + + " → " + + item.targetVersion.get() + + " | " + + item.targetVersionType.get() + ); + + setGraphic(pane); + } } private static final class AddonChangelog extends JFXDialogLayout { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 3d40150fb9..a3acd76c64 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -175,6 +175,7 @@ addon.check_update.empty=All files are up-to-date addon.check_update.failed_check=Failed to check for updates. addon.check_update.failed_download=Failed to download some files. addon.check_update.file=File +addon.check_update.only_preview=Updates are available only for preview versions.\nEnable "Show preview versions" below to view them. addon.check_update.preview=Show preview versions addon.check_update.source=Source addon.check_update.target_version=Target Version diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties index 42c0e0a60e..c47d6a8955 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -175,6 +175,7 @@ addon.check_update.empty=没有需要更新的文件 addon.check_update.failed_check=检查更新失败 addon.check_update.failed_download=部分文件下载失败 addon.check_update.file=文件 +addon.check_update.only_preview=仅找到测试版本的更新\n你可以启用下方的“显示测试版本”来查看 addon.check_update.preview=显示测试版本 addon.check_update.source=来源 addon.check_update.target_version=目标版本 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties index 5257e0b50d..703cf92a0f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -173,6 +173,7 @@ addon.check_update.empty=沒有需要更新的檔案 addon.check_update.failed_check=檢查更新失敗 addon.check_update.failed_download=部分檔案下載失敗 addon.check_update.file=檔案 +addon.check_update.only_preview=僅找到測試版本的更新\n你可以啟用下方的“顯示 Alpha/Beta 版本”來查看 addon.check_update.preview=顯示 Alpha/Beta 版本 addon.check_update.source=來源 addon.check_update.target_version=目標版本