优化模组管理布局 (#3229)

Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
Zkitefly
2025-10-03 21:20:52 +08:00
committed by GitHub
parent 2b43ee24a1
commit dda30274a4
2 changed files with 157 additions and 76 deletions

View File

@@ -48,4 +48,8 @@ public enum VersionIconType {
public Image getIcon() {
return FXUtils.newBuiltinImage(resourceUrl);
}
public Image getIcon(int size) {
return FXUtils.newBuiltinImage(resourceUrl, size, size, true, true);
}
}

View File

@@ -27,6 +27,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
@@ -45,6 +46,7 @@ import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -64,6 +66,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -81,7 +84,7 @@ import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
class ModListPageSkin extends SkinBase<ModListPage> {
final class ModListPageSkin extends SkinBase<ModListPage> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
@@ -279,6 +282,85 @@ class ModListPageSkin extends SkinBase<ModListPage> {
}
}
private static Task<Image> loadModIcon(LocalModFile modFile, int size) {
return Task.supplyAsync(() -> {
List<String> iconPaths = new ArrayList<>();
if (StringUtils.isNotBlank(modFile.getLogoPath())) {
iconPaths.add(modFile.getLogoPath());
}
iconPaths.addAll(List.of(
"icon.png",
"logo.png",
"mod_logo.png",
"pack.png",
"logoFile.png",
"assets/icon.png",
"assets/logo.png",
"assets/mod_icon.png",
"assets/mod_logo.png",
"META-INF/icon.png",
"META-INF/logo.png",
"META-INF/mod_icon.png",
"textures/icon.png",
"textures/logo.png",
"textures/mod_icon.png",
"resources/icon.png",
"resources/logo.png",
"resources/mod_icon.png"
));
String modId = modFile.getId();
if (StringUtils.isNotBlank(modId)) {
iconPaths.addAll(List.of(
"assets/" + modId + "/icon.png",
"assets/" + modId + "/logo.png",
"assets/" + modId.replace("-", "") + "/icon.png",
"assets/" + modId.replace("-", "") + "/logo.png",
modId + ".png",
modId + "-logo.png",
modId + "-icon.png",
modId + "_logo.png",
modId + "_icon.png",
"textures/" + modId + "/icon.png",
"textures/" + modId + "/logo.png",
"resources/" + modId + "/icon.png",
"resources/" + modId + "/logo.png"
));
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.getFile())) {
for (String path : iconPaths) {
Path iconPath = fs.getPath(path);
if (Files.exists(iconPath)) {
Image image = FXUtils.loadImage(iconPath, size, size, 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);
}
VersionIconType defaultIcon = switch (modFile.getModLoaderType()) {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGED -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
case CLEANROOM -> VersionIconType.CLEANROOM;
default -> VersionIconType.COMMAND;
};
return defaultIcon.getIcon(size);
});
}
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
private final BooleanProperty active;
private final LocalModFile localModFile;
@@ -286,22 +368,25 @@ class ModListPageSkin extends SkinBase<ModListPage> {
private final String message;
private final ModTranslations.Mod mod;
private SoftReference<Image> iconCache;
ModInfoObject(LocalModFile localModFile) {
this.localModFile = localModFile;
this.active = localModFile.activeProperty();
StringBuilder title = new StringBuilder(localModFile.getName());
if (isNotBlank(localModFile.getVersion()))
title.append(" ").append(localModFile.getVersion());
this.title = title.toString();
StringBuilder message = new StringBuilder(localModFile.getFileName());
if (isNotBlank(localModFile.getGameVersion()))
message.append(", ").append(i18n("mods.game.version")).append(": ").append(localModFile.getGameVersion());
if (isNotBlank(localModFile.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(localModFile.getAuthors());
this.message = message.toString();
this.title = localModFile.getName();
List<String> parts = new ArrayList<>();
if (isNotBlank(localModFile.getId())) {
parts.add(localModFile.getId());
}
if (isNotBlank(localModFile.getVersion())) {
parts.add(localModFile.getVersion());
}
if (isNotBlank(localModFile.getGameVersion())) {
parts.add(i18n("game.version") + ": " + localModFile.getGameVersion());
}
this.message = String.join(", ", parts);
this.mod = ModTranslations.MOD.getModById(localModFile.getId());
}
@@ -334,77 +419,45 @@ class ModListPageSkin extends SkinBase<ModListPage> {
titleContainer.setSpacing(8);
ImageView imageView = new ImageView();
Task.supplyAsync(() -> {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
String logoPath = modInfo.getModInfo().getLogoPath();
if (StringUtils.isNotBlank(logoPath)) {
Path iconPath = fs.getPath(logoPath);
if (Files.exists(iconPath)) {
try {
Image image = FXUtils.loadImage(iconPath, 40, 40, true, true);
if (!image.isError() && image.getWidth() == image.getHeight())
return image;
} catch (Throwable e) {
LOG.warning("Failed to load image " + logoPath, e);
}
}
}
List<String> defaultPaths = new ArrayList<>(Arrays.asList(
"icon.png",
"logo.png",
"mod_logo.png",
"pack.png",
"logoFile.png"
));
String id = modInfo.getModInfo().getId();
if (StringUtils.isNotBlank(id)) {
defaultPaths.addAll(Arrays.asList(
"assets/" + id + "/icon.png",
"assets/" + id.replace("-", "") + "/icon.png",
id + ".png",
id + "-logo.png",
id + "-icon.png",
id + "_logo.png",
id + "_icon.png"
));
}
for (String path : defaultPaths) {
Path iconPath = fs.getPath(path);
if (Files.exists(iconPath)) {
Image image = FXUtils.loadImage(iconPath, 40, 40, true, true);
if (!image.isError() && image.getWidth() == image.getHeight())
return image;
}
}
} catch (Exception e) {
LOG.warning("Failed to load icon", e);
}
return null;
}).whenComplete(Schedulers.javafx(), (image, exception) -> {
if (image != null) {
imageView.setImage(image);
} else {
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
}
}).start();
FXUtils.limitSize(imageView, 40, 40);
loadModIcon(modInfo.getModInfo(), 40)
.whenComplete(Schedulers.javafx(), (image, exception) -> {
imageView.setImage(image);
}).start();
TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName());
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
title.addTag(modInfo.getModInfo().getVersion());
if (modInfo.getMod() != null) {
title.addTag(modInfo.getMod().getDisplayName());
}
title.setSubtitle(FileUtils.getName(modInfo.getModInfo().getFile()));
List<String> subtitleParts = new ArrayList<>();
subtitleParts.add(FileUtils.getName(modInfo.getModInfo().getFile()));
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
subtitleParts.add(modInfo.getModInfo().getGameVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
subtitleParts.add(modInfo.getModInfo().getVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
subtitleParts.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
}
title.setSubtitle(String.join(", ", subtitleParts));
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
setHeading(titleContainer);
Label description = new Label(modInfo.getModInfo().getDescription().toString());
description.setWrapText(true);
FXUtils.copyOnDoubleClick(description);
setBody(description);
ScrollPane descriptionPane = new ScrollPane(description);
FXUtils.smoothScrolling(descriptionPane);
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
setBody(descriptionPane);
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
@@ -512,6 +565,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
final class ModInfoListCell extends MDListCell<ModInfoObject> {
JFXCheckBox checkBox = new JFXCheckBox();
ImageView imageView = new ImageView();
TwoLineListItem content = new TwoLineListItem();
JFXButton restoreButton = new JFXButton();
JFXButton infoButton = new JFXButton();
@@ -528,6 +582,11 @@ class ModListPageSkin extends SkinBase<ModListPage> {
content.setMouseTransparent(true);
setSelectable();
imageView.setFitWidth(24);
imageView.setFitHeight(24);
imageView.setPreserveRatio(true);
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 24, 24, true, true));
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24), 24, 24));
@@ -539,7 +598,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
infoButton.getStyleClass().add("toggle-icon4");
infoButton.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(Theme.blackFill(), 24), 24, 24));
container.getChildren().setAll(checkBox, content, restoreButton, revealButton, infoButton);
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
StackPane.setMargin(container, new Insets(8));
getContainer().getChildren().setAll(container);
@@ -548,6 +607,19 @@ class ModListPageSkin extends SkinBase<ModListPage> {
@Override
protected void updateControl(ModInfoObject dataItem, boolean empty) {
if (empty) return;
SoftReference<Image> iconCache = dataItem.iconCache;
Image icon;
if (iconCache != null && (icon = iconCache.get()) != null) {
imageView.setImage(icon);
} else {
loadModIcon(dataItem.getModInfo(), 24)
.whenComplete(Schedulers.javafx(), (image, exception) -> {
dataItem.iconCache = new SoftReference<>(image);
imageView.setImage(image);
}).start();
}
content.setTitle(dataItem.getTitle());
content.getTags().clear();
switch (dataItem.getModInfo().getModLoaderType()) {
@@ -571,9 +643,14 @@ class ModListPageSkin extends SkinBase<ModListPage> {
break;
}
if (dataItem.getMod() != null && I18n.isUseChinese()) {
content.addTag(dataItem.getMod().getDisplayName());
if (isNotBlank(dataItem.getSubtitle())) {
content.setSubtitle(dataItem.getSubtitle() + ", " + dataItem.getMod().getDisplayName());
} else {
content.setSubtitle(dataItem.getMod().getDisplayName());
}
} else {
content.setSubtitle(dataItem.getSubtitle());
}
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}