优化模组管理页面模组信息的展示方式 (#4621)

This commit is contained in:
Glavo
2025-10-09 15:21:28 +08:00
committed by GitHub
parent 08f6f973c0
commit 11c45e0ab8
11 changed files with 230 additions and 129 deletions

2
.gitignore vendored
View File

@@ -16,6 +16,8 @@ NVIDIA
minecraft-exported-crash-info* minecraft-exported-crash-info*
hmcl-exported-logs-* hmcl-exported-logs-*
/.java/ /.java/
/.local/
/.cache/
# gradle build # gradle build
/build/ /build/

View File

@@ -33,9 +33,8 @@ import org.jackhuang.hmcl.util.AggregatedObservableList;
public class TwoLineListItem extends VBox { public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item"; private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
public static Label createTagLabel(String tag) { private static Label createTagLabel(String tag) {
Label tagLabel = new Label(); Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag); tagLabel.setText(tag);
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0)); HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel; return tagLabel;
@@ -111,7 +110,15 @@ public class TwoLineListItem extends VBox {
} }
public void addTag(String tag) { public void addTag(String tag) {
getTags().add(createTagLabel(tag)); Label tagLabel = createTagLabel(tag);
tagLabel.getStyleClass().add("tag");
getTags().add(tagLabel);
}
public void addTagWarning(String tag) {
Label tagLabel = createTagLabel(tag);
tagLabel.getStyleClass().add("tag-warning");
getTags().add(tagLabel);
} }
public ObservableList<Label> getTags() { public ObservableList<Label> getTags() {

View File

@@ -712,10 +712,7 @@ public class TerracottaControllerPage extends StackPane {
TwoLineListItem item = new TwoLineListItem(); TwoLineListItem item = new TwoLineListItem();
item.setTitle(profile.getName()); item.setTitle(profile.getName());
item.setSubtitle(profile.getVendor()); item.setSubtitle(profile.getVendor());
item.getTags().setAll(TwoLineListItem.createTagLabel( item.addTag(i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)));
i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)))
);
pane.getChildren().add(item); pane.getChildren().add(item);
} }

View File

