修复模组列表页面加载模组图标相关问题 (#4638)

1. 修复高分屏上模组图标模糊的问题;
2. 修复快速滚动时可能会显示其他模组的图标的问题;
3. 修复快速滚动时可能重复加载图标的问题。
This commit is contained in:
Glavo
2025-10-09 21:33:35 +08:00
committed by GitHub
parent c8edf4be62
commit 2e878e43cb
2 changed files with 76 additions and 67 deletions

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting; package org.jackhuang.hmcl.setting;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
public enum VersionIconType { public enum VersionIconType {
@@ -39,6 +40,18 @@ public enum VersionIconType {
// Please append new items at last // Please append new items at last
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
return switch (modLoaderType) {
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;
};
}
private final String resourceUrl; private final String resourceUrl;
VersionIconType(String resourceUrl) { VersionIconType(String resourceUrl) {
@@ -48,8 +61,4 @@ public enum VersionIconType {
public Image getIcon() { public Image getIcon() {
return FXUtils.newBuiltinImage(resourceUrl); return FXUtils.newBuiltinImage(resourceUrl);
} }
public Image getIcon(int size) {
return FXUtils.newBuiltinImage(resourceUrl, size, size, true, true);
}
} }

View File

@@ -22,6 +22,7 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition; import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
@@ -55,10 +56,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
@@ -67,10 +65,12 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -289,12 +289,34 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
} }
} }
private static Task<Image> loadModIcon(LocalModFile modFile, int size) { static final class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
return Task.supplyAsync(() -> { private final BooleanProperty active;
private final LocalModFile localModFile;
private final @Nullable ModTranslations.Mod modTranslations;
private SoftReference<CompletableFuture<Image>> iconCache;
ModInfoObject(LocalModFile localModFile) {
this.localModFile = localModFile;
this.active = localModFile.activeProperty();
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
}
LocalModFile getModInfo() {
return localModFile;
}
public @Nullable ModTranslations.Mod getModTranslations() {
return modTranslations;
}
@FXThread
private Image loadIcon() {
List<String> iconPaths = new ArrayList<>(); List<String> iconPaths = new ArrayList<>();
if (StringUtils.isNotBlank(modFile.getLogoPath())) { if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) {
iconPaths.add(modFile.getLogoPath()); iconPaths.add(this.localModFile.getLogoPath());
} }
iconPaths.addAll(List.of( iconPaths.addAll(List.of(
@@ -318,7 +340,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
"resources/mod_icon.png" "resources/mod_icon.png"
)); ));
String modId = modFile.getId(); String modId = this.localModFile.getId();
if (StringUtils.isNotBlank(modId)) { if (StringUtils.isNotBlank(modId)) {
iconPaths.addAll(List.of( iconPaths.addAll(List.of(
"assets/" + modId + "/icon.png", "assets/" + modId + "/icon.png",
@@ -337,14 +359,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
)); ));
} }
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.getFile())) { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
for (String path : iconPaths) { for (String path : iconPaths) {
Path iconPath = fs.getPath(path); Path iconPath = fs.getPath(path);
if (Files.exists(iconPath)) { if (Files.exists(iconPath)) {
Image image = FXUtils.loadImage(iconPath, size, size, true, true); Image image = FXUtils.loadImage(iconPath, 80, 80, true, true);
if (!image.isError() && if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
image.getWidth() > 0 &&
image.getHeight() > 0 &&
Math.abs(image.getWidth() - image.getHeight()) < 1) { Math.abs(image.getWidth() - image.getHeight()) < 1) {
return image; return image;
} }
@@ -354,40 +374,34 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
LOG.warning("Failed to load mod icons", e); LOG.warning("Failed to load mod icons", e);
} }
VersionIconType defaultIcon = switch (modFile.getModLoaderType()) { return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
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;
private final @Nullable ModTranslations.Mod modTranslations;
private SoftReference<Image> iconCache;
ModInfoObject(LocalModFile localModFile) {
this.localModFile = localModFile;
this.active = localModFile.activeProperty();
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
} }
LocalModFile getModInfo() { public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
return localModFile; SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
} CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
Image image = imageFuture.getNow(null);
if (image != null) {
imageView.setImage(image);
return;
}
} else {
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
this.iconCache = new SoftReference<>(imageFuture);
}
imageView.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
// The current ListCell has already switched to another object
return;
}
}
public @Nullable ModTranslations.Mod getModTranslations() { imageView.setImage(image);
return modTranslations; }, Schedulers.javafx());
} }
@Override @Override
@@ -397,7 +411,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
} }
} }
class ModInfoDialog extends JFXDialogLayout { final class ModInfoDialog extends JFXDialogLayout {
ModInfoDialog(ModInfoObject modInfo) { ModInfoDialog(ModInfoObject modInfo) {
HBox titleContainer = new HBox(); HBox titleContainer = new HBox();
@@ -408,10 +422,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
ImageView imageView = new ImageView(); ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 40, 40); FXUtils.limitSize(imageView, 40, 40);
loadModIcon(modInfo.getModInfo(), 40) modInfo.loadIcon(imageView, null);
.whenComplete(Schedulers.javafx(), (image, exception) -> {
imageView.setImage(image);
}).start();
TwoLineListItem title = new TwoLineListItem(); TwoLineListItem title = new TwoLineListItem();
if (modInfo.getModTranslations() != null && I18n.isUseChinese()) if (modInfo.getModTranslations() != null && I18n.isUseChinese())
@@ -584,7 +595,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
imageView.setFitWidth(24); imageView.setFitWidth(24);
imageView.setFitHeight(24); imageView.setFitHeight(24);
imageView.setPreserveRatio(true); imageView.setPreserveRatio(true);
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 24, 24, true, true)); imageView.setImage(VersionIconType.COMMAND.getIcon());
restoreButton.getStyleClass().add("toggle-icon4"); restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24), 24, 24)); restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24), 24, 24));
@@ -620,18 +631,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
LocalModFile modInfo = dataItem.getModInfo(); LocalModFile modInfo = dataItem.getModInfo();
ModTranslations.Mod modTranslations = dataItem.getModTranslations(); ModTranslations.Mod modTranslations = dataItem.getModTranslations();
SoftReference<Image> iconCache = dataItem.iconCache; dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
Image icon;
if (iconCache != null && (icon = iconCache.get()) != null) {
imageView.setImage(icon);
} else {
loadModIcon(modInfo, 24)
.whenComplete(Schedulers.javafx(), (image, exception) -> {
dataItem.iconCache = new SoftReference<>(image);
imageView.setImage(image);
}).start();
}
String displayName = modInfo.getName(); String displayName = modInfo.getName();
if (modTranslations != null && I18n.isUseChinese()) { if (modTranslations != null && I18n.isUseChinese()) {