优化模组管理页面模组信息的展示方式 (#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*
hmcl-exported-logs-*
/.java/
/.local/
/.cache/
# gradle build
/build/

View File

@@ -33,9 +33,8 @@ import org.jackhuang.hmcl.util.AggregatedObservableList;
public class TwoLineListItem extends VBox {
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();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
@@ -111,7 +110,15 @@ public class TwoLineListItem extends VBox {
}
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() {

View File

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

View File

@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
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.Version;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
@@ -44,19 +44,22 @@ import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.*;
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.i18n.I18n.i18n;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private final ReentrantLock lock = new ReentrantLock();
private ModManager modManager;
private LibraryAnalyzer libraryAnalyzer;
private Profile profile;
private String versionId;
private String instanceId;
private String gameVersion;
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
public ModListPage() {
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
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.versionId = id;
this.instanceId = id;
HMCLGameRepository repository = profile.getRepository();
Version resolved = repository.getResolvedPreservingPatchesVersion(id);
libraryAnalyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null));
modded.set(libraryAnalyzer.hasModLoader());
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
modded.set(analyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
}
private CompletableFuture<?> loadMods(ModManager modManager) {
private void loadMods(ModManager modManager) {
setLoading(true);
this.modManager = modManager;
return CompletableFuture.supplyAsync(() -> {
CompletableFuture.supplyAsync(() -> {
lock.lock();
try {
synchronized (ModListPage.this) {
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
return new ArrayList<>(modManager.getMods());
}
modManager.refreshMods();
return modManager.getMods();
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
lock.unlock();
}
}, Schedulers.defaultScheduler()).whenCompleteAsync((list, exception) -> {
loadingProperty().set(false);
if (exception == null)
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).sorted().collect(Collectors.toList()));
else
getProperties().remove(ModListPage.class);
}, Platform::runLater);
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
updateSupportedLoaders(modManager);
if (exception == null) {
getItems().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).toList());
} else {
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() {
@@ -148,7 +193,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}).start();
}
public void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()
.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()
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true));
}
public void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
selectedItems.stream()
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
@@ -175,13 +220,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void openModFolder() {
FXUtils.openFolder(profile.getRepository().getRunDirectory(versionId).resolve("mods"));
FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
}
public void checkUpdates() {
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(versionId);
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
if (gameVersion.isPresent()) {
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")),
i18n("update.checking"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(versionId)) {
if (profile.getRepository().isModpack(instanceId)) {
Controllers.confirm(
i18n("mods.update_modpack_mod.warning"), null,
MessageDialogPane.MessageType.WARNING,
@@ -210,7 +255,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void download() {
Controllers.getDownloadPage().showModDownloads().selectVersion(versionId);
Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId);
Controllers.navigate(Controllers.getDownloadPage());
}
@@ -239,7 +284,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
return this.profile;
}
public String getVersionId() {
return this.versionId;
public String getInstanceId() {
return this.instanceId;
}
}

View File

@@ -22,14 +22,13 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
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.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
@@ -74,14 +73,12 @@ import java.nio.file.Path;
import java.util.*;
import java.util.function.Predicate;
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.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.Lang.mapOf;
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.logging.Logger.LOG;
@@ -98,6 +95,9 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
// FXThread
private boolean isSearching = false;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Boolean> holder;
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
@@ -109,6 +109,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
this.holder = FXUtils.onWeakChange(skinnable.loadingProperty(), loading -> {
if (!loading) {
listView.scrollTo(0);
}
});
{
toolbarPane = new TransitionPane();
@@ -276,7 +282,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|| predicate.test(modInfo.getGameVersion())
|| predicate.test(modInfo.getId())
|| 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);
}
}
@@ -365,9 +371,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
private final BooleanProperty active;
private final LocalModFile localModFile;
private final String title;
private final String message;
private final ModTranslations.Mod mod;
private final @Nullable ModTranslations.Mod modTranslations;
private SoftReference<Image> iconCache;
@@ -375,36 +379,15 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
this.localModFile = localModFile;
this.active = localModFile.activeProperty();
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.getMod(localModFile.getId(), localModFile.getName());
}
String getTitle() {
return title;
}
String getSubtitle() {
return message;
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
}
LocalModFile getModInfo() {
return localModFile;
}
public ModTranslations.Mod getMod() {
return mod;
public @Nullable ModTranslations.Mod getModTranslations() {
return modTranslations;
}
@Override
@@ -431,23 +414,23 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
}).start();
TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName());
if (modInfo.getMod() != null) {
title.addTag(modInfo.getMod().getDisplayName());
}
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
title.setTitle(modInfo.getModTranslations().getDisplayName());
else
title.setTitle(modInfo.getModInfo().getName());
List<String> subtitleParts = new ArrayList<>();
subtitleParts.add(FileUtils.getName(modInfo.getModInfo().getFile()));
StringJoiner subtitle = new StringJoiner(" | ");
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
subtitleParts.add(modInfo.getModInfo().getGameVersion());
subtitle.add(modInfo.getModInfo().getGameVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
subtitleParts.add(modInfo.getModInfo().getVersion());
subtitle.add(modInfo.getModInfo().getVersion());
}
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);
setHeading(titleContainer);
@@ -517,7 +500,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Controllers.navigate(new DownloadPage(
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
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")
));
});
@@ -540,7 +523,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
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"));
searchButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
@@ -555,7 +538,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
mcmodButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getMod()));
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
});
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()));
final class ModInfoListCell extends MDListCell<ModInfoObject> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageView imageView = new ImageView();
TwoLineListItem content = new TwoLineListItem();
@@ -582,9 +567,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
JFXButton revealButton = new JFXButton();
BooleanProperty booleanProperty;
Tooltip warningTooltip;
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
super(listView, lastCell);
this.getStyleClass().add("mod-info-list-cell");
HBox container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
@@ -616,68 +605,104 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
@Override
protected void updateControl(ModInfoObject dataItem, boolean empty) {
pseudoClassStateChanged(WARNING, false);
if (warningTooltip != null) {
Tooltip.uninstall(this, warningTooltip);
warningTooltip = null;
}
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;
Image icon;
if (iconCache != null && (icon = iconCache.get()) != null) {
imageView.setImage(icon);
} else {
loadModIcon(dataItem.getModInfo(), 24)
loadModIcon(modInfo, 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()) {
case FORGE:
content.addTag(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.addTag(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.addTag(i18n("install.installer.neoforge"));
break;
case FABRIC:
content.addTag(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.addTag(i18n("install.installer.liteloader"));
break;
case QUILT:
content.addTag(i18n("install.installer.quilt"));
break;
}
if (dataItem.getMod() != null && I18n.isUseChinese()) {
if (isNotBlank(dataItem.getSubtitle())) {
content.setSubtitle(dataItem.getSubtitle() + ", " + dataItem.getMod().getDisplayName());
} else {
content.setSubtitle(dataItem.getMod().getDisplayName());
if (modTranslations != null && I18n.isUseChinese() && !modInfo.getName().equals(modTranslations.getName()))
content.setTitle(modInfo.getName() + " (" + modTranslations.getName() + ")");
else
content.setTitle(modInfo.getName());
StringJoiner joiner = new StringJoiner(" | ");
if (StringUtils.isNotBlank(modInfo.getId()))
joiner.add(modInfo.getId());
joiner.add(FileUtils.getName(modInfo.getFile()));
content.setSubtitle(joiner.toString());
ModLoaderType modLoaderType = modInfo.getModLoaderType();
if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
warning.add(i18n("mods.warning.loader_mismatch"));
switch (dataItem.getModInfo().getModLoaderType()) {
case FORGE:
content.addTagWarning(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.addTagWarning(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.addTagWarning(i18n("install.installer.neoforge"));
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) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty());
restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
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(),
() -> getSkinnable().rollback(dataItem.getModInfo(), localModFile),
() -> getSkinnable().rollback(modInfo, localModFile),
popup.get()))
.collect(Collectors.toList())
.toList()
);
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)));
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());
for (Mod mod : mods) {
for (String id : mod.getModIds()) {
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
modIdMap.put(id, mod);
if (StringUtils.isNotBlank(id)) {
modIdMap.putIfAbsent(id, mod);
}
}
}
@@ -164,7 +164,7 @@ public enum ModTranslations {
for (Mod mod : mods) {
String subname = cleanSubname(mod.getSubname());
if (StringUtils.isNotBlank(subname)) {
subnameMap.put(subname, mod);
subnameMap.putIfAbsent(subname, mod);
}
}
@@ -186,7 +186,7 @@ public enum ModTranslations {
curseForgeMap = new HashMap<>(mods.size());
for (Mod mod : mods) {
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;
}
.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 {
}
@@ -918,6 +926,10 @@
-fx-background-color: derive(-fx-base-color, 60%);
}
.mod-info-list-cell:warning {
-fx-background-color: #F8D7DA;
}
.options-sublist {
-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.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.warning.loader_mismatch=Mod loader mismatch
mods.install=Install
mods.save_as=Save As

View File

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

View File

@@ -897,6 +897,7 @@ mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge
mods.restore=回退
mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
mods.warning.loader_mismatch=模组加载器不匹配
mods.install=安装到当前实例
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.FileUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.file.*;
@@ -62,7 +63,7 @@ public final class ModManager {
private final GameRepository repository;
private final String id;
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 boolean loaded = false;
@@ -84,8 +85,17 @@ public final class ModManager {
return repository.getModsDirectory(id);
}
public LocalMod getLocalMod(String id, ModLoaderType modLoaderType) {
return localMods.computeIfAbsent(new LocalMod(id, modLoaderType), x -> x);
public LibraryAnalyzer getLibraryAnalyzer() {
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) {
@@ -184,10 +194,10 @@ public final class ModManager {
loaded = true;
}
public Collection<LocalModFile> getMods() throws IOException {
public @Unmodifiable List<LocalModFile> getMods() throws IOException {
if (!loaded)
refreshMods();
return localModFiles;
return List.copyOf(localModFiles);
}
public void addMod(Path file) throws IOException {