@@ -17,7 +17,6 @@
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@@ -27,6 +26,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
@@ -44,19 +44,22 @@ import java.io.UncheckedIOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.concurrent.locks.ReentrantLock;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware { public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false); private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private final ReentrantLock lock = new ReentrantLock();
private ModManager modManager; private ModManager modManager;
private LibraryAnalyzer libraryAnalyzer;
private Profile profile; private Profile profile;
private String versionId; private String instanceId;
private String gameVersion;
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
public ModListPage() { public ModListPage() {
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> { FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
@@ -83,34 +86,76 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
@Override @Override
public void loadVersion(Profile profile, String id) { public void loadVersion(Profile profile, String id) {
this.profile = profile; this.profile = profile;
this.versionId = id; this.instanceId = id;
HMCLGameRepository repository = profile.getRepository(); HMCLGameRepository repository = profile.getRepository();
Version resolved = repository.getResolvedPreservingPatchesVersion(id); Version resolved = repository.getResolvedPreservingPatchesVersion(id);
libraryAnalyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null)); this.gameVersion = repository.getGameVersion(resolved).orElse(null);
modded.set(libraryAnalyzer.hasModLoader()); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
modded.set(analyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id)); loadMods(profile.getRepository().getModManager(id));
} }
private CompletableFuture<?> loadMods(ModManager modManager) { private void loadMods(ModManager modManager) {
setLoading(true);
this.modManager = modManager; this.modManager = modManager;
return CompletableFuture.supplyAsync(() -> { CompletableFuture.supplyAsync(() -> {
lock.lock();
try { try {
synchronized (ModListPage.this) { modManager.refreshMods();
runInFX(() -> loadingProperty().set(true)); return modManager.getMods();
modManager.refreshMods();
return new ArrayList<>(modManager.getMods());
}
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} finally {
lock.unlock();
} }
}, Schedulers.defaultScheduler()).whenCompleteAsync((list, exception) -> { }, Schedulers.io()).whenCompleteAsync((list, exception) -> {
loadingProperty().set(false); updateSupportedLoaders(modManager);
if (exception == null)
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).sorted().collect(Collectors.toList())); if (exception == null) {
else getItems().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).toList());
getProperties().remove(ModListPage.class); } else {
}, Platform::runLater); LOG.warning("Failed to load mods", exception);
getItems().clear();
}
setLoading(false);
}, Schedulers.javafx());
}
private void updateSupportedLoaders(ModManager modManager) {
supportedLoaders.clear();
LibraryAnalyzer analyzer = modManager.getLibraryAnalyzer();
if (analyzer == null) {
Collections.addAll(supportedLoaders, ModLoaderType.values());
return;
}
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
if (type.isModLoader() && analyzer.has(type)) {
ModLoaderType modLoaderType = type.getModLoaderType();
if (modLoaderType != null) {
supportedLoaders.add(modLoaderType);
if (modLoaderType == ModLoaderType.CLEANROOM)
supportedLoaders.add(ModLoaderType.FORGE);
}
}
}
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
supportedLoaders.add(ModLoaderType.FORGE);
}
if (analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
supportedLoaders.add(ModLoaderType.FORGE);
supportedLoaders.add(ModLoaderType.NEO_FORGED);
}
} }
public void add() { public void add() {
@@ -148,7 +193,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}).start(); }).start();
} }
public void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) { void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
try { try {
modManager.removeMods(selectedItems.stream() modManager.removeMods(selectedItems.stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
@@ -160,14 +205,14 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
} }
} }
public void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) { void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
selectedItems.stream() selectedItems.stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo) .map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true)); .forEach(info -> info.setActive(true));
} }
public void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) { void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
selectedItems.stream() selectedItems.stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo) .map(ModListPageSkin.ModInfoObject::getModInfo)
@@ -175,13 +220,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
} }
public void openModFolder() { public void openModFolder() {
FXUtils.openFolder(profile.getRepository().getRunDirectory(versionId).resolve("mods")); FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
} }
public void checkUpdates() { public void checkUpdates() {
Runnable action = () -> Controllers.taskDialog(Task Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> { .composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(versionId); Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
if (gameVersion.isPresent()) { if (gameVersion.isPresent()) {
return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods()); return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods());
} }
@@ -199,7 +244,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
.withStagesHint(Collections.singletonList("mods.check_updates")), .withStagesHint(Collections.singletonList("mods.check_updates")),
i18n("update.checking"), TaskCancellationAction.NORMAL); i18n("update.checking"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(versionId)) { if (profile.getRepository().isModpack(instanceId)) {
Controllers.confirm( Controllers.confirm(
i18n("mods.update_modpack_mod.warning"), null, i18n("mods.update_modpack_mod.warning"), null,
MessageDialogPane.MessageType.WARNING, MessageDialogPane.MessageType.WARNING,
@@ -210,7 +255,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
} }
public void download() { public void download() {
Controllers.getDownloadPage().showModDownloads().selectVersion(versionId); Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId);
Controllers.navigate(Controllers.getDownloadPage()); Controllers.navigate(Controllers.getDownloadPage());
} }
@@ -239,7 +284,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
return this.profile; return this.profile;
} }
public String getVersionId() { public String getInstanceId() {
return this.versionId; return this.instanceId;
} }
} }

View File

