mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2026-10-29 07:06:49 +08:00
feat: 参考 VersionsPage 样式改进 AddonUpdatesPage (#6970)
This commit is contained in:
@@ -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=目標版本
|
||||
|
||||
Reference in New Issue
Block a user