@@ -48,4 +48,8 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import javafx.geometry.Insets;
|
|||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.SelectionMode;
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
import javafx.scene.image.Image;
|
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.mod.modrinth.ModrinthRemoteModRepository;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
|
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
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.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
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;
|
||||||
@@ -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.i18n.I18n.i18n;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
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 TransitionPane toolbarPane;
|
||||||
private final HBox searchBar;
|
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> {
|
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
|
||||||
private final BooleanProperty active;
|
private final BooleanProperty active;
|
||||||
private final LocalModFile localModFile;
|
private final LocalModFile localModFile;
|
||||||
@@ -286,22 +368,25 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
private final String message;
|
private final String message;
|
||||||
private final ModTranslations.Mod mod;
|
private final ModTranslations.Mod mod;
|
||||||
|
|
||||||
|
private SoftReference<Image> iconCache;
|
||||||
|
|
||||||
ModInfoObject(LocalModFile localModFile) {
|
ModInfoObject(LocalModFile localModFile) {
|
||||||
this.localModFile = localModFile;
|
this.localModFile = localModFile;
|
||||||
this.active = localModFile.activeProperty();
|
this.active = localModFile.activeProperty();
|
||||||
|
|
||||||
StringBuilder title = new StringBuilder(localModFile.getName());
|
this.title = 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();
|
|
||||||
|
|
||||||
|
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());
|
this.mod = ModTranslations.MOD.getModById(localModFile.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,77 +419,45 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
titleContainer.setSpacing(8);
|
titleContainer.setSpacing(8);
|
||||||
|
|
||||||
ImageView imageView = new ImageView();
|
ImageView imageView = new ImageView();
|
||||||
Task.supplyAsync(() -> {
|
FXUtils.limitSize(imageView, 40, 40);
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
|
loadModIcon(modInfo.getModInfo(), 40)
|
||||||
String logoPath = modInfo.getModInfo().getLogoPath();
|
.whenComplete(Schedulers.javafx(), (image, exception) -> {
|
||||||
if (StringUtils.isNotBlank(logoPath)) {
|
imageView.setImage(image);
|
||||||
Path iconPath = fs.getPath(logoPath);
|
}).start();
|
||||||
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();
|
|
||||||
|
|
||||||
TwoLineListItem title = new TwoLineListItem();
|
TwoLineListItem title = new TwoLineListItem();
|
||||||
title.setTitle(modInfo.getModInfo().getName());
|
title.setTitle(modInfo.getModInfo().getName());
|
||||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
if (modInfo.getMod() != null) {
|
||||||
title.addTag(modInfo.getModInfo().getVersion());
|
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);
|
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
|
||||||
setHeading(titleContainer);
|
setHeading(titleContainer);
|
||||||
|
|
||||||
Label description = new Label(modInfo.getModInfo().getDescription().toString());
|
Label description = new Label(modInfo.getModInfo().getDescription().toString());
|
||||||
|
description.setWrapText(true);
|
||||||
FXUtils.copyOnDoubleClick(description);
|
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())) {
|
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||||
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
|
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
|
||||||
@@ -512,6 +565,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
|
|
||||||
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||||
JFXCheckBox checkBox = new JFXCheckBox();
|
JFXCheckBox checkBox = new JFXCheckBox();
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
JFXButton restoreButton = new JFXButton();
|
JFXButton restoreButton = new JFXButton();
|
||||||
JFXButton infoButton = new JFXButton();
|
JFXButton infoButton = new JFXButton();
|
||||||
@@ -528,6 +582,11 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
content.setMouseTransparent(true);
|
content.setMouseTransparent(true);
|
||||||
setSelectable();
|
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.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));
|
||||||
|
|
||||||
@@ -539,7 +598,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
infoButton.getStyleClass().add("toggle-icon4");
|
infoButton.getStyleClass().add("toggle-icon4");
|
||||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(Theme.blackFill(), 24), 24, 24));
|
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));
|
StackPane.setMargin(container, new Insets(8));
|
||||||
getContainer().getChildren().setAll(container);
|
getContainer().getChildren().setAll(container);
|
||||||
@@ -548,6 +607,19 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
@Override
|
@Override
|
||||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||||
if (empty) return;
|
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.setTitle(dataItem.getTitle());
|
||||||
content.getTags().clear();
|
content.getTags().clear();
|
||||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||||
@@ -571,9 +643,14 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (dataItem.getMod() != null && I18n.isUseChinese()) {
|
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) {
|
if (booleanProperty != null) {
|
||||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user