@@ -22,14 +22,13 @@ 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.value.ChangeListener;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets; 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.*;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
@@ -74,14 +73,12 @@ import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
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;
@@ -98,6 +95,9 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
// FXThread // FXThread
private boolean isSearching = false; private boolean isSearching = false;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Boolean> holder;
ModListPageSkin(ModListPage skinnable) { ModListPageSkin(ModListPage skinnable) {
super(skinnable); super(skinnable);
@@ -109,6 +109,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
root.getStyleClass().add("no-padding"); root.getStyleClass().add("no-padding");
listView = new JFXListView<>(); listView = new JFXListView<>();
this.holder = FXUtils.onWeakChange(skinnable.loadingProperty(), loading -> {
if (!loading) {
listView.scrollTo(0);
}
});
{ {
toolbarPane = new TransitionPane(); toolbarPane = new TransitionPane();
@@ -276,7 +282,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|| predicate.test(modInfo.getGameVersion()) || predicate.test(modInfo.getGameVersion())
|| predicate.test(modInfo.getId()) || predicate.test(modInfo.getId())
|| predicate.test(Objects.toString(modInfo.getModLoaderType())) || predicate.test(Objects.toString(modInfo.getModLoaderType()))
|| predicate.test((item.getMod() != null ? item.getMod().getDisplayName() : null))) { || predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) {
listView.getItems().add(item); listView.getItems().add(item);
} }
} }
@@ -365,9 +371,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
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;
private final String title; private final @Nullable ModTranslations.Mod modTranslations;
private final String message;
private final ModTranslations.Mod mod;
private SoftReference<Image> iconCache; private SoftReference<Image> iconCache;
@@ -375,36 +379,15 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
this.localModFile = localModFile; this.localModFile = localModFile;
this.active = localModFile.activeProperty(); this.active = localModFile.activeProperty();
this.title = localModFile.getName(); this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), 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.getMod(localModFile.getId(), localModFile.getName());
}
String getTitle() {
return title;
}
String getSubtitle() {
return message;
} }
LocalModFile getModInfo() { LocalModFile getModInfo() {
return localModFile; return localModFile;
} }
public ModTranslations.Mod getMod() { public @Nullable ModTranslations.Mod getModTranslations() {
return mod; return modTranslations;
} }
@Override @Override
@@ -431,23 +414,23 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}).start(); }).start();
TwoLineListItem title = new TwoLineListItem(); TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName()); if (modInfo.getModTranslations() != null && I18n.isUseChinese())
if (modInfo.getMod() != null) { title.setTitle(modInfo.getModTranslations().getDisplayName());
title.addTag(modInfo.getMod().getDisplayName()); else
} title.setTitle(modInfo.getModInfo().getName());
List<String> subtitleParts = new ArrayList<>(); StringJoiner subtitle = new StringJoiner(" | ");
subtitleParts.add(FileUtils.getName(modInfo.getModInfo().getFile())); subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) { if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
subtitleParts.add(modInfo.getModInfo().getGameVersion()); subtitle.add(modInfo.getModInfo().getGameVersion());
} }
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) { if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
subtitleParts.add(modInfo.getModInfo().getVersion()); subtitle.add(modInfo.getModInfo().getVersion());
} }
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) { if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
subtitleParts.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors()); subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
} }
title.setSubtitle(String.join(", ", subtitleParts)); title.setSubtitle(subtitle.toString());
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title); titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
setHeading(titleContainer); setHeading(titleContainer);
@@ -517,7 +500,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Controllers.navigate(new DownloadPage( Controllers.navigate(new DownloadPage(
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
remoteMod, remoteMod,
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getVersionId()), new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
(profile, version, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods") (profile, version, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
)); ));
}); });
@@ -540,7 +523,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
getActions().add(officialPageButton); getActions().add(officialPageButton);
} }
if (modInfo.getMod() == null || StringUtils.isBlank(modInfo.getMod().getMcmod())) { if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search")); JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
searchButton.setOnAction(e -> { searchButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent()); fireEvent(new DialogCloseEvent());
@@ -555,7 +538,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page")); JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
mcmodButton.setOnAction(e -> { mcmodButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent()); fireEvent(new DialogCloseEvent());
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getMod())); FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
}); });
getActions().add(mcmodButton); getActions().add(mcmodButton);
} }
@@ -574,6 +557,8 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get())); private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
final class ModInfoListCell extends MDListCell<ModInfoObject> { final class ModInfoListCell extends MDListCell<ModInfoObject> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox(); JFXCheckBox checkBox = new JFXCheckBox();
ImageView imageView = new ImageView(); ImageView imageView = new ImageView();
TwoLineListItem content = new TwoLineListItem(); TwoLineListItem content = new TwoLineListItem();
@@ -582,9 +567,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
JFXButton revealButton = new JFXButton(); JFXButton revealButton = new JFXButton();
BooleanProperty booleanProperty; BooleanProperty booleanProperty;
Tooltip warningTooltip;
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) { ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
super(listView, lastCell); super(listView, lastCell);
this.getStyleClass().add("mod-info-list-cell");
HBox container = new HBox(8); HBox container = new HBox(8);
container.setPickOnBounds(false); container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT); container.setAlignment(Pos.CENTER_LEFT);
@@ -616,68 +605,104 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
@Override @Override
protected void updateControl(ModInfoObject dataItem, boolean empty) { protected void updateControl(ModInfoObject dataItem, boolean empty) {
pseudoClassStateChanged(WARNING, false);
if (warningTooltip != null) {
Tooltip.uninstall(this, warningTooltip);
warningTooltip = null;
}
if (empty) return; if (empty) return;
List<String> warning = new ArrayList<>();
content.getTags().clear();
LocalModFile modInfo = dataItem.getModInfo();
ModTranslations.Mod modTranslations = dataItem.getModTranslations();
SoftReference<Image> iconCache = dataItem.iconCache; SoftReference<Image> iconCache = dataItem.iconCache;
Image icon; Image icon;
if (iconCache != null && (icon = iconCache.get()) != null) { if (iconCache != null && (icon = iconCache.get()) != null) {
imageView.setImage(icon); imageView.setImage(icon);
} else { } else {
loadModIcon(dataItem.getModInfo(), 24) loadModIcon(modInfo, 24)
.whenComplete(Schedulers.javafx(), (image, exception) -> { .whenComplete(Schedulers.javafx(), (image, exception) -> {
dataItem.iconCache = new SoftReference<>(image); dataItem.iconCache = new SoftReference<>(image);
imageView.setImage(image); imageView.setImage(image);
}).start(); }).start();
} }
content.setTitle(dataItem.getTitle()); if (modTranslations != null && I18n.isUseChinese() && !modInfo.getName().equals(modTranslations.getName()))
content.getTags().clear(); content.setTitle(modInfo.getName() + " (" + modTranslations.getName() + ")");
switch (dataItem.getModInfo().getModLoaderType()) { else
case FORGE: content.setTitle(modInfo.getName());
content.addTag(i18n("install.installer.forge"));
break; StringJoiner joiner = new StringJoiner(" | ");
case CLEANROOM:
content.addTag(i18n("install.installer.cleanroom")); if (StringUtils.isNotBlank(modInfo.getId()))
break; joiner.add(modInfo.getId());
case NEO_FORGED:
content.addTag(i18n("install.installer.neoforge")); joiner.add(FileUtils.getName(modInfo.getFile()));
break;
case FABRIC: content.setSubtitle(joiner.toString());
content.addTag(i18n("install.installer.fabric"));
break; ModLoaderType modLoaderType = modInfo.getModLoaderType();
case LITE_LOADER: if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
content.addTag(i18n("install.installer.liteloader")); warning.add(i18n("mods.warning.loader_mismatch"));
break; switch (dataItem.getModInfo().getModLoaderType()) {
case QUILT: case FORGE:
content.addTag(i18n("install.installer.quilt")); content.addTagWarning(i18n("install.installer.forge"));
break; break;
} case CLEANROOM:
if (dataItem.getMod() != null && I18n.isUseChinese()) { content.addTagWarning(i18n("install.installer.cleanroom"));
if (isNotBlank(dataItem.getSubtitle())) { break;
content.setSubtitle(dataItem.getSubtitle() + ", " + dataItem.getMod().getDisplayName()); case NEO_FORGED:
} else { content.addTagWarning(i18n("install.installer.neoforge"));
content.setSubtitle(dataItem.getMod().getDisplayName()); break;
case FABRIC:
content.addTagWarning(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.addTagWarning(i18n("install.installer.liteloader"));
break;
case QUILT:
content.addTagWarning(i18n("install.installer.quilt"));
break;
} }
} else {
content.setSubtitle(dataItem.getSubtitle());
} }
String modVersion = modInfo.getVersion();
if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) {
content.addTag(modVersion);
}
if (booleanProperty != null) { if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty); checkBox.selectedProperty().unbindBidirectional(booleanProperty);
} }
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active); checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty()); restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
restoreButton.setOnAction(e -> { restoreButton.setOnAction(e -> {
menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream() menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream()
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(), .map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(),
() -> getSkinnable().rollback(dataItem.getModInfo(), localModFile), () -> getSkinnable().rollback(modInfo, localModFile),
popup.get())) popup.get()))
.collect(Collectors.toList()) .toList()
); );
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight()); popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
}); });
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(dataItem.getModInfo().getFile())); revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile()));
infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem))); infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem)));
if (!warning.isEmpty()) {
pseudoClassStateChanged(WARNING, true);
//noinspection ConstantValue
this.warningTooltip = warning.size() == 1
? new Tooltip(warning.get(0))
: new Tooltip(String.join("\n", warning));
FXUtils.installFastTooltip(this, warningTooltip);
}
} }
} }
} }

