Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.stream.Collectors;

public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";

Expand Down Expand Up @@ -158,6 +162,15 @@ public Label getSubtitleLabel() {
return lblSubtitle;
}

private static Label createTag(String tag, PseudoClass pseudoClass) {
var tagLabel = new Label(tag);
tagLabel.getStyleClass().add("tag");
tagLabel.setMinWidth(Label.USE_PREF_SIZE);
if (pseudoClass != null)
tagLabel.pseudoClassStateChanged(pseudoClass, true);
return tagLabel;
}

private ObservableList<Label> tags;

public ObservableList<Label> getTags() {
Expand All @@ -183,18 +196,24 @@ public ObservableList<Label> getTags() {
}

public void addTag(String tag, PseudoClass pseudoClass) {
var tagLabel = new Label(tag);
tagLabel.getStyleClass().add("tag");
tagLabel.setMinWidth(Label.USE_PREF_SIZE);
if (pseudoClass != null)
tagLabel.pseudoClassStateChanged(pseudoClass, true);
getTags().add(tagLabel);
getTags().add(createTag(tag, pseudoClass));
}

public void addTag(String tag) {
addTag(tag, null);
}

public void addTags(Collection<String> tags) {
getTags().addAll(tags.stream().map(tag -> createTag(tag, null)).toList());
}

public void addTagsIfNotExist(Collection<String> tags) {
var current = getTags().stream().map(Label::getText).collect(Collectors.toSet());
var target = new LinkedHashSet<>(tags);
target.removeAll(current);
addTags(target);
}

private static final PseudoClass WARNING_PSEUDO_CLASS = PseudoClass.getPseudoClass("warning");

public void addTagWarning(String tag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.addon.LoaderType;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
Expand Down Expand Up @@ -282,8 +283,9 @@ protected DownloadPageSkin(DownloadPage control) {
resolve:
for (RemoteAddon.Version modVersion : modVersions) {
if (getSkinnable().type == RemoteAddonRepository.Type.MOD) {
for (ModLoaderType loader : modVersion.loaders()) {
if (targetLoaders.contains(loader)) {
for (Either<LoaderType, String> loader : modVersion.loaders()) {
//noinspection SuspiciousMethodCalls
if (loader.left() instanceof ModLoaderType && targetLoaders.contains(loader.left())) {
list.getContent().addAll(
ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)),
new AddonItem(control.addon, modVersion, control)
Expand Down Expand Up @@ -452,28 +454,12 @@ private static final class AddonItem extends StackPane {
break;
}

for (ModLoaderType modLoaderType : dataItem.loaders()) {
switch (modLoaderType) {
case FORGE:
content.addTag(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.addTag(i18n("install.installer.cleanroom"));
break;
case NEO_FORGE:
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;
}
Set<String> tags = new LinkedHashSet<>();
for (Either<LoaderType, String> loader : dataItem.loaders()) {
String tag = I18n.translateLoaderType(loader);
if (tag != null) tags.add(tag);
}
content.addTags(tags);

descPane.getChildren().setAll(graphicPane, content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
Expand Down Expand Up @@ -477,25 +474,12 @@ final class ModInfoDialog extends JFXDialogLayout {
if (versionOptional.isPresent()) {
RemoteAddon remoteAddon = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().modid());
FXUtils.runInFX(() -> {
for (ModLoaderType modLoaderType : versionOptional.get().loaders()) {
String loaderName = switch (modLoaderType) {
case FORGE -> i18n("install.installer.forge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
case NEO_FORGE -> i18n("install.installer.neoforge");
case FABRIC -> i18n("install.installer.fabric");
case LITE_LOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
default -> null;
};
if (loaderName == null)
continue;
if (title.getTags()
.stream()
.noneMatch(it -> it.getText().equals(loaderName))) {
title.addTag(loaderName);
}
Set<String> tags = new LinkedHashSet<>();
for (Either<LoaderType, String> loader : versionOptional.get().loaders()) {
String tag = I18n.translateLoaderType(loader);
if (tag != null) tags.add(tag);
}
title.addTagsIfNotExist(tags);

button.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
Expand Down
25 changes: 25 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
*/
package org.jackhuang.hmcl.util.i18n;

import org.jackhuang.hmcl.addon.LoaderType;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.Either;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.translator.Translator;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -124,6 +128,27 @@ public static String getWikiLink(GameRemoteVersion remoteVersion) {
return MinecraftWiki.getWikiLink(locale, remoteVersion);
}

public static @Nullable String translateLoaderType(Either<LoaderType, String> loader) {
return loader.map(
loaderType -> {
if (loaderType instanceof ModLoaderType modLoaderType) {
return switch (modLoaderType) {
case FORGE -> i18n("install.installer.forge");
case CLEANROOM -> i18n("install.installer.cleanroom");
case NEO_FORGE -> i18n("install.installer.neoforge");
case FABRIC -> i18n("install.installer.fabric");
case LITE_LOADER -> i18n("install.installer.liteloader");
case QUILT -> i18n("install.installer.quilt");
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
default -> null;
};
}
return null;
},
s -> "bungeecord".equalsIgnoreCase(s) ? "BungeeCord" : StringUtils.capitalizeWords(s)
);
}

public static boolean hasKey(String key) {
return getResourceBundle().containsKey(key);
}
Expand Down
53 changes: 53 additions & 0 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LoaderType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.addon;

import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.util.Either;
import org.jackhuang.hmcl.util.StringUtils;

import java.util.Locale;
import java.util.Set;

/// For mods and shaders
public interface LoaderType {

Set<String> names();

static boolean mightBeLoader(String str) {
if (StringUtils.isBlank(str)
|| !StringUtils.isASCII(str)
|| "client".equalsIgnoreCase(str) || "server".equalsIgnoreCase(str))
return false;
int l = str.length();
for (int i = 0; i < l; i++) {
char c = str.charAt(i);
if (c != '-' && c != ' ' && c != '_' && !StringUtils.isAlphabetic(c)) return false;
}
return true;
}

static Either<LoaderType, String> toEither(String loader) {
String l = loader.toLowerCase(Locale.ROOT);
for (var m : ModLoaderType.values()) {
if (m.names().contains(l)) return Either.left(m);
}
return Either.right(loader);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.addon;

import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Either;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
Expand Down Expand Up @@ -204,7 +204,7 @@ public interface IVersion {

public record Version(IVersion self, String modid, String name, String version, String changelog,
Instant datePublished, VersionType versionType, File file, List<Dependency> dependencies,
List<String> gameVersions, List<ModLoaderType> loaders) {
List<String> gameVersions, List<Either<LoaderType, String>> loaders) {
}

public record File(Map<String, String> hashes, String url, String filename) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.util.Either;
import org.jackhuang.hmcl.util.io.FileUtils;

import java.io.IOException;
Expand Down Expand Up @@ -202,7 +203,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe
if (currentVersion.isEmpty()) return null;
List<RemoteAddon.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().modid())
.filter(version -> version.gameVersions().contains(gameVersion))
.filter(version -> version.loaders().contains(getModLoaderType()))
.filter(version -> version.loaders().contains(Either.left(getModLoaderType())))
Comment thread
ToobLac marked this conversation as resolved.
.filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0)
.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed())
.toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,29 @@
*/
package org.jackhuang.hmcl.addon.mod;

public enum ModLoaderType {
import org.jackhuang.hmcl.addon.LoaderType;

import java.util.Set;

public enum ModLoaderType implements LoaderType {
UNKNOWN,
FORGE,
CLEANROOM,
NEO_FORGE,
FABRIC,
QUILT,
LITE_LOADER,
LEGACY_FABRIC
FORGE("forge"),
CLEANROOM("cleanroom"),
NEO_FORGE("neoforge"),
FABRIC("fabric"),
QUILT("quilt"),
LITE_LOADER("liteloader"),
LEGACY_FABRIC("legacy-fabric");

private final Set<String> names;

ModLoaderType(String... names) {
this.names = Set.of(names);
}

@Override
public Set<String> names() {
return names;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@
package org.jackhuang.hmcl.addon.repository;

import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.addon.LoaderType;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
Expand Down Expand Up @@ -482,13 +479,7 @@ public RemoteAddon.Version toVersion() {
return RemoteAddon.Dependency.ofGeneral(RELATION_TYPE.get(dependency.relationType()), MODS, Integer.toString(dependency.modId()));
}).distinct().filter(Objects::nonNull).collect(Collectors.toList()),
gameVersions.stream().filter(GameVersionNumber::isKnown).toList(),
gameVersions.stream().flatMap(version -> {
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT);
else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGE);
else return Stream.empty();
}).collect(Collectors.toList())
gameVersions.stream().filter(LoaderType::mightBeLoader).map(LoaderType::toEither).toList()
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.addon.LoaderType;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
Expand Down Expand Up @@ -417,14 +417,7 @@ public Optional<RemoteAddon.Version> toVersion() {
return RemoteAddon.Dependency.ofGeneral(DEPENDENCY_TYPE.get(dependency.dependencyType), MODS, dependency.projectId);
}).filter(Objects::nonNull).collect(Collectors.toList()),
gameVersions,
loaders.stream().flatMap(loader -> {
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE);
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER);
else return Stream.empty();
}).collect(Collectors.toList())
loaders.stream().map(LoaderType::toEither).toList()
));
}
}
Expand Down
Loading