From dda30274a49da231793b93b0e0ea7df460380a3d Mon Sep 17 00:00:00 2001 From: Zkitefly <64117916+zkitefly@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:20:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A8=A1=E7=BB=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=B8=83=E5=B1=80=20(#3229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Glavo --- .../hmcl/setting/VersionIconType.java | 4 + .../hmcl/ui/versions/ModListPageSkin.java | 229 ++++++++++++------ 2 files changed, 157 insertions(+), 76 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java index 51c178130..703c0bf82 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java @@ -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); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index 28f5f4cbb..3bf95b4c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -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 { +final class ModListPageSkin extends SkinBase { private final TransitionPane toolbarPane; private final HBox searchBar; @@ -279,6 +282,85 @@ class ModListPageSkin extends SkinBase { } } + private static Task loadModIcon(LocalModFile modFile, int size) { + return Task.supplyAsync(() -> { + List 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 implements Comparable { private final BooleanProperty active; private final LocalModFile localModFile; @@ -286,22 +368,25 @@ class ModListPageSkin extends SkinBase { private final String message; private final ModTranslations.Mod mod; + private SoftReference 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 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 { 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 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 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 item : Arrays.asList( @@ -512,6 +565,7 @@ class ModListPageSkin extends SkinBase { final class ModInfoListCell extends MDListCell { 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 { 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 { 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 { @Override protected void updateControl(ModInfoObject dataItem, boolean empty) { if (empty) return; + + SoftReference 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 { 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); }