View File

@@ -139,8 +139,8 @@ public enum ModTranslations {
modIdMap = new HashMap<>(mods.size()); modIdMap = new HashMap<>(mods.size());
for (Mod mod : mods) { for (Mod mod : mods) {
for (String id : mod.getModIds()) { for (String id : mod.getModIds()) {
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) { if (StringUtils.isNotBlank(id)) {
modIdMap.put(id, mod); modIdMap.putIfAbsent(id, mod);
} }
} }
} }
@@ -164,7 +164,7 @@ public enum ModTranslations {
for (Mod mod : mods) { for (Mod mod : mods) {
String subname = cleanSubname(mod.getSubname()); String subname = cleanSubname(mod.getSubname());
if (StringUtils.isNotBlank(subname)) { if (StringUtils.isNotBlank(subname)) {
subnameMap.put(subname, mod); subnameMap.putIfAbsent(subname, mod);
} }
} }
@@ -186,7 +186,7 @@ public enum ModTranslations {
curseForgeMap = new HashMap<>(mods.size()); curseForgeMap = new HashMap<>(mods.size());
for (Mod mod : mods) { for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getCurseforge())) { if (StringUtils.isNotBlank(mod.getCurseforge())) {
curseForgeMap.put(mod.getCurseforge(), mod); curseForgeMap.putIfAbsent(mod.getCurseforge(), mod);
} }
} }

