feat: 参考 VersionsPage 样式改进 AddonUpdatesPage (#6970)

This commit is contained in:
快乐凤爪王
2026-10-16 21:51:41 +08:00
committed by GitHub
parent 68b3a9f076
commit 93eaee317c
4 changed files with 262 additions and 76 deletions
@@ -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<AddonUpdateObject> objects;
@SuppressWarnings("unchecked")
private final JFXListView<AddonUpdateObject> listView;
public AddonUpdatesPage(DownloadProvider downloadProvider, Path localAddonDirectory, AddonUpdates updates) {
this.downloadProvider = downloadProvider;
this.localAddonDirectory = localAddonDirectory;
getStyleClass().add("gray-background");
TableColumn<AddonUpdateObject, Boolean> 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<AddonUpdateObject, String> fileNameColumn = new TableColumn<>(i18n("addon.check_update.file"));
fileNameColumn.setPrefWidth(180);
setupCellValueFactory(fileNameColumn, AddonUpdateObject::fileNameProperty);
TableColumn<AddonUpdateObject, String> currentVersionColumn = new TableColumn<>(i18n("addon.check_update.current_version"));
currentVersionColumn.setPrefWidth(180);
setupCellValueFactory(currentVersionColumn, AddonUpdateObject::currentVersionProperty);
TableColumn<AddonUpdateObject, String> targetVersionColumn = new TableColumn<>(i18n("addon.check_update.target_version"));
targetVersionColumn.setPrefWidth(180);
setupCellValueFactory(targetVersionColumn, AddonUpdateObject::targetVersionProperty);
TableColumn<AddonUpdateObject, String> targetVersionTypeColumn = new TableColumn<>(i18n("addon.version_type"));
setupCellValueFactory(targetVersionTypeColumn, AddonUpdateObject::targetVersionTypeProperty);
TableColumn<AddonUpdateObject, String> sourceColumn = new TableColumn<>(i18n("addon.check_update.source"));
setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty);
TableColumn<AddonUpdateObject, String> changelogColumn = new TableColumn<>(i18n("addon.changelog"));
{
var oldCellFactory = changelogColumn.getCellFactory();
changelogColumn.setCellFactory(param -> {
TableCell<AddonUpdateObject, String> 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<AddonUpdateObject> 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<Subscription> 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<Subscription> 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 <T> void setupCellValueFactory(TableColumn<AddonUpdateObject, T> column, Function<AddonUpdateObject, ObservableValue<T>> mapper) {
@@ -240,6 +246,8 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
final StringProperty source = new SimpleStringProperty();
String changelog = null;
final ItemPropertyAsyncCache<Image, AddonUpdateObject> 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<String> 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<AddonUpdateObject> {
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 {
@@ -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
@@ -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=目标版本
@@ -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=目標版本