View File

@@ -293,6 +293,14 @@
-fx-font-size: 12px; -fx-font-size: 12px;
} }
.two-line-list-item > .first-line > .tag-warning {
-fx-text-fill: #d34336;;
-fx-background-color: #f1aeb5;
-fx-padding: 2;
-fx-font-weight: normal;
-fx-font-size: 12px;
}
.two-line-item-second-large { .two-line-item-second-large {
} }
@@ -918,6 +926,10 @@
-fx-background-color: derive(-fx-base-color, 60%); -fx-background-color: derive(-fx-base-color, 60%);
} }
.mod-info-list-cell:warning {
-fx-background-color: #F8D7DA;
}
.options-sublist { .options-sublist {
-fx-background-color: white; -fx-background-color: white;
} }

View File

@@ -1090,6 +1090,7 @@ mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Quilt, or
mods.restore=Restore mods.restore=Restore
mods.url=Official Page mods.url=Official Page
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update? mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
mods.warning.loader_mismatch=Mod loader mismatch
mods.install=Install mods.install=Install
mods.save_as=Save As mods.save_as=Save As

View File

@@ -887,6 +887,7 @@ mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge
mods.restore=回退 mods.restore=回退
mods.url=官方頁面 mods.url=官方頁面
mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎? mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎?
mods.warning.loader_mismatch=模組加載器不匹配
mods.install=安裝到目前實例 mods.install=安裝到目前實例
mods.save_as=下載到本機目錄 mods.save_as=下載到本機目錄

View File

@@ -897,6 +897,7 @@ mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge
mods.restore=回退 mods.restore=回退
mods.url=官方页面 mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗? mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
mods.warning.loader_mismatch=模组加载器不匹配
mods.install=安装到当前实例 mods.install=安装到当前实例
mods.save_as=下载到本地文件夹 mods.save_as=下载到本地文件夹

View File

@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.StringUtils;
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;
import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
@@ -62,7 +63,7 @@ public final class ModManager {
private final GameRepository repository; private final GameRepository repository;
private final String id; private final String id;
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>(); private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
private final HashMap<LocalMod, LocalMod> localMods = new HashMap<>(); private final HashMap<Pair<String, ModLoaderType>, LocalMod> localMods = new HashMap<>();
private LibraryAnalyzer analyzer; private LibraryAnalyzer analyzer;
private boolean loaded = false; private boolean loaded = false;
@@ -84,8 +85,17 @@ public final class ModManager {
return repository.getModsDirectory(id); return repository.getModsDirectory(id);
} }
public LocalMod getLocalMod(String id, ModLoaderType modLoaderType) { public LibraryAnalyzer getLibraryAnalyzer() {
return localMods.computeIfAbsent(new LocalMod(id, modLoaderType), x -> x); return analyzer;
}
public LocalMod getLocalMod(String modId, ModLoaderType modLoaderType) {
return localMods.computeIfAbsent(pair(modId, modLoaderType),
x -> new LocalMod(x.getKey(), x.getValue()));
}
public boolean hasMod(String modId, ModLoaderType modLoaderType) {
return localMods.containsKey(pair(modId, modLoaderType));
} }
private void addModInfo(Path file) { private void addModInfo(Path file) {
@@ -184,10 +194,10 @@ public final class ModManager {
loaded = true; loaded = true;
} }
public Collection<LocalModFile> getMods() throws IOException { public @Unmodifiable List<LocalModFile> getMods() throws IOException {
if (!loaded) if (!loaded)
refreshMods(); refreshMods();
return localModFiles; return List.copyOf(localModFiles);
} }
public void addMod(Path file) throws IOException { public void addMod(Path file) throws IOException {