From 652491ca20c2a4336e21c08aa890fd24a894ce62 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 14 Dec 2025 10:32:50 +0800 Subject: [PATCH 01/61] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=8C=85=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E8=BE=B9=E6=A1=86?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java index acd07b1d58..be212d7474 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java @@ -216,7 +216,6 @@ public ResourcepackListCell(JFXListView listView, Holder this.page = page; BorderPane root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); root.setPadding(new Insets(8)); HBox left = new HBox(8); From 2bc3fa79b30703f91a1a3ed7121685515a9a8e53 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 14 Dec 2025 12:22:11 +0800 Subject: [PATCH 02/61] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20#4680?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/download/DownloadPage.java | 13 +++++-- .../hmcl/ui/versions/DownloadPage.java | 27 ++++++++++--- .../hmcl/ui/versions/ModListPageSkin.java | 2 +- .../org/jackhuang/hmcl/mod/RemoteMod.java | 38 ++++++++++++++++++- .../hmcl/mod/RemoteModRepository.java | 1 + .../jackhuang/hmcl/mod/curse/CurseAddon.java | 5 ++- .../curse/CurseForgeRemoteModRepository.java | 10 +++-- .../modrinth/ModrinthRemoteModRepository.java | 28 ++++++++++++-- 8 files changed, 103 insertions(+), 21 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 5d071d4531..8ae54b3b90 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -63,6 +63,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage { + public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_MOD = + (profile, version, file) -> download(profile, version, file, "mods"); + public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_RESOURCE_PACK = + (profile, version, file) -> download(profile, version, file, "resourcepacks"); + public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_SHADER = + (profile, version, file) -> download(profile, version, file, "shaderpacks"); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1)); private final TabHeader tab; private final TabHeader.Tab newGameTab = new TabHeader.Tab<>("newGameTab"); @@ -94,9 +101,9 @@ public DownloadPage(String uploadVersion) { page.getActions().add(installLocalModpackButton); return page; })); - modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true))); - resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true))); - shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, (profile, version, file) -> download(profile, version, file, "shaderpacks"), true))); + modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(FOR_MOD, true))); + resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(FOR_RESOURCE_PACK, true))); + shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, FOR_SHADER, true))); worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS))); tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index db518f3896..e2429b3a9c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -73,14 +73,20 @@ public class DownloadPage extends Control implements DecoratorPage { private final Profile.ProfileVersion version; private final DownloadCallback callback; private final DownloadListPage page; + private final RemoteModRepository.Type type; private SimpleMultimap> versions; public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { + this(page, addon, version, callback, null); + } + + public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback, @Nullable RemoteModRepository.Type type) { this.page = page; this.repository = page.repository; this.addon = addon; - this.translations = ModTranslations.getTranslationsByRepositoryType(repository.getType()); + this.type = Objects.requireNonNullElse(type, repository.getType()); + this.translations = ModTranslations.getTranslationsByRepositoryType(this.type); this.mod = translations.getModByCurseForgeId(addon.getSlug()); this.version = version; this.callback = callback; @@ -335,7 +341,7 @@ private static final class DependencyModItem extends StackPane { Pair.pair(RemoteMod.DependencyType.BROKEN, "mods.dependency.broken") )); - DependencyModItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, DownloadCallback callback) { + DependencyModItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version) { HBox pane = new HBox(8); pane.setPadding(new Insets(0, 8, 0, 8)); pane.setAlignment(Pos.CENTER_LEFT); @@ -346,15 +352,24 @@ private static final class DependencyModItem extends StackPane { imageView.setFitHeight(40); pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content); + RemoteModRepository.Type type = addon.getProjectType().getRepositoryType(); + + DownloadCallback callback = switch (type) { + case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD; + case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK; + case SHADER -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; + default -> null; + }; + RipplerContainer container = new RipplerContainer(pane); FXUtils.onClicked(container, () -> { fireEvent(new DialogCloseEvent()); - Controllers.navigate(new DownloadPage(page, addon, version, callback)); + Controllers.navigate(new DownloadPage(page, addon, version, callback, type)); }); getChildren().setAll(container); if (addon != RemoteMod.BROKEN) { - ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug()); + ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(type).getModByCurseForgeId(addon.getSlug()); content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.getTitle()); content.setSubtitle(addon.getDescription()); addon.getCategories().stream() @@ -445,7 +460,7 @@ private static final class ModItem extends StackPane { private static final class ModVersion extends JFXDialogLayout { public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { - RemoteModRepository.Type type = selfPage.repository.getType(); + RemoteModRepository.Type type = selfPage.type; String title; switch (type) { @@ -538,7 +553,7 @@ private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, list.add(title); dependencies.put(dependency.getType(), list); } - DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); + DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version); dependencies.get(dependency.getType()).add(dependencyModItem); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index 18b7a068d3..33e92c9031 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -504,7 +504,7 @@ final class ModInfoDialog extends JFXDialogLayout { repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), remoteMod, 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") + org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD )); }); button.setDisable(false); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index a936f887be..239407e94c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -40,7 +40,7 @@ public List loadDependencies(RemoteModRepository modRepository) throw public Stream loadVersions(RemoteModRepository modRepository) throws IOException { throw new IOException(); } - }); + }, ProjectType.MOD); private final String slug; private final String author; @@ -50,8 +50,9 @@ public Stream loadVersions(RemoteModRepository modRepository) private final String pageUrl; private final String iconUrl; private final IMod data; + private final ProjectType projectType; - public RemoteMod(String slug, String author, String title, String description, List categories, String pageUrl, String iconUrl, IMod data) { + public RemoteMod(String slug, String author, String title, String description, List categories, String pageUrl, String iconUrl, IMod data, ProjectType projectType) { this.slug = slug; this.author = author; this.title = title; @@ -60,6 +61,7 @@ public RemoteMod(String slug, String author, String title, String description, L this.pageUrl = pageUrl; this.iconUrl = iconUrl; this.data = data; + this.projectType = projectType; } public String getSlug() { @@ -94,6 +96,38 @@ public IMod getData() { return data; } + public ProjectType getProjectType() { + return projectType; + } + + public enum ProjectType { + MOD(RemoteModRepository.Type.MOD), + MODPACK(RemoteModRepository.Type.MODPACK), + RESOURCE_PACK(RemoteModRepository.Type.RESOURCE_PACK), + SHADER(RemoteModRepository.Type.SHADER), + WORLD(RemoteModRepository.Type.WORLD); + + public static ProjectType getByRepositoryType(RemoteModRepository.Type repositoryType) { + return switch (repositoryType) { + case WORLD -> WORLD; + case SHADER -> SHADER; + case RESOURCE_PACK -> RESOURCE_PACK; + case MODPACK -> MODPACK; + default -> MOD; + }; + } + + final RemoteModRepository.Type repositoryType; + + ProjectType(RemoteModRepository.Type repositoryType) { + this.repositoryType = repositoryType; + } + + public RemoteModRepository.Type getRepositoryType() { + return repositoryType; + } + } + public enum VersionType { Release, Beta, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 5f74ba7d49..af6d8ad2ba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -33,6 +33,7 @@ enum Type { MODPACK, RESOURCE_PACK, WORLD, + SHADER, CUSTOMIZATION } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index e77fb259e1..5c93d7d2b7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -209,7 +209,7 @@ public Stream loadVersions(RemoteModRepository modRepository) return modRepository.getRemoteVersionsById(Integer.toString(id)); } - public RemoteMod toMod() { + public RemoteMod toMod(RemoteMod.ProjectType type) { String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse(""); return new RemoteMod( @@ -220,7 +220,8 @@ public RemoteMod toMod() { categories.stream().map(category -> Integer.toString(category.getId())).collect(Collectors.toList()), links.websiteUrl, iconUrl, - this + this, + type ); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 4b518c061d..7b842553ee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -62,10 +62,12 @@ public static boolean isAvailable() { private final Type type; private final int section; + private final RemoteMod.ProjectType projectType; public CurseForgeRemoteModRepository(Type type, int section) { this.type = type; this.section = section; + this.projectType = RemoteMod.ProjectType.getByRepositoryType(type); } @Override @@ -126,7 +128,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion pair("pageSize", Integer.toString(pageSize))))))) .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); if (searchFilter.isEmpty()) { - return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); + return new SearchResult(response.getData().stream().map(addon -> addon.toMod(projectType)), calculateTotalPages(response, pageSize)); } // https://github.com/HMCL-dev/HMCL/issues/1549 @@ -138,7 +140,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); - return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> { + return new SearchResult(response.getData().stream().map(addon -> addon.toMod(projectType)).map(remoteMod -> { String lowerCaseResult = remoteMod.getTitle().toLowerCase(Locale.ROOT); int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult); @@ -149,7 +151,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } return pair(remoteMod, diff); - }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize)); + }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(addon -> addon.toMod(projectType)), calculateTotalPages(response, pageSize)); } @Override @@ -188,7 +190,7 @@ public Optional getRemoteVersionByLocalFile(LocalModFile loca public RemoteMod getModById(String id) throws IOException { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) .getJson(Response.typeOf(CurseAddon.class)); - return response.data.toMod(); + return response.data.toMod(projectType); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 4b60bf3cdd..5631021da2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -53,13 +53,21 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { private final String projectType; + private final RemoteModRepository.Type type; + private ModrinthRemoteModRepository(String projectType) { this.projectType = projectType; + this.type = switch (projectType) { + case "modpack" -> Type.MODPACK; + case "resourcepack" -> Type.RESOURCE_PACK; + case "shader" -> Type.SHADER; + default -> Type.MOD; + }; } @Override public Type getType() { - return Type.MOD; + return this.type; } private static String convertSortType(SortType sortType) { @@ -307,6 +315,12 @@ public Stream loadVersions(RemoteModRepository modRepository) } public RemoteMod toMod() { + RemoteMod.ProjectType type = switch (projectType) { + case "modpack" -> RemoteMod.ProjectType.MODPACK; + case "resourcepack" -> RemoteMod.ProjectType.RESOURCE_PACK; + case "shader" -> RemoteMod.ProjectType.SHADER; + default -> RemoteMod.ProjectType.MOD; + }; return new RemoteMod( slug, "", @@ -315,7 +329,8 @@ public RemoteMod toMod() { categories, String.format("https://modrinth.com/%s/%s", projectType, id), iconUrl, - this + this, + type ); } } @@ -686,6 +701,12 @@ public Stream loadVersions(RemoteModRepository modRepository) } public RemoteMod toMod() { + RemoteMod.ProjectType type = switch (projectType) { + case "modpack" -> RemoteMod.ProjectType.MODPACK; + case "resourcepack" -> RemoteMod.ProjectType.RESOURCE_PACK; + case "shader" -> RemoteMod.ProjectType.SHADER; + default -> RemoteMod.ProjectType.MOD; + }; return new RemoteMod( slug, author, @@ -694,7 +715,8 @@ public RemoteMod toMod() { categories, String.format("https://modrinth.com/%s/%s", projectType, projectId), iconUrl, - this + this, + type ); } } From c5406e3d8af7bf80feec5c34ca9f59d84320d191 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 14 Dec 2025 12:27:51 +0800 Subject: [PATCH 03/61] update --- .../hmcl/ui/versions/DownloadPage.java | 2 +- .../org/jackhuang/hmcl/mod/RemoteMod.java | 40 +++---------------- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 2 +- .../curse/CurseForgeRemoteModRepository.java | 10 ++--- .../modrinth/ModrinthRemoteModRepository.java | 20 +++++----- 5 files changed, 22 insertions(+), 52 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index e2429b3a9c..defa7ea719 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -352,7 +352,7 @@ private static final class DependencyModItem extends StackPane { imageView.setFitHeight(40); pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content); - RemoteModRepository.Type type = addon.getProjectType().getRepositoryType(); + RemoteModRepository.Type type = addon.getRepositoryType(); DownloadCallback callback = switch (type) { case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index 239407e94c..fdbc0724dc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -40,7 +40,7 @@ public List loadDependencies(RemoteModRepository modRepository) throw public Stream loadVersions(RemoteModRepository modRepository) throws IOException { throw new IOException(); } - }, ProjectType.MOD); + }, RemoteModRepository.Type.MOD); private final String slug; private final String author; @@ -50,9 +50,9 @@ public Stream loadVersions(RemoteModRepository modRepository) private final String pageUrl; private final String iconUrl; private final IMod data; - private final ProjectType projectType; + private final RemoteModRepository.Type repoType; - public RemoteMod(String slug, String author, String title, String description, List categories, String pageUrl, String iconUrl, IMod data, ProjectType projectType) { + public RemoteMod(String slug, String author, String title, String description, List categories, String pageUrl, String iconUrl, IMod data, RemoteModRepository.Type repoType) { this.slug = slug; this.author = author; this.title = title; @@ -61,7 +61,7 @@ public RemoteMod(String slug, String author, String title, String description, L this.pageUrl = pageUrl; this.iconUrl = iconUrl; this.data = data; - this.projectType = projectType; + this.repoType = repoType; } public String getSlug() { @@ -96,36 +96,8 @@ public IMod getData() { return data; } - public ProjectType getProjectType() { - return projectType; - } - - public enum ProjectType { - MOD(RemoteModRepository.Type.MOD), - MODPACK(RemoteModRepository.Type.MODPACK), - RESOURCE_PACK(RemoteModRepository.Type.RESOURCE_PACK), - SHADER(RemoteModRepository.Type.SHADER), - WORLD(RemoteModRepository.Type.WORLD); - - public static ProjectType getByRepositoryType(RemoteModRepository.Type repositoryType) { - return switch (repositoryType) { - case WORLD -> WORLD; - case SHADER -> SHADER; - case RESOURCE_PACK -> RESOURCE_PACK; - case MODPACK -> MODPACK; - default -> MOD; - }; - } - - final RemoteModRepository.Type repositoryType; - - ProjectType(RemoteModRepository.Type repositoryType) { - this.repositoryType = repositoryType; - } - - public RemoteModRepository.Type getRepositoryType() { - return repositoryType; - } + public RemoteModRepository.Type getRepositoryType() { + return repoType; } public enum VersionType { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 5c93d7d2b7..6d6c44ebfe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -209,7 +209,7 @@ public Stream loadVersions(RemoteModRepository modRepository) return modRepository.getRemoteVersionsById(Integer.toString(id)); } - public RemoteMod toMod(RemoteMod.ProjectType type) { + public RemoteMod toMod(RemoteModRepository.Type type) { String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse(""); return new RemoteMod( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 7b842553ee..7bbaa76d0f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -62,12 +62,10 @@ public static boolean isAvailable() { private final Type type; private final int section; - private final RemoteMod.ProjectType projectType; public CurseForgeRemoteModRepository(Type type, int section) { this.type = type; this.section = section; - this.projectType = RemoteMod.ProjectType.getByRepositoryType(type); } @Override @@ -128,7 +126,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion pair("pageSize", Integer.toString(pageSize))))))) .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); if (searchFilter.isEmpty()) { - return new SearchResult(response.getData().stream().map(addon -> addon.toMod(projectType)), calculateTotalPages(response, pageSize)); + return new SearchResult(response.getData().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); } // https://github.com/HMCL-dev/HMCL/issues/1549 @@ -140,7 +138,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); - return new SearchResult(response.getData().stream().map(addon -> addon.toMod(projectType)).map(remoteMod -> { + return new SearchResult(response.getData().stream().map(addon -> addon.toMod(type)).map(remoteMod -> { String lowerCaseResult = remoteMod.getTitle().toLowerCase(Locale.ROOT); int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult); @@ -151,7 +149,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } return pair(remoteMod, diff); - }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(addon -> addon.toMod(projectType)), calculateTotalPages(response, pageSize)); + }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); } @Override @@ -190,7 +188,7 @@ public Optional getRemoteVersionByLocalFile(LocalModFile loca public RemoteMod getModById(String id) throws IOException { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) .getJson(Response.typeOf(CurseAddon.class)); - return response.data.toMod(projectType); + return response.data.toMod(type); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 5631021da2..574cf17b11 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -315,11 +315,11 @@ public Stream loadVersions(RemoteModRepository modRepository) } public RemoteMod toMod() { - RemoteMod.ProjectType type = switch (projectType) { - case "modpack" -> RemoteMod.ProjectType.MODPACK; - case "resourcepack" -> RemoteMod.ProjectType.RESOURCE_PACK; - case "shader" -> RemoteMod.ProjectType.SHADER; - default -> RemoteMod.ProjectType.MOD; + RemoteModRepository.Type type = switch (projectType) { + case "modpack" -> RemoteModRepository.Type.MODPACK; + case "resourcepack" -> RemoteModRepository.Type.RESOURCE_PACK; + case "shader" -> RemoteModRepository.Type.SHADER; + default -> RemoteModRepository.Type.MOD; }; return new RemoteMod( slug, @@ -701,11 +701,11 @@ public Stream loadVersions(RemoteModRepository modRepository) } public RemoteMod toMod() { - RemoteMod.ProjectType type = switch (projectType) { - case "modpack" -> RemoteMod.ProjectType.MODPACK; - case "resourcepack" -> RemoteMod.ProjectType.RESOURCE_PACK; - case "shader" -> RemoteMod.ProjectType.SHADER; - default -> RemoteMod.ProjectType.MOD; + RemoteModRepository.Type type = switch (projectType) { + case "modpack" -> RemoteModRepository.Type.MODPACK; + case "resourcepack" -> RemoteModRepository.Type.RESOURCE_PACK; + case "shader" -> RemoteModRepository.Type.SHADER; + default -> RemoteModRepository.Type.MOD; }; return new RemoteMod( slug, From facdd47078298f7305f2fe72566589d0c5d88907 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 14 Dec 2025 12:32:00 +0800 Subject: [PATCH 04/61] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=89=E5=BD=B1?= =?UTF-8?q?=E5=8C=85=E7=95=8C=E9=9D=A2=E4=B8=8B=E8=BD=BD=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/DownloadPage.java | 23 ++++++------------- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index defa7ea719..9f582aec3c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -462,22 +462,13 @@ private static final class ModVersion extends JFXDialogLayout { public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { RemoteModRepository.Type type = selfPage.type; - String title; - switch (type) { - case WORLD: - title = "world.download.title"; - break; - case MODPACK: - title = "modpack.download.title"; - break; - case RESOURCE_PACK: - title = "resourcepack.download.title"; - break; - case MOD: - default: - title = "mods.download.title"; - break; - } + String title = switch (type) { + case WORLD -> "world.download.title"; + case MODPACK -> "modpack.download.title"; + case RESOURCE_PACK -> "resourcepack.download.title"; + case SHADER -> "download.shader.title"; + default -> "mods.download.title"; + }; this.setHeading(new HBox(new Label(i18n(title, version.getName())))); VBox box = new VBox(8); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 5fabd687ec..3e0bd28f97 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -337,6 +337,7 @@ download.hint=Install games and modpacks or download mods, resource packs, shade download.code.404=File "%s" not found on the remote server. download.content=Addons download.shader=Shaders +download.shader.title=Download Shader - %1s download.curseforge.unavailable=This HMCL build does not support access to CurseForge. Please use the official build to access CurseForge. download.existing=The file cannot be saved because it already exists. You can click "Save As" to save the file elsewhere. download.external_link=Visit Download Website diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 6ccc3dd27b..43fa0a3b43 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -339,6 +339,7 @@ download.hint=安裝遊戲和模組包或下載模組、資源包、光影和世 download.code.404=遠端伺服器沒有需要下載的檔案:%s download.content=遊戲內容 download.shader=光影 +download.shader.title=下載光影 - %1s download.curseforge.unavailable=這個 HMCL 版本不支援訪問 CurseForge。請使用官方版本進行下載。 download.existing=檔案已存在,無法儲存。你可以將檔案儲存至其他地方。 download.external_link=開啟下載網站 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 0e2e57588f..04662a4da2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -347,6 +347,7 @@ download.hint=安装游戏和整合包或下载模组、资源包、光影和世 download.code.404=远程服务器不包含需要下载的文件: %s\n你可以点击右上角帮助按钮进行求助。 download.content=游戏内容 download.shader=光影 +download.shader.title=下载光影 - %1s download.curseforge.unavailable=此 HMCL 版本不支持访问 CurseForge。请使用官方版本进行下载。 download.existing=文件已存在,无法保存。你可以将文件保存至其他地方。 download.external_link=打开下载网站 From d24b7b2b78ed00311f76a145a91c967b65fac3fb Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 16 Dec 2025 17:49:59 +0800 Subject: [PATCH 05/61] =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8C=85=E5=90=AF?= =?UTF-8?q?=E7=94=A8/=E7=A6=81=E7=94=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...istPage.java => ResourcePackListPage.java} | 116 +++---- .../hmcl/ui/versions/VersionPage.java | 4 +- .../hmcl/game/DefaultGameRepository.java | 9 +- .../jackhuang/hmcl/game/GameRepository.java | 6 + .../hmcl/mod/modinfo/PackMcMeta.java | 50 ++- .../hmcl/resourcepack/ResourcePackFile.java | 84 +++++ .../hmcl/resourcepack/ResourcePackFolder.java | 53 +++ .../resourcepack/ResourcePackManager.java | 304 ++++++++++++++++++ ...kZipFile.java => ResourcePackZipFile.java} | 42 +-- .../hmcl/resourcepack/ResourcepackFile.java | 30 -- .../hmcl/resourcepack/ResourcepackFolder.java | 60 ---- .../org/jackhuang/hmcl/util/StringUtils.java | 31 ++ .../org/jackhuang/hmcl/util/io/FileUtils.java | 6 +- 13 files changed, 614 insertions(+), 181 deletions(-) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/{ResourcepackListPage.java => ResourcePackListPage.java} (68%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/{ResourcepackZipFile.java => ResourcePackZipFile.java} (52%) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java similarity index 68% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index be212d7474..23aa3b5bf5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -1,8 +1,13 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXListView; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Skin; @@ -15,7 +20,8 @@ import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.resourcepack.ResourcepackFile; +import org.jackhuang.hmcl.resourcepack.ResourcePackFile; +import org.jackhuang.hmcl.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -32,62 +38,54 @@ import java.lang.ref.WeakReference; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Comparator; import java.util.List; -import java.util.Objects; -import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ResourcepackListPage extends ListPageBase implements VersionPage.VersionLoadable { - private Path resourcepackDirectory; +public final class ResourcePackListPage extends ListPageBase implements VersionPage.VersionLoadable { + private Path resourcePackDirectory; + private ResourcePackManager resourcePackManager; - public ResourcepackListPage() { + public ResourcePackListPage() { FXUtils.applyDragListener(this, file -> file.getFileName().toString().endsWith(".zip"), this::addFiles); } @Override protected Skin createDefaultSkin() { - return new ResourcepackListPageSkin(this); + return new ResourcePackListPageSkin(this); } @Override public void loadVersion(Profile profile, String version) { - this.resourcepackDirectory = profile.getRepository().getResourcepacksDirectory(version); + this.resourcePackDirectory = profile.getRepository().getResourcePackDirectory(version); + this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version); try { - if (!Files.exists(resourcepackDirectory)) { - Files.createDirectories(resourcepackDirectory); + if (!Files.exists(resourcePackDirectory)) { + Files.createDirectories(resourcePackDirectory); } } catch (IOException e) { - LOG.warning("Failed to create resourcepack directory" + resourcepackDirectory, e); + LOG.warning("Failed to create resource pack directory" + resourcePackDirectory, e); } refresh(); } public void refresh() { - if (resourcepackDirectory == null || !Files.isDirectory(resourcepackDirectory)) return; + if (resourcePackManager == null || !Files.isDirectory(resourcePackDirectory)) return; setLoading(true); Task.supplyAsync(Schedulers.io(), () -> { - try (Stream stream = Files.list(resourcepackDirectory)) { - return stream.sorted(Comparator.comparing(FileUtils::getName)) - .flatMap(item -> { - try { - return Stream.of(ResourcepackFile.parse(item)).filter(Objects::nonNull).map(ResourcepackInfoObject::new); - } catch (IOException e) { - LOG.warning("Failed to load resourcepack " + item, e); - return Stream.empty(); - } - }) - .toList(); - } + resourcePackManager.refreshResourcePacks(); + return resourcePackManager.getResourcePacks() + .stream() + .map(ResourcePackInfoObject::new) + .toList(); }).whenComplete(Schedulers.javafx(), ((result, exception) -> { if (exception == null) { getItems().setAll(result); } else { - LOG.warning("Failed to load resourcepacks", exception); + LOG.warning("Failed to load resource packs", exception); getItems().clear(); } setLoading(false); @@ -95,17 +93,14 @@ public void refresh() { } public void addFiles(List files) { - if (resourcepackDirectory == null) return; + if (resourcePackManager == null) return; try { for (Path file : files) { - Path target = resourcepackDirectory.resolve(file.getFileName()); - if (!Files.exists(target)) { - Files.copy(file, target); - } + resourcePackManager.importResourcePack(file); } } catch (IOException e) { - LOG.warning("Failed to add resourcepacks", e); + LOG.warning("Failed to add resource packs", e); Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); } @@ -127,10 +122,10 @@ private void onDownload() { Controllers.navigate(Controllers.getDownloadPage()); } - private static final class ResourcepackListPageSkin extends SkinBase { - private final JFXListView listView; + private static final class ResourcePackListPageSkin extends SkinBase { + private final JFXListView listView; - private ResourcepackListPageSkin(ResourcepackListPage control) { + private ResourcePackListPageSkin(ResourcePackListPage control) { super(control); StackPane pane = new StackPane(); @@ -157,7 +152,7 @@ private ResourcepackListPageSkin(ResourcepackListPage control) { center.loadingProperty().bind(control.loadingProperty()); Holder lastCell = new Holder<>(); - listView.setCellFactory(x -> new ResourcepackListCell(listView, lastCell, control)); + listView.setCellFactory(x -> new ResourcePackListCell(listView, lastCell, control)); Bindings.bindContent(listView.getItems(), control.getItems()); center.setContent(listView); @@ -168,18 +163,25 @@ private ResourcepackListPageSkin(ResourcepackListPage control) { } } - public static class ResourcepackInfoObject { - private final ResourcepackFile file; + public static class ResourcePackInfoObject { + private final ResourcePackFile file; + private final BooleanProperty enabled; private WeakReference iconCache; - public ResourcepackInfoObject(ResourcepackFile file) { + public ResourcePackInfoObject(ResourcePackFile file) { this.file = file; + this.enabled = new SimpleBooleanProperty(this, "enabled", file.isEnabled()); + this.enabled.addListener(__ -> file.setEnabled(enabled.get())); } - public ResourcepackFile getFile() { + public ResourcePackFile getFile() { return file; } + public BooleanProperty enabledProperty() { + return enabled; + } + Image getIcon() { Image image = null; if (iconCache != null && (image = iconCache.get()) != null) { @@ -190,7 +192,7 @@ Image getIcon() { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { image = new Image(inputStream, 64, 64, true, true); } catch (Exception e) { - LOG.warning("Failed to load resourcepack icon " + file.getPath(), e); + LOG.warning("Failed to load resource pack icon " + file.getPath(), e); } } @@ -203,14 +205,17 @@ Image getIcon() { } } - private static final class ResourcepackListCell extends MDListCell { + private static final class ResourcePackListCell extends MDListCell { + private final JFXCheckBox checkBox = new JFXCheckBox(); private final ImageView imageView = new ImageView(); private final TwoLineListItem content = new TwoLineListItem(); private final JFXButton btnReveal = new JFXButton(); private final JFXButton btnDelete = new JFXButton(); - private final ResourcepackListPage page; + private final ResourcePackListPage page; - public ResourcepackListCell(JFXListView listView, Holder lastCell, ResourcepackListPage page) { + private BooleanProperty booleanProperty = null; + + public ResourcePackListCell(JFXListView listView, Holder lastCell, ResourcePackListPage page) { super(listView, lastCell); this.page = page; @@ -221,7 +226,7 @@ public ResourcepackListCell(JFXListView listView, Holder HBox left = new HBox(8); left.setAlignment(Pos.CENTER); FXUtils.limitSize(imageView, 32, 32); - left.getChildren().add(imageView); + left.getChildren().addAll(checkBox, imageView); left.setPadding(new Insets(0, 8, 0, 0)); FXUtils.setLimitWidth(left, 48); root.setLeft(left); @@ -244,12 +249,12 @@ public ResourcepackListCell(JFXListView listView, Holder } @Override - protected void updateControl(ResourcepackListPage.ResourcepackInfoObject item, boolean empty) { + protected void updateControl(ResourcePackInfoObject item, boolean empty) { if (empty || item == null) { return; } - ResourcepackFile file = item.getFile(); + ResourcePackFile file = item.getFile(); imageView.setImage(item.getIcon()); content.setTitle(file.getName()); @@ -262,19 +267,22 @@ protected void updateControl(ResourcepackListPage.ResourcepackInfoObject item, b btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> onDelete(file), null)); + + if (booleanProperty != null) { + checkBox.selectedProperty().unbindBidirectional(booleanProperty); + } + checkBox.selectedProperty().bindBidirectional(booleanProperty = item.enabledProperty()); } - private void onDelete(ResourcepackFile file) { + private void onDelete(ResourcePackFile file) { try { - if (Files.isDirectory(file.getPath())) { - FileUtils.deleteDirectory(file.getPath()); - } else { - Files.delete(file.getPath()); + if (page.resourcePackManager != null) { + page.resourcePackManager.removeResourcePacks(file); + page.refresh(); } - page.refresh(); } catch (IOException e) { Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resourcepack", e); + LOG.warning("Failed to delete resource pack", e); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 34ded2d756..a3265d38f3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -56,7 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage private final TabHeader.Tab modListTab = new TabHeader.Tab<>("modListTab"); private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); - private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); + private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); private final TransitionPane transitionPane = new TransitionPane(); private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); private final ObjectProperty version = new SimpleObjectProperty<>(); @@ -68,7 +68,7 @@ public VersionPage() { versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false))); installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new)); modListTab.setNodeSupplier(loadVersionFor(ModListPage::new)); - resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new)); + resourcePackTab.setNodeSupplier(loadVersionFor(ResourcePackListPage::new)); worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new)); schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 42d5fcafec..3f2407a5ff 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -154,6 +154,11 @@ public Path getModsDirectory(String id) { return getRunDirectory(id).resolve("mods"); } + @Override + public Path getResourcePackDirectory(String id) { + return getRunDirectory(id).resolve("resourcepacks"); + } + @Override public Path getVersionRoot(String id) { return getBaseDirectory().resolve("versions/" + id); @@ -564,8 +569,4 @@ public String toString() { .append("baseDirectory", baseDirectory) .toString(); } - - public Path getResourcepacksDirectory(String id) { - return getRunDirectory(id).resolve("resourcepacks"); - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java index e9049123a9..1b7cc27dcd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -137,6 +137,12 @@ default Task refreshVersionsAsync() { /// @return the mods directory Path getModsDirectory(String id); + /// Get the directory for placing resource packs. + /// + /// @param id instance id + /// @return the resource pack directory + Path getResourcePackDirectory(String id); + /** * Get minecraft jar * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index 80b89d2599..71debe32a6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -29,6 +29,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.lang.reflect.Type; @@ -50,6 +51,7 @@ public void validate() throws JsonParseException { @JsonAdapter(PackInfoDeserializer.class) public record PackInfo(@SerializedName("pack_format") int packFormat, + @SerializedName("supported_formats") SupportedFormats supportedFormats, @SerializedName("min_format") PackVersion minPackVersion, @SerializedName("max_format") PackVersion maxPackVersion, @SerializedName("description") LocalModFile.Description description) { @@ -62,12 +64,51 @@ public PackVersion getEffectiveMaxVersion() { } } + public record SupportedFormats(int min, int max) { + + public static final SupportedFormats UNSPECIFIED = new SupportedFormats(-1, -1); + + public static SupportedFormats fromJson(JsonElement element) { + if (element == null || element.isJsonNull()) { + return UNSPECIFIED; + } + + try { + if (element instanceof JsonArray jsonArray) { + if (jsonArray.size() == 2 && jsonArray.get(0) instanceof JsonPrimitive && jsonArray.get(1) instanceof JsonPrimitive) { + return new SupportedFormats(jsonArray.get(0).getAsInt(), jsonArray.get(1).getAsInt()); + } else { + LOG.warning("Supported formats array must have 2 elements, but got " + jsonArray.size()); + } + } + } catch (NumberFormatException e) { + LOG.warning("Failed to parse datapack version component as a number. Value: " + element, e); + } + + return UNSPECIFIED; + } + + public boolean isUnspecified() { + return getMin().isUnspecified() || getMax().isUnspecified() || getMin().compareTo(getMax()) > 0; + } + + public PackVersion getMin() { + return new PackVersion(min, 0); + } + + public PackVersion getMax() { + return new PackVersion(max, 0); + } + + } + public record PackVersion(int majorVersion, int minorVersion) implements Comparable { - public static final PackVersion UNSPECIFIED = new PackVersion(0, 0); + public static final PackVersion UNSPECIFIED = new PackVersion(-1, -1); @Override - public String toString() { + public @NotNull String toString() { + if (isUnspecified()) return "UNSPECIFIED"; return minorVersion != 0 ? majorVersion + "." + minorVersion : String.valueOf(majorVersion); } @@ -81,7 +122,7 @@ public int compareTo(PackVersion other) { } public boolean isUnspecified() { - return this.equals(UNSPECIFIED); + return this.majorVersion < 0 || this.minorVersion < 0; } public static PackVersion fromJson(JsonElement element) throws JsonParseException { @@ -175,11 +216,12 @@ public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationC } else { packFormat = 0; } + SupportedFormats supportedFormats = SupportedFormats.fromJson(packInfo.get("supported_formats")); PackVersion minVersion = PackVersion.fromJson(packInfo.get("min_format")); PackVersion maxVersion = PackVersion.fromJson(packInfo.get("max_format")); List parts = parseDescription(packInfo.get("description")); - return new PackInfo(packFormat, minVersion, maxVersion, new LocalModFile.Description(parts)); + return new PackInfo(packFormat, supportedFormats, minVersion, maxVersion, new LocalModFile.Description(parts)); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java new file mode 100644 index 0000000000..6914a3942b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java @@ -0,0 +1,84 @@ +package org.jackhuang.hmcl.resourcepack; + +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; + +public sealed abstract class ResourcePackFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { + static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOException { + String fileName = path.getFileName().toString(); + if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { + return new ResourcePackZipFile(manager, path); + } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { + return new ResourcePackFolder(manager, path); + } + return null; + } + + protected final ResourcePackManager manager; + protected final Path path; + protected final String name; + protected final String fileName; + + protected ResourcePackFile(ResourcePackManager manager, Path path) { + this.manager = manager; + this.path = path; + this.fileName = FileUtils.getName(path); + this.name = FileUtils.getNameWithoutExtension(path); + } + + public Path getPath() { + return path; + } + + public String getName() { + return name; + } + + public String getFileName() { + return getPath().getFileName().toString(); + } + + public boolean isIncompatible() { + return manager.isIncompatible(this); + } + + public boolean isEnabled() { + return manager.isEnabled(this); + } + + public void setEnabled(boolean enabled) { + if (enabled) { + manager.enableResourcePack(this); + } else { + manager.disableResourcePack(this); + } + } + + @Nullable + @Contract(pure = true) + public abstract PackMcMeta getMeta(); + + @Nullable + public LocalModFile.Description getDescription() { + if (getMeta() == null || getMeta().pack() == null) return null; + return getMeta().pack().description(); + } + + public abstract byte @Nullable [] getIcon(); + + public abstract void delete() throws IOException; + + @Override + public int compareTo(@NotNull ResourcePackFile other) { + return this.getFileName().compareToIgnoreCase(other.getFileName()); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java new file mode 100644 index 0000000000..da60950243 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java @@ -0,0 +1,53 @@ +package org.jackhuang.hmcl.resourcepack; + +import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +final class ResourcePackFolder extends ResourcePackFile { + private final PackMcMeta meta; + private final byte @Nullable [] icon; + + public ResourcePackFolder(ResourcePackManager manager, Path path) { + super(manager, path); + + PackMcMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class); + } catch (Exception e) { + LOG.warning("Failed to parse resource pack meta", e); + } + this.meta = meta; + + byte[] icon; + try { + icon = Files.readAllBytes(path.resolve("pack.png")); + } catch (IOException e) { + icon = null; + LOG.warning("Failed to read resource pack icon", e); + } + this.icon = icon; + } + + @Override + public PackMcMeta getMeta() { + return meta; + } + + @Override + public byte @Nullable [] getIcon() { + return icon; + } + + @Override + public void delete() throws IOException { + FileUtils.deleteDirectory(path); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java new file mode 100644 index 0000000000..8eab075fb2 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java @@ -0,0 +1,304 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui 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 . + */ +package org.jackhuang.hmcl.resourcepack; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonSerializable; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.tree.ZipFileTree; +import org.jackhuang.hmcl.util.versioning.VersionRange; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class ResourcePackManager { + + @NotNull + public static PackMcMeta.PackVersion getPackVersion(Path gameJar) { + try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(gameJar))) { + return JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/version.json"), GameVersionInfo.class) + .packVersionInfo().resource(); + } catch (Exception e) { + LOG.error("Failed to load Minecraft resource pack version", e); + return PackMcMeta.PackVersion.UNSPECIFIED; + } + } + + @NotNull + @Contract(pure = true) + public static VersionRange getResourcePackVersionRange(PackMcMeta.PackInfo packInfo) { + if (packInfo == null) { + return VersionRange.empty(); + } + boolean packFormatUnspecified = packInfo.packFormat() <= 0; + boolean supportedFormatsUnspecified = packInfo.supportedFormats().isUnspecified(); + + // See https://zh.minecraft.wiki/w/Pack.mcmeta + // Also referring to Minecraft's source code + if (!(packInfo.minPackVersion().isUnspecified() || packInfo.maxPackVersion().isUnspecified())) { + int minMajor = packInfo.minPackVersion().majorVersion(); + int maxMajor = packInfo.maxPackVersion().majorVersion(); + if (packInfo.minPackVersion().compareTo(packInfo.maxPackVersion()) > 0) { + return VersionRange.empty(); + } + if (minMajor > 64) { + if (!supportedFormatsUnspecified) { + return VersionRange.empty(); + } + + if (!packFormatUnspecified && isPackFormatInvalidate(minMajor, maxMajor, packInfo.packFormat())) { + return VersionRange.empty(); + } + } else { + if (supportedFormatsUnspecified) { + return VersionRange.empty(); + } + PackMcMeta.SupportedFormats supportedFormats = packInfo.supportedFormats(); + if (supportedFormats.min() != minMajor) { + return VersionRange.empty(); + } + if (supportedFormats.max() != maxMajor && supportedFormats.max() != 64) { + return VersionRange.empty(); + } + if (packFormatUnspecified) return VersionRange.empty(); + if (isPackFormatInvalidate(minMajor, maxMajor, packInfo.packFormat())) return VersionRange.empty(); + } + + return VersionRange.between(packInfo.minPackVersion(), packInfo.maxPackVersion()); + } else if (!supportedFormatsUnspecified) { + PackMcMeta.SupportedFormats supportedFormats = packInfo.supportedFormats(); + int min = supportedFormats.min(); + int max = supportedFormats.max(); + if (max > 64) { + return VersionRange.empty(); + } else { + if (packFormatUnspecified) return VersionRange.empty(); + if (isPackFormatInvalidate(min, max, packInfo.packFormat())) return VersionRange.empty(); + } + + return VersionRange.between(supportedFormats.getMin(), supportedFormats.getMax()); + } else if (!packFormatUnspecified) { + int packFormat = packInfo.packFormat(); + PackMcMeta.PackVersion packVersion = new PackMcMeta.PackVersion(packFormat, 0); + return packFormat > 64 ? VersionRange.empty() : VersionRange.between(packVersion, packVersion); + } + return VersionRange.empty(); + } + + @Contract(pure = true) + private static boolean isPackFormatInvalidate(int i, int j, int k) { + if (k >= i && k <= j) { + return k < 15; + } else { + return true; + } + } + + private final GameRepository repository; + private final String id; + private final TreeSet resourcePackFiles = new TreeSet<>(); + private final Path optionsFile; + private final @NotNull PackMcMeta.PackVersion requiredVersion; + + private boolean loaded = false; + + public ResourcePackManager(GameRepository repository, String id) { + this.repository = repository; + this.id = id; + this.optionsFile = repository.getRunDirectory(id).resolve("options.txt"); + this.requiredVersion = getPackVersion(repository.getVersionJar(id)); + } + + @NotNull + private Map loadOptions() { + Map options = new LinkedHashMap<>(); + if (!Files.isRegularFile(optionsFile)) return options; + try (var stream = Files.lines(optionsFile)) { + stream.forEach(s -> { + if (StringUtils.isNotBlank(s)) { + var entry = s.split(":", 2); + if (entry.length == 2) { + options.put(entry[0], entry[1]); + } + } + }); + } catch (IOException e) { + LOG.warning("Failed to read instance options file", e); + } + return options; + } + + private void saveOptions(@NotNull Map options) { + try { + if (!Files.isRegularFile(optionsFile)) { + Files.createFile(optionsFile); + } + StringBuilder sb = new StringBuilder(); + for (var entry : options.entrySet()) { + sb.append(entry.getKey()).append(":").append(entry.getValue()).append(System.lineSeparator()); + } + Files.writeString(optionsFile, sb.toString(), StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + LOG.warning("Failed to save instance options file", e); + } + } + + public GameRepository getRepository() { + return repository; + } + + public String getInstanceId() { + return id; + } + + public Path getResourcePackDirectory() { + return repository.getResourcePackDirectory(id); + } + + private void addResourcePackInfo(Path file) throws IOException { + ResourcePackFile resourcePack = ResourcePackFile.parse(this, file); + if (resourcePack != null) resourcePackFiles.add(resourcePack); + } + + public void refreshResourcePacks() throws IOException { + resourcePackFiles.clear(); + + if (Files.isDirectory(getResourcePackDirectory())) { + try (DirectoryStream directoryStream = Files.newDirectoryStream(getResourcePackDirectory())) { + for (Path subitem : directoryStream) { + addResourcePackInfo(subitem); + } + } + } + loaded = true; + } + + public @Unmodifiable List getResourcePacks() throws IOException { + if (!loaded) + refreshResourcePacks(); + return List.copyOf(resourcePackFiles); + } + + public void importResourcePack(Path file) throws IOException { + if (!loaded) + refreshResourcePacks(); + + Path resourcePackDirectory = getResourcePackDirectory(); + Files.createDirectories(resourcePackDirectory); + + Path newFile = resourcePackDirectory.resolve(file.getFileName()); + FileUtils.copyFile(file, newFile); + + addResourcePackInfo(newFile); + } + + public void removeResourcePacks(ResourcePackFile... resourcePacks) throws IOException { + for (ResourcePackFile resourcePack : resourcePacks) { + if (resourcePack != null && resourcePack.manager == this) { + resourcePack.delete(); + resourcePackFiles.remove(resourcePack); + } + } + } + + public void enableResourcePack(ResourcePackFile resourcePack) { + if (resourcePack.manager != this) return; + Map options = loadOptions(); + String packId = "file/" + resourcePack.getFileName(); + boolean b = false; + List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); + if (!resourcePacks.contains(packId)) { + resourcePacks.add(packId); + options.put("resourcePacks", StringUtils.serializeStringList(resourcePacks)); + b = true; + } + List incompatibleResourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("incompatibleResourcePacks"))); + if (!incompatibleResourcePacks.contains(packId) && isIncompatible(resourcePack)) { + incompatibleResourcePacks.add(packId); + options.put("incompatibleResourcePacks", StringUtils.serializeStringList(incompatibleResourcePacks)); + b = true; + } + if (b) saveOptions(options); + } + + public void disableResourcePack(ResourcePackFile resourcePack) { + if (resourcePack.manager != this) return; + Map options = loadOptions(); + String packId = "file/" + resourcePack.getFileName(); + List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); + if (resourcePacks.contains(packId)) { + resourcePacks.remove(packId); + options.put("resourcePacks", StringUtils.serializeStringList(resourcePacks)); + saveOptions(options); + } + } + + public boolean isEnabled(ResourcePackFile resourcePack) { + if (resourcePack.manager != this) return false; + Map options = loadOptions(); + String packId = "file/" + resourcePack.getFileName(); + List resourcePacks = StringUtils.deserializeStringList(options.get("resourcePacks")); + if (!resourcePacks.contains(packId)) return false; + List incompatibleResourcePacks = StringUtils.deserializeStringList(options.get("incompatibleResourcePacks")); + return isIncompatible(resourcePack) == incompatibleResourcePacks.contains(packId); + } + + public boolean isIncompatible(ResourcePackFile resourcePack) { + if (resourcePack.getMeta() == null || resourcePack.getMeta().pack() == null) return true; + if (this.requiredVersion.isUnspecified()) return true; + PackMcMeta.PackInfo packInfo = resourcePack.getMeta().pack(); + return !getResourcePackVersionRange(packInfo).contains(this.requiredVersion); + } + + @JsonSerializable + private record GameVersionInfo(@SerializedName("pack_version") PackVersionInfo packVersionInfo) { + } + + @JsonSerializable + @JsonAdapter(PackVersionInfoDeserializer.class) + private record PackVersionInfo(PackMcMeta.PackVersion resource) { + } + + private static final class PackVersionInfoDeserializer implements JsonDeserializer { + @Override + public PackVersionInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new PackVersionInfo(PackMcMeta.PackVersion.fromJson(json.getAsJsonObject().get("resource"))); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java similarity index 52% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java index 8f35f933bc..1ca3f33548 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java @@ -1,72 +1,62 @@ package org.jackhuang.hmcl.resourcepack; -import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.tree.ZipFileTree; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ResourcepackZipFile implements ResourcepackFile { - private final Path path; +final class ResourcePackZipFile extends ResourcePackFile { + private final PackMcMeta meta; private final byte @Nullable [] icon; - private final String name; - private final LocalModFile.Description description; - public ResourcepackZipFile(Path path) throws IOException { - this.path = path; - LocalModFile.Description description = null; + public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException { + super(manager, path); + PackMcMeta meta = null; byte[] icon = null; try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(path))) { try { - description = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class).pack().description(); + meta = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class); } catch (Exception e) { - LOG.warning("Failed to parse resourcepack meta", e); + LOG.warning("Failed to parse resource pack meta", e); } + this.meta = meta; var iconEntry = zipFileTree.getEntry("/pack.png"); if (iconEntry != null) { try (InputStream is = zipFileTree.getInputStream(iconEntry)) { icon = is.readAllBytes(); } catch (Exception e) { - LOG.warning("Failed to load resourcepack icon", e); + LOG.warning("Failed to load resource pack icon", e); } } } this.icon = icon; - this.description = description; - - name = FileUtils.getNameWithoutExtension(path); - } - - @Override - public String getName() { - return name; } @Override - public Path getPath() { - return path; + public PackMcMeta getMeta() { + return meta; } @Override - public LocalModFile.Description getDescription() { - return description; + public byte @Nullable [] getIcon() { + return icon; } @Override - public byte @Nullable [] getIcon() { - return icon; + public void delete() throws IOException { + Files.deleteIfExists(path); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java deleted file mode 100644 index 6bfea93923..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.jackhuang.hmcl.resourcepack; - -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Locale; - -public interface ResourcepackFile { - @Nullable - LocalModFile.Description getDescription(); - - String getName(); - - Path getPath(); - - byte @Nullable [] getIcon(); - - static ResourcepackFile parse(Path path) throws IOException { - String fileName = path.getFileName().toString(); - if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { - return new ResourcepackZipFile(path); - } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { - return new ResourcepackFolder(path); - } - return null; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java deleted file mode 100644 index 14b5d3c637..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.jackhuang.hmcl.resourcepack; - -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; -import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class ResourcepackFolder implements ResourcepackFile { - private final Path path; - private final LocalModFile.Description description; - private final byte @Nullable [] icon; - - public ResourcepackFolder(Path path) { - this.path = path; - - LocalModFile.Description description = null; - try { - description = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class).pack().description(); - } catch (Exception e) { - LOG.warning("Failed to parse resourcepack meta", e); - } - - byte[] icon; - try { - icon = Files.readAllBytes(path.resolve("pack.png")); - } catch (IOException e) { - icon = null; - LOG.warning("Failed to read resourcepack icon", e); - } - this.icon = icon; - - this.description = description; - } - - @Override - public String getName() { - return path.getFileName().toString(); - } - - @Override - public Path getPath() { - return path; - } - - @Override - public LocalModFile.Description getDescription() { - return description; - } - - @Override - public byte @Nullable [] getIcon() { - return icon; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 26456b8f75..5f1de08dc4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.util; +import org.jetbrains.annotations.Contract; + import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; @@ -529,6 +531,35 @@ public static boolean isAlphabeticOrNumber(String str) { return true; } + /// Turns `List.of("a", "b", "c")` into `["a", "b", "c"]` + @Contract(pure = true) + public static String serializeStringList(List list) { + if (list == null || list.isEmpty()) return "[]"; + StringBuilder sb = new StringBuilder(); + var it = list.iterator(); + sb.append("[\"").append(it.next()).append("\""); + while (it.hasNext()) { + sb.append(",\"").append(it.next()).append("\""); + } + sb.append("]"); + return sb.toString(); + } + + /// Turns `["a", "b", "c"]` into `List.of("a", "b", "c")` + @Contract(pure = true) + public static List deserializeStringList(String list) { + if (list == null || list.isBlank()) return List.of(); + list = list.trim(); + if (!list.startsWith("[") || !list.endsWith("]")) return List.of(); + return Arrays.stream(list.substring(1, list.length() - 1).split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .filter(s -> s.startsWith("\"") && s.endsWith("\"")) + .map(s -> s.substring(1, s.length() - 1)) + .filter(StringUtils::isNotBlank) + .toList(); + } + public static class LevCalculator { private int[][] lev; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index 0a7043f236..6be9e4dfcc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -99,7 +99,11 @@ public static String getNameWithoutExtension(String fileName) { } public static String getNameWithoutExtension(Path file) { - return StringUtils.substringBeforeLast(getName(file), '.'); + String name = getName(file); + if (Files.isDirectory(file)) { + return name; + } + return StringUtils.substringBeforeLast(name, '.'); } public static String getExtension(String fileName) { From a2b5e8b9c79ce78679e779231c391582e297cfc1 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 16 Dec 2025 18:08:56 +0800 Subject: [PATCH 06/61] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=A8=A1=E7=BB=84?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=20Part1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 34 ++++++++----------- .../resourcepack/ResourcePackManager.java | 17 ++++------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 23aa3b5bf5..87f4368c00 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -3,8 +3,6 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXListView; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -14,7 +12,6 @@ import javafx.scene.control.SkinBase; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; @@ -220,32 +217,29 @@ public ResourcePackListCell(JFXListView listView, Holder this.page = page; - BorderPane root = new BorderPane(); - root.setPadding(new Insets(8)); + HBox root = new HBox(8); + root.setPickOnBounds(false); + root.setAlignment(Pos.CENTER_LEFT); - HBox left = new HBox(8); - left.setAlignment(Pos.CENTER); - FXUtils.limitSize(imageView, 32, 32); - left.getChildren().addAll(checkBox, imageView); - left.setPadding(new Insets(0, 8, 0, 0)); - FXUtils.setLimitWidth(left, 48); - root.setLeft(left); + imageView.setFitWidth(24); + imageView.setFitHeight(24); + imageView.setPreserveRatio(true); HBox.setHgrow(content, Priority.ALWAYS); - root.setCenter(content); + content.setMouseTransparent(true); btnReveal.getStyleClass().add("toggle-icon4"); - btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon()); + btnReveal.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24)); btnDelete.getStyleClass().add("toggle-icon4"); - btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon()); + btnDelete.setGraphic(FXUtils.limitingSize(SVG.DELETE_FOREVER.createIcon(24), 24, 24)); - HBox right = new HBox(8); - right.setAlignment(Pos.CENTER_RIGHT); - right.getChildren().setAll(btnReveal, btnDelete); - root.setRight(right); + root.getChildren().setAll(checkBox, imageView, content, btnReveal, btnDelete); - getContainer().getChildren().add(new RipplerContainer(root)); + setSelectable(); + + StackPane.setMargin(root, new Insets(8)); + getContainer().getChildren().add(root); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java index 8eab075fb2..c975f3d8ba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java @@ -36,10 +36,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; -import java.io.FileWriter; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.*; @@ -151,13 +148,13 @@ private Map loadOptions() { if (!Files.isRegularFile(optionsFile)) return options; try (var stream = Files.lines(optionsFile)) { stream.forEach(s -> { - if (StringUtils.isNotBlank(s)) { - var entry = s.split(":", 2); - if (entry.length == 2) { - options.put(entry[0], entry[1]); - } - } - }); + if (StringUtils.isNotBlank(s)) { + var entry = s.split(":", 2); + if (entry.length == 2) { + options.put(entry[0], entry[1]); + } + } + }); } catch (IOException e) { LOG.warning("Failed to read instance options file", e); } From 2ca2f3ed4ba4a158b8f9fb663893f46d08ca2d49 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 16 Dec 2025 21:21:28 +0800 Subject: [PATCH 07/61] =?UTF-8?q?=E4=B8=BA=E4=B8=8D=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8C=85=E6=B7=BB=E5=8A=A0=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 29 +++++++++++++++++++ HMCL/src/main/resources/assets/css/root.css | 4 +++ .../resources/assets/lang/I18N.properties | 9 ++++-- .../resources/assets/lang/I18N_zh.properties | 9 ++++-- .../assets/lang/I18N_zh_CN.properties | 9 ++++-- .../hmcl/resourcepack/ResourcePackFile.java | 20 +++++++++++-- .../resourcepack/ResourcePackManager.java | 22 ++++++++++---- 7 files changed, 89 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 87f4368c00..8bd7d4cb13 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -6,10 +6,12 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; @@ -203,6 +205,8 @@ Image getIcon() { } private static final class ResourcePackListCell extends MDListCell { + private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); + private final JFXCheckBox checkBox = new JFXCheckBox(); private final ImageView imageView = new ImageView(); private final TwoLineListItem content = new TwoLineListItem(); @@ -210,11 +214,15 @@ private static final class ResourcePackListCell extends MDListCell listView, Holder lastCell, ResourcePackListPage page) { super(listView, lastCell); + getStyleClass().add("resource-pack-list-cell"); + this.page = page; HBox root = new HBox(8); @@ -244,6 +252,12 @@ public ResourcePackListCell(JFXListView listView, Holder @Override protected void updateControl(ResourcePackInfoObject item, boolean empty) { + pseudoClassStateChanged(WARNING, false); + if (warningTooltip != null) { + Tooltip.uninstall(this, warningTooltip); + warningTooltip = null; + } + if (empty || item == null) { return; } @@ -266,6 +280,21 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { checkBox.selectedProperty().unbindBidirectional(booleanProperty); } checkBox.selectedProperty().bindBidirectional(booleanProperty = item.enabledProperty()); + + { + String warningKey = switch (item.file.getCompatibility()) { + case TOO_NEW -> "resourcepack.warning.too_new"; + case TOO_OLD -> "resourcepack.warning.too_old"; + case INVALID -> "resourcepack.warning.invalid"; + case MISSING_PACK_META -> "resourcepack.warning.missing_pack_meta"; + case MISSING_GAME_META -> "resourcepack.warning.missing_game_meta"; + default -> null; + }; + if (warningKey != null) { + pseudoClassStateChanged(WARNING, true); + FXUtils.installFastTooltip(this, warningTooltip = new Tooltip(i18n(warningKey))); + } + } } private void onDelete(ResourcePackFile file) { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 5b0ade9c98..23a81d4321 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -984,6 +984,10 @@ -fx-background-color: -monet-error-container; } +.resource-pack-list-cell:warning { + -fx-background-color: -monet-error-container; +} + .options-sublist { -fx-background-color: -monet-surface; } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 3e0bd28f97..1d8f852b16 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1209,11 +1209,16 @@ repositories.chooser.title=Choose download source for JavaFX resourcepack=Resource Packs resourcepack.add=Add -resourcepack.manage=Resource Packs -resourcepack.download=Download resourcepack.add.failed=Failed to add resource pack resourcepack.delete.failed=Failed to delete resource pack +resourcepack.download=Download resourcepack.download.title=Download Resource Pack - %1s +resourcepack.manage=Resource Packs +resourcepack.warning.invalid=Invalid pack metadata +resourcepack.warning.missing_game_meta=Missing game metadata +resourcepack.warning.missing_pack_meta=Missing pack metadata +resourcepack.warning.too_new=For newer game versions +resourcepack.warning.too_old=For older game versions reveal.in_file_manager=Reveal in File Manager diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 43fa0a3b43..e44adeeb53 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1002,11 +1002,16 @@ repositories.chooser.title=選取 JavaFX 下載源 resourcepack=資源包 resourcepack.add=新增資源包 -resourcepack.manage=資源包管理 -resourcepack.download=下載資源包 resourcepack.add.failed=新增資源包失敗 resourcepack.delete.failed=刪除資源包失敗 +resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s +resourcepack.manage=資源包管理 +resourcepack.warning.invalid=資源包元數據無效 +resourcepack.warning.missing_game_meta=遊戲版本元數據缺失 +resourcepack.warning.missing_pack_meta=資源包元數據缺失 +resourcepack.warning.too_new=爲更新的遊戲版本打造 +resourcepack.warning.too_old=爲更老的遊戲版本打造 reveal.in_file_manager=在檔案管理員中查看 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 04662a4da2..5e8d6eb08f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1012,11 +1012,16 @@ repositories.chooser.title=选择 JavaFX 下载源 resourcepack=资源包 resourcepack.add=添加资源包 -resourcepack.manage=资源包管理 -resourcepack.download=下载资源包 resourcepack.add.failed=添加资源包失败 resourcepack.delete.failed=删除资源包失败 +resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s +resourcepack.manage=资源包管理 +resourcepack.warning.invalid=资源包元数据无效 +resourcepack.warning.missing_game_meta=游戏版本元数据缺失 +resourcepack.warning.missing_pack_meta=资源包元数据缺失 +resourcepack.warning.too_new=为更新的游戏版本打造 +resourcepack.warning.too_old=为更老的游戏版本打造 reveal.in_file_manager=在文件管理器中查看 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java index 6914a3942b..897d70705d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java @@ -1,5 +1,7 @@ package org.jackhuang.hmcl.resourcepack; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.io.FileUtils; @@ -28,6 +30,8 @@ static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOE protected final String name; protected final String fileName; + private ObjectProperty compatibility = null; + protected ResourcePackFile(ResourcePackManager manager, Path path) { this.manager = manager; this.path = path; @@ -47,8 +51,11 @@ public String getFileName() { return getPath().getFileName().toString(); } - public boolean isIncompatible() { - return manager.isIncompatible(this); + public Compatibility getCompatibility() { + if (compatibility == null) { + compatibility = new SimpleObjectProperty<>(this, "compatibility", manager.getCompatibility(this)); + } + return compatibility.get(); } public boolean isEnabled() { @@ -81,4 +88,13 @@ public LocalModFile.Description getDescription() { public int compareTo(@NotNull ResourcePackFile other) { return this.getFileName().compareToIgnoreCase(other.getFileName()); } + + public enum Compatibility { + COMPATIBLE, + TOO_NEW, + TOO_OLD, + INVALID, + MISSING_PACK_META, + MISSING_GAME_META + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java index c975f3d8ba..f4e0223459 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java @@ -276,11 +276,23 @@ public boolean isEnabled(ResourcePackFile resourcePack) { return isIncompatible(resourcePack) == incompatibleResourcePacks.contains(packId); } - public boolean isIncompatible(ResourcePackFile resourcePack) { - if (resourcePack.getMeta() == null || resourcePack.getMeta().pack() == null) return true; - if (this.requiredVersion.isUnspecified()) return true; - PackMcMeta.PackInfo packInfo = resourcePack.getMeta().pack(); - return !getResourcePackVersionRange(packInfo).contains(this.requiredVersion); + public ResourcePackFile.Compatibility getCompatibility(@NotNull ResourcePackFile resourcePack) { + if (resourcePack.getMeta() == null || resourcePack.getMeta().pack() == null) return ResourcePackFile.Compatibility.MISSING_PACK_META; + if (this.requiredVersion.isUnspecified()) return ResourcePackFile.Compatibility.MISSING_GAME_META; + var versionRange = getResourcePackVersionRange(resourcePack.getMeta().pack()); + if (versionRange.isEmpty()) { + return ResourcePackFile.Compatibility.INVALID; + } else if (versionRange.getMaximum().compareTo(this.requiredVersion) < 0) { + return ResourcePackFile.Compatibility.TOO_OLD; + } else if (versionRange.getMinimum().compareTo(this.requiredVersion) > 0) { + return ResourcePackFile.Compatibility.TOO_NEW; + } else { + return ResourcePackFile.Compatibility.COMPATIBLE; + } + } + + public boolean isIncompatible(@NotNull ResourcePackFile resourcePack) { + return getCompatibility(resourcePack) != ResourcePackFile.Compatibility.COMPATIBLE; } @JsonSerializable From ce9377b0c511b9b2720123f688126a8a3151033c Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 17 Dec 2025 17:58:41 +0800 Subject: [PATCH 08/61] update --- .../java/org/jackhuang/hmcl/util/StringUtils.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 5f1de08dc4..5932965e01 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -24,6 +24,7 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * @author huangyuhui @@ -535,14 +536,7 @@ public static boolean isAlphabeticOrNumber(String str) { @Contract(pure = true) public static String serializeStringList(List list) { if (list == null || list.isEmpty()) return "[]"; - StringBuilder sb = new StringBuilder(); - var it = list.iterator(); - sb.append("[\"").append(it.next()).append("\""); - while (it.hasNext()) { - sb.append(",\"").append(it.next()).append("\""); - } - sb.append("]"); - return sb.toString(); + return list.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")); } /// Turns `["a", "b", "c"]` into `List.of("a", "b", "c")` @@ -550,11 +544,10 @@ public static String serializeStringList(List list) { public static List deserializeStringList(String list) { if (list == null || list.isBlank()) return List.of(); list = list.trim(); - if (!list.startsWith("[") || !list.endsWith("]")) return List.of(); + if (list.length() < 4 || !list.startsWith("[") || !list.endsWith("]")) return List.of(); return Arrays.stream(list.substring(1, list.length() - 1).split(",")) .map(String::trim) - .filter(StringUtils::isNotBlank) - .filter(s -> s.startsWith("\"") && s.endsWith("\"")) + .filter(s -> s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) .map(s -> s.substring(1, s.length() - 1)) .filter(StringUtils::isNotBlank) .toList(); From bd96337c36f79c3ee329b59caf9e111ecc523ae0 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 17 Dec 2025 20:00:26 +0800 Subject: [PATCH 09/61] =?UTF-8?q?=E5=90=AF=E7=94=A8/=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8C=85=E6=97=B6=E6=B7=BB=E5=8A=A0=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=20=E5=9C=A8BMC4=E4=B8=AD=E6=B5=8B=E8=AF=95=E6=97=B6?= =?UTF-8?q?=E5=8F=91=E7=8E=B0BMC4=E4=BC=9A=E5=88=A4=E5=AE=9A=E6=89=80?= =?UTF-8?q?=E6=9C=89=E8=B5=84=E6=BA=90=E5=8C=85=E5=85=BC=E5=AE=B9=EF=BC=8C?= =?UTF-8?q?=E4=B8=8E=E5=8E=9F=E7=89=88=E8=A1=A8=E7=8E=B0=E4=B8=8D=E7=AC=A6?= =?UTF-8?q?=EF=BC=8C=E4=BC=9A=E5=AF=BC=E8=87=B4=E5=90=AF=E7=94=A8/?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E5=8A=9F=E8=83=BD=E5=A4=B1=E6=95=88=EF=BC=8C?= =?UTF-8?q?=E6=95=85=E6=B7=BB=E5=8A=A0=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 21 +++++++- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../resourcepack/ResourcePackManager.java | 53 ++++++++++++++----- .../hmcl/util/versioning/VersionRange.java | 5 ++ 6 files changed, 67 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 8bd7d4cb13..178348bcc6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -47,6 +47,8 @@ public final class ResourcePackListPage extends ListPageBase file.getFileName().toString().endsWith(".zip"), this::addFiles); } @@ -58,8 +60,8 @@ protected Skin createDefaultSkin() { @Override public void loadVersion(Profile profile, String version) { - this.resourcePackDirectory = profile.getRepository().getResourcePackDirectory(version); this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version); + this.resourcePackDirectory = this.resourcePackManager.getResourcePackDirectory(); try { if (!Files.exists(resourcePackDirectory)) { @@ -207,7 +209,7 @@ Image getIcon() { private static final class ResourcePackListCell extends MDListCell { private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); - private final JFXCheckBox checkBox = new JFXCheckBox(); + private final JFXCheckBox checkBox; private final ImageView imageView = new ImageView(); private final TwoLineListItem content = new TwoLineListItem(); private final JFXButton btnReveal = new JFXButton(); @@ -229,6 +231,21 @@ public ResourcePackListCell(JFXListView listView, Holder root.setPickOnBounds(false); root.setAlignment(Pos.CENTER_LEFT); + checkBox = new JFXCheckBox() { + @Override + public void fire() { + if (!page.warningShown) { + Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), + () -> { + super.fire(); + page.warningShown = true; + }, null); + } else { + super.fire(); + } + } + }; + imageView.setFitWidth(24); imageView.setFitHeight(24); imageView.setPreserveRatio(true); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 1d8f852b16..a837179da5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1215,6 +1215,7 @@ resourcepack.download=Download resourcepack.download.title=Download Resource Pack - %1s resourcepack.manage=Resource Packs resourcepack.warning.invalid=Invalid pack metadata +resourcepack.warning.manipulate=Resource pack loading could be influenced by mods, causing unexpected behavior.\nYou may need to re-check after launching the game. resourcepack.warning.missing_game_meta=Missing game metadata resourcepack.warning.missing_pack_meta=Missing pack metadata resourcepack.warning.too_new=For newer game versions diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e44adeeb53..557bdf126c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1008,6 +1008,7 @@ resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包元數據無效 +resourcepack.warning.manipulate=資源包的加載可能受到模組影響,實際效果可能與預期不符。\n可能需要在遊戲內重新確認。 resourcepack.warning.missing_game_meta=遊戲版本元數據缺失 resourcepack.warning.missing_pack_meta=資源包元數據缺失 resourcepack.warning.too_new=爲更新的遊戲版本打造 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 5e8d6eb08f..8c1b3ec1f4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1018,6 +1018,7 @@ resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 +resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n可能需要在游戏内重新确认。 resourcepack.warning.missing_game_meta=游戏版本元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java index f4e0223459..8222eee303 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java @@ -59,7 +59,23 @@ public static PackMcMeta.PackVersion getPackVersion(Path gameJar) { @NotNull @Contract(pure = true) - public static VersionRange getResourcePackVersionRange(PackMcMeta.PackInfo packInfo) { + public static VersionRange getResourcePackVersionRangeOld(PackMcMeta.PackInfo packInfo) { + if (packInfo == null) { + return VersionRange.empty(); + } + boolean supportedFormatsUnspecified = packInfo.supportedFormats().isUnspecified(); + if (supportedFormatsUnspecified && packInfo.packFormat() <= 0) { + return VersionRange.empty(); + } + if (supportedFormatsUnspecified) { + return VersionRange.only(new PackMcMeta.PackVersion(packInfo.packFormat(), 0)); + } + return VersionRange.between(packInfo.supportedFormats().getMin(), packInfo.supportedFormats().getMax()); + } + + @NotNull + @Contract(pure = true) + public static VersionRange getResourcePackVersionRangeNew(PackMcMeta.PackInfo packInfo) { if (packInfo == null) { return VersionRange.empty(); } @@ -129,7 +145,10 @@ private static boolean isPackFormatInvalidate(int i, int j, int k) { private final GameRepository repository; private final String id; + + private final Path resourcePackDirectory; private final TreeSet resourcePackFiles = new TreeSet<>(); + private final Path optionsFile; private final @NotNull PackMcMeta.PackVersion requiredVersion; @@ -138,6 +157,7 @@ private static boolean isPackFormatInvalidate(int i, int j, int k) { public ResourcePackManager(GameRepository repository, String id) { this.repository = repository; this.id = id; + this.resourcePackDirectory = this.repository.getResourcePackDirectory(this.id); this.optionsFile = repository.getRunDirectory(id).resolve("options.txt"); this.requiredVersion = getPackVersion(repository.getVersionJar(id)); } @@ -185,7 +205,7 @@ public String getInstanceId() { } public Path getResourcePackDirectory() { - return repository.getResourcePackDirectory(id); + return resourcePackDirectory; } private void addResourcePackInfo(Path file) throws IOException { @@ -196,8 +216,8 @@ private void addResourcePackInfo(Path file) throws IOException { public void refreshResourcePacks() throws IOException { resourcePackFiles.clear(); - if (Files.isDirectory(getResourcePackDirectory())) { - try (DirectoryStream directoryStream = Files.newDirectoryStream(getResourcePackDirectory())) { + if (Files.isDirectory(resourcePackDirectory)) { + try (DirectoryStream directoryStream = Files.newDirectoryStream(resourcePackDirectory)) { for (Path subitem : directoryStream) { addResourcePackInfo(subitem); } @@ -216,7 +236,6 @@ public void importResourcePack(Path file) throws IOException { if (!loaded) refreshResourcePacks(); - Path resourcePackDirectory = getResourcePackDirectory(); Files.createDirectories(resourcePackDirectory); Path newFile = resourcePackDirectory.resolve(file.getFileName()); @@ -258,12 +277,20 @@ public void disableResourcePack(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return; Map options = loadOptions(); String packId = "file/" + resourcePack.getFileName(); + boolean b = false; List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); if (resourcePacks.contains(packId)) { resourcePacks.remove(packId); options.put("resourcePacks", StringUtils.serializeStringList(resourcePacks)); - saveOptions(options); + b = true; } + List incompatibleResourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("incompatibleResourcePacks"))); + if (incompatibleResourcePacks.contains(packId)) { + incompatibleResourcePacks.remove(packId); + options.put("incompatibleResourcePacks", StringUtils.serializeStringList(incompatibleResourcePacks)); + b = true; + } + if (b) saveOptions(options); } public boolean isEnabled(ResourcePackFile resourcePack) { @@ -279,16 +306,16 @@ public boolean isEnabled(ResourcePackFile resourcePack) { public ResourcePackFile.Compatibility getCompatibility(@NotNull ResourcePackFile resourcePack) { if (resourcePack.getMeta() == null || resourcePack.getMeta().pack() == null) return ResourcePackFile.Compatibility.MISSING_PACK_META; if (this.requiredVersion.isUnspecified()) return ResourcePackFile.Compatibility.MISSING_GAME_META; - var versionRange = getResourcePackVersionRange(resourcePack.getMeta().pack()); - if (versionRange.isEmpty()) { + var versionRange = requiredVersion.majorVersion() > 64 + ? getResourcePackVersionRangeNew(resourcePack.getMeta().pack()) + : getResourcePackVersionRangeOld(resourcePack.getMeta().pack()); + if (versionRange.isEmpty()) return ResourcePackFile.Compatibility.INVALID; - } else if (versionRange.getMaximum().compareTo(this.requiredVersion) < 0) { + if (versionRange.getMaximum().compareTo(this.requiredVersion) < 0) return ResourcePackFile.Compatibility.TOO_OLD; - } else if (versionRange.getMinimum().compareTo(this.requiredVersion) > 0) { + if (versionRange.getMinimum().compareTo(this.requiredVersion) > 0) return ResourcePackFile.Compatibility.TOO_NEW; - } else { - return ResourcePackFile.Compatibility.COMPATIBLE; - } + return ResourcePackFile.Compatibility.COMPATIBLE; } public boolean isIncompatible(@NotNull ResourcePackFile resourcePack) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java index c0d6cd1912..1e5b4db1d5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java @@ -33,6 +33,11 @@ public static > VersionRange atMost(T maximum) { return new VersionRange<>(null, maximum); } + public static > VersionRange only(T version) { + assert version != null; + return new VersionRange<>(version, version); + } + private final T minimum; private final T maximum; From bc229e3dbc9026a1ddf04e30f8ce563bf6ff7512 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 17 Dec 2025 20:37:33 +0800 Subject: [PATCH 10/61] =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8C=85=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E4=BF=A1=E6=81=AF=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 93 ++++++++++++++++--- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 178348bcc6..907b1909fc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -2,6 +2,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXListView; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; @@ -9,16 +10,14 @@ import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; -import org.jackhuang.hmcl.mod.LocalModFile; +import javafx.stage.Stage; import org.jackhuang.hmcl.resourcepack.ResourcePackFile; import org.jackhuang.hmcl.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.setting.Profile; @@ -31,6 +30,7 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -38,12 +38,25 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Objects; +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class ResourcePackListPage extends ListPageBase implements VersionPage.VersionLoadable { + private static @Nullable String getWarningKey(ResourcePackFile.Compatibility compatibility) { + return switch (compatibility) { + case TOO_NEW -> "resourcepack.warning.too_new"; + case TOO_OLD -> "resourcepack.warning.too_old"; + case INVALID -> "resourcepack.warning.invalid"; + case MISSING_PACK_META -> "resourcepack.warning.missing_pack_meta"; + case MISSING_GAME_META -> "resourcepack.warning.missing_game_meta"; + default -> null; + }; + } + private Path resourcePackDirectory; private ResourcePackManager resourcePackManager; @@ -214,6 +227,7 @@ private static final class ResourcePackListCell extends MDListCell FXUtils.showFileInExplorer(file.getPath())); + btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(item))); + btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> onDelete(file), null)); @@ -299,14 +317,7 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { checkBox.selectedProperty().bindBidirectional(booleanProperty = item.enabledProperty()); { - String warningKey = switch (item.file.getCompatibility()) { - case TOO_NEW -> "resourcepack.warning.too_new"; - case TOO_OLD -> "resourcepack.warning.too_old"; - case INVALID -> "resourcepack.warning.invalid"; - case MISSING_PACK_META -> "resourcepack.warning.missing_pack_meta"; - case MISSING_GAME_META -> "resourcepack.warning.missing_game_meta"; - default -> null; - }; + String warningKey = getWarningKey(file.getCompatibility()); if (warningKey != null) { pseudoClassStateChanged(WARNING, true); FXUtils.installFastTooltip(this, warningTooltip = new Tooltip(i18n(warningKey))); @@ -326,4 +337,56 @@ private void onDelete(ResourcePackFile file) { } } } + + private static final class ResourcePackInfoDialog extends JFXDialogLayout { + + ResourcePackInfoDialog(ResourcePackInfoObject packInfoObject) { + HBox titleContainer = new HBox(); + titleContainer.setSpacing(8); + + Stage stage = Controllers.getStage(); + maxWidthProperty().bind(stage.widthProperty().multiply(0.7)); + + ImageView imageView = new ImageView(); + imageView.setImage(packInfoObject.getIcon()); + FXUtils.limitSize(imageView, 40, 40); + + TwoLineListItem title = new TwoLineListItem(); + title.setTitle(packInfoObject.file.getName()); + title.setSubtitle(packInfoObject.file.getFileName()); + if (packInfoObject.file.getCompatibility() == ResourcePackFile.Compatibility.COMPATIBLE) { + title.addTag(i18n("resourcepack.compatible")); + } else { + title.addTagWarning(i18n(getWarningKey(packInfoObject.file.getCompatibility()))); + } + + titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title); + setHeading(titleContainer); + + Label description = new Label(Objects.requireNonNullElse(packInfoObject.file.getDescription(), "").toString()); + description.setWrapText(true); + FXUtils.copyOnDoubleClick(description); + + ScrollPane descriptionPane = new ScrollPane(description); + FXUtils.smoothScrolling(descriptionPane); + descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + descriptionPane.setFitToWidth(true); + description.heightProperty().addListener((obs, oldVal, newVal) -> { + double maxHeight = stage.getHeight() * 0.5; + double targetHeight = Math.min(newVal.doubleValue(), maxHeight); + descriptionPane.setPrefViewportHeight(targetHeight); + }); + + setBody(descriptionPane); + + JFXButton okButton = new JFXButton(); + okButton.getStyleClass().add("dialog-accept"); + okButton.setText(i18n("button.ok")); + okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + getActions().add(okButton); + + onEscPressed(this, okButton::fire); + } + } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a837179da5..544c652594 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1210,6 +1210,7 @@ repositories.chooser.title=Choose download source for JavaFX resourcepack=Resource Packs resourcepack.add=Add resourcepack.add.failed=Failed to add resource pack +resourcepack.compatible=Compatible resourcepack.delete.failed=Failed to delete resource pack resourcepack.download=Download resourcepack.download.title=Download Resource Pack - %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 557bdf126c..8690bddfa5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1003,6 +1003,7 @@ repositories.chooser.title=選取 JavaFX 下載源 resourcepack=資源包 resourcepack.add=新增資源包 resourcepack.add.failed=新增資源包失敗 +resourcepack.compatible=適用於此版本 resourcepack.delete.failed=刪除資源包失敗 resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 8c1b3ec1f4..2875f1be6d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1013,6 +1013,7 @@ repositories.chooser.title=选择 JavaFX 下载源 resourcepack=资源包 resourcepack.add=添加资源包 resourcepack.add.failed=添加资源包失败 +resourcepack.compatible=适用于此版本 resourcepack.delete.failed=删除资源包失败 resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s From 777aea38eaf152781d244c3e39f08a72ad900a52 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 17 Dec 2025 20:42:31 +0800 Subject: [PATCH 11/61] =?UTF-8?q?=E4=BF=AE=E5=A4=8Desc=E9=94=AE=E5=A4=B1?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/ResourcePackListPage.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 907b1909fc..553f318711 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -13,6 +13,8 @@ import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; @@ -40,6 +42,7 @@ import java.util.List; import java.util.Objects; +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.i18n.I18n.i18n; @@ -148,8 +151,19 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { ComponentList root = new ComponentList(); root.getStyleClass().add("no-padding"); + listView = new JFXListView<>(); + root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + HBox toolbar = new HBox(); toolbar.setAlignment(Pos.CENTER_LEFT); toolbar.setPickOnBounds(false); From e7c4f2e06d85725164cfa792009045b0aa63a340 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 17 Dec 2025 21:28:38 +0800 Subject: [PATCH 12/61] Update i18n Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- .../main/resources/assets/lang/I18N_zh.properties | 12 ++++++------ .../main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 8690bddfa5..0b910c3f86 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1008,12 +1008,12 @@ resourcepack.delete.failed=刪除資源包失敗 resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 -resourcepack.warning.invalid=資源包元數據無效 -resourcepack.warning.manipulate=資源包的加載可能受到模組影響,實際效果可能與預期不符。\n可能需要在遊戲內重新確認。 -resourcepack.warning.missing_game_meta=遊戲版本元數據缺失 -resourcepack.warning.missing_pack_meta=資源包元數據缺失 -resourcepack.warning.too_new=爲更新的遊戲版本打造 -resourcepack.warning.too_old=爲更老的遊戲版本打造 +resourcepack.warning.invalid=資源包中繼資料無效 +resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n可能需要在遊戲內重新確認。 +resourcepack.warning.missing_game_meta=遊戲版本中繼資料缺失 +resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 +resourcepack.warning.too_new=為更新的遊戲版本製作 +resourcepack.warning.too_old=為更老的遊戲版本製作 reveal.in_file_manager=在檔案管理員中查看 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 2875f1be6d..329c3a1244 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -347,7 +347,7 @@ download.hint=安装游戏和整合包或下载模组、资源包、光影和世 download.code.404=远程服务器不包含需要下载的文件: %s\n你可以点击右上角帮助按钮进行求助。 download.content=游戏内容 download.shader=光影 -download.shader.title=下载光影 - %1s +download.shader.title=光影下载 - %1s download.curseforge.unavailable=此 HMCL 版本不支持访问 CurseForge。请使用官方版本进行下载。 download.existing=文件已存在,无法保存。你可以将文件保存至其他地方。 download.external_link=打开下载网站 From 07a9cc0a0defc098d411ae90c474f166b4c0809b Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 17 Dec 2025 21:29:18 +0800 Subject: [PATCH 13/61] Update i18n Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0b910c3f86..d14e673b14 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -339,7 +339,7 @@ download.hint=安裝遊戲和模組包或下載模組、資源包、光影和世 download.code.404=遠端伺服器沒有需要下載的檔案:%s download.content=遊戲內容 download.shader=光影 -download.shader.title=下載光影 - %1s +download.shader.title=光影下載 - %1s download.curseforge.unavailable=這個 HMCL 版本不支援訪問 CurseForge。請使用官方版本進行下載。 download.existing=檔案已存在,無法儲存。你可以將檔案儲存至其他地方。 download.external_link=開啟下載網站 From 9aca021ef9fbf216ed600541d98ff9f2c97cd35a Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 18 Dec 2025 20:01:39 +0800 Subject: [PATCH 14/61] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=8C=85=E7=89=88=E6=9C=AC=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 6 ++ .../resourcepack/ResourcePackManager.java | 65 ++++++++++++++----- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 553f318711..d46419ff6c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -91,6 +91,12 @@ public void loadVersion(Profile profile, String version) { public void refresh() { if (resourcePackManager == null || !Files.isDirectory(resourcePackDirectory)) return; + setDisable(false); + if (resourcePackManager.getMinecraftVersion().compareTo(ResourcePackManager.LEAST_MC_VERSION) < 0) { + getItems().clear(); + setDisable(true); + return; + } setLoading(true); Task.supplyAsync(Schedulers.io(), () -> { resourcePackManager.refreshResourcePacks(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java index 8222eee303..4dcf7d7295 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java @@ -17,11 +17,6 @@ */ package org.jackhuang.hmcl.resourcepack; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; @@ -31,13 +26,13 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.tree.ZipFileTree; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jackhuang.hmcl.util.versioning.VersionRange; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; @@ -46,11 +41,41 @@ public final class ResourcePackManager { + private static final List RESOURCE_PACK_VERSION_OLD = List.of( + "13w24a", // 1 + "15w31a", // 2 + "16w32a", // 3 + "17w48a", // 4 + "18w47b" // 5 + ); + + public static final String LEAST_MC_VERSION = "13w24a"; + @NotNull - public static PackMcMeta.PackVersion getPackVersion(Path gameJar) { + public static PackMcMeta.PackVersion getPackVersion(GameVersionNumber minecraftVersion, Path gameJar) { + for (int i = 0; i < RESOURCE_PACK_VERSION_OLD.size(); i++) { + if (minecraftVersion.compareTo(RESOURCE_PACK_VERSION_OLD.get(i)) < 0) { + return i == 0 ? PackMcMeta.PackVersion.UNSPECIFIED : new PackMcMeta.PackVersion(i, 0); + } + } try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(gameJar))) { - return JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/version.json"), GameVersionInfo.class) - .packVersionInfo().resource(); + String versionJson = zipFileTree.readTextEntry("/version.json"); + try { + var info = JsonUtils.fromNonNullJson(versionJson, GameVersionInfo117.class).packVersionInfo(); + if (info.resourceMajor() > 64) { + return new PackMcMeta.PackVersion(info.resourceMajor(), info.resourceMinor()); + } else { + return new PackMcMeta.PackVersion(info.resource(), 0); + } + } catch (Exception e) { + LOG.warning("Failed to load Minecraft resource pack version for 25w31a+", e); + } + try { + return new PackMcMeta.PackVersion(JsonUtils.fromNonNullJson(versionJson, GameVersionInfo114.class).packVersion, 0); + } catch (Exception e) { + LOG.warning("Failed to load Minecraft resource pack version for 18w47b+", e); + } + return PackMcMeta.PackVersion.UNSPECIFIED; } catch (Exception e) { LOG.error("Failed to load Minecraft resource pack version", e); return PackMcMeta.PackVersion.UNSPECIFIED; @@ -145,6 +170,7 @@ private static boolean isPackFormatInvalidate(int i, int j, int k) { private final GameRepository repository; private final String id; + private final GameVersionNumber minecraftVersion; private final Path resourcePackDirectory; private final TreeSet resourcePackFiles = new TreeSet<>(); @@ -157,9 +183,10 @@ private static boolean isPackFormatInvalidate(int i, int j, int k) { public ResourcePackManager(GameRepository repository, String id) { this.repository = repository; this.id = id; + this.minecraftVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(id)); this.resourcePackDirectory = this.repository.getResourcePackDirectory(this.id); this.optionsFile = repository.getRunDirectory(id).resolve("options.txt"); - this.requiredVersion = getPackVersion(repository.getVersionJar(id)); + this.requiredVersion = getPackVersion(minecraftVersion, repository.getVersionJar(id)); } @NotNull @@ -204,6 +231,10 @@ public String getInstanceId() { return id; } + public GameVersionNumber getMinecraftVersion() { + return minecraftVersion; + } + public Path getResourcePackDirectory() { return resourcePackDirectory; } @@ -323,18 +354,16 @@ public boolean isIncompatible(@NotNull ResourcePackFile resourcePack) { } @JsonSerializable - private record GameVersionInfo(@SerializedName("pack_version") PackVersionInfo packVersionInfo) { + private record GameVersionInfo114(@SerializedName("pack_version") int packVersion) { } @JsonSerializable - @JsonAdapter(PackVersionInfoDeserializer.class) - private record PackVersionInfo(PackMcMeta.PackVersion resource) { + private record GameVersionInfo117(@SerializedName("pack_version") PackVersionInfo117 packVersionInfo) { } - private static final class PackVersionInfoDeserializer implements JsonDeserializer { - @Override - public PackVersionInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return new PackVersionInfo(PackMcMeta.PackVersion.fromJson(json.getAsJsonObject().get("resource"))); - } + @JsonSerializable + private record PackVersionInfo117(int resource, + @SerializedName("resource_major") int resourceMajor, + @SerializedName("resource_minor") int resourceMinor) { } } From 8d444243025971027febd6fde3c0a3fe4cbd2384 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 18 Dec 2025 22:50:10 +0800 Subject: [PATCH 15/61] =?UTF-8?q?=E4=BB=8E=E6=A8=A1=E7=BB=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=95=8C=E9=9D=A2=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 265 +++++++++++++++--- .../resourcepack/ResourcePackManager.java | 12 + 2 files changed, 231 insertions(+), 46 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index d46419ff6c..adaac612b5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -1,15 +1,16 @@ package org.jackhuang.hmcl.ui.versions; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXDialogLayout; -import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.*; +import javafx.animation.PauseTransition; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -20,6 +21,7 @@ import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; import javafx.stage.Stage; +import javafx.util.Duration; import org.jackhuang.hmcl.resourcepack.ResourcePackFile; import org.jackhuang.hmcl.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.setting.Profile; @@ -29,8 +31,11 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.ListPageBase; import org.jackhuang.hmcl.ui.SVG; +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.Holder; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; @@ -40,7 +45,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Locale; import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Pattern; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -145,8 +153,64 @@ private void onDownload() { Controllers.navigate(Controllers.getDownloadPage()); } + private void onDelete(ResourcePackFile file) { + try { + if (resourcePackManager != null) { + resourcePackManager.removeResourcePacks(file); + refresh(); + } + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete resource pack", e); + } + } + + private void onOpenFolder() { + if (resourcePackDirectory != null) { + FXUtils.openFolder(resourcePackDirectory); + } + } + + private void setSelectedEnabled(List selectedItems, boolean enabled) { + if (!warningShown) { + Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), + () -> { + warningShown = true; + setSelectedEnabled(selectedItems, enabled); + }, null); + } else { + for (ResourcePackInfoObject item : selectedItems) { + item.enabledProperty().set(enabled); + } + } + } + + private void removeSelected(List selectedItems) { + try { + if (resourcePackManager != null) { + if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { + refresh(); + } + } + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete resource packs", e); + } + } + private static final class ResourcePackListPageSkin extends SkinBase { private final JFXListView listView; + private final JFXTextField searchField = new JFXTextField(); + + private final TransitionPane toolbarPane = new TransitionPane(); + private final HBox searchBar = new HBox(); + private final HBox toolbarNormal = new HBox(); + private final HBox toolbarSelecting = new HBox(); + + private boolean isSearching; + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final ChangeListener holder; private ResourcePackListPageSkin(ResourcePackListPage control) { super(control); @@ -160,41 +224,162 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { listView = new JFXListView<>(); - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } + this.holder = FXUtils.onWeakChange(control.loadingProperty(), loading -> { + if (!loading) { + listView.scrollTo(0); } }); - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - HBox toolbar = new HBox(); - toolbar.setAlignment(Pos.CENTER_LEFT); - toolbar.setPickOnBounds(false); - toolbar.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), - createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), - createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload) - ); - root.getContent().add(toolbar); - - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.getStyleClass().add("large-spinner-pane"); - center.loadingProperty().bind(control.loadingProperty()); - - Holder lastCell = new Holder<>(); - listView.setCellFactory(x -> new ResourcePackListCell(listView, lastCell, control)); - Bindings.bindContent(listView.getItems(), control.getItems()); - - center.setContent(listView); - root.getContent().add(center); + + { + + // Toolbar Selecting + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + control.removeSelected(listView.getSelectionModel().getSelectedItems()); + }, null); + }), + createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), + createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + + // Search Bar + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(new Insets(0, 5, 0, 5)); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> search()); + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + pause.setRate(1); + pause.playFromStart(); + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); + + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + // Toolbar Normal + toolbarNormal.setAlignment(Pos.CENTER_LEFT); + toolbarNormal.setPickOnBounds(false); + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), + createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), + selectedItem -> { + if (selectedItem == null) + changeToolbar(isSearching ? searchBar : toolbarNormal); + else + changeToolbar(toolbarSelecting); + }); + root.getContent().add(toolbarPane); + + // Clear selection when pressing ESC + root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(control.loadingProperty()); + + Holder lastCell = new Holder<>(); + listView.setCellFactory(x -> new ResourcePackListCell(listView, lastCell, control)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), control.getItems()); + + listView.setOnContextMenuRequested(event -> { + ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ResourcePackInfoDialog(selectedItem)); + } + }); + + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + center.setContent(listView); + root.getContent().add(center); + } pane.getChildren().setAll(root); getChildren().setAll(pane); } + + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } + } + } + + private void search() { + isSearching = true; + + Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); + + String queryString = searchField.getText(); + if (StringUtils.isBlank(queryString)) { + listView.getItems().setAll(getSkinnable().getItems()); + } else { + listView.getItems().clear(); + + Predicate<@Nullable String> predicate; + if (queryString.startsWith("regex:")) { + try { + Pattern pattern = Pattern.compile(queryString.substring("regex:".length())); + predicate = s -> s != null && pattern.matcher(s).find(); + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return; + } + } else { + String lowerQueryString = queryString.toLowerCase(Locale.ROOT); + predicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerQueryString); + } + + // Do we need to search in the background thread? + for (ResourcePackInfoObject item : getSkinnable().getItems()) { + ResourcePackFile resourcePack = item.getFile(); + if (predicate.test(resourcePack.getFileName()) + || predicate.test(resourcePack.getName()) + || predicate.test(resourcePack.getFileName())) { + listView.getItems().add(item); + } + } + } + } } public static class ResourcePackInfoObject { @@ -329,7 +514,7 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), - () -> onDelete(file), null)); + () -> page.onDelete(file), null)); if (booleanProperty != null) { checkBox.selectedProperty().unbindBidirectional(booleanProperty); @@ -344,18 +529,6 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { } } } - - private void onDelete(ResourcePackFile file) { - try { - if (page.resourcePackManager != null) { - page.resourcePackManager.removeResourcePacks(file); - page.refresh(); - } - } catch (IOException e) { - Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resource pack", e); - } - } } private static final class ResourcePackInfoDialog extends JFXDialogLayout { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java index 4dcf7d7295..71848c0f07 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java @@ -284,6 +284,18 @@ public void removeResourcePacks(ResourcePackFile... resourcePacks) throws IOExce } } + public boolean removeResourcePacks(Iterable resourcePacks) throws IOException { + boolean b = false; + for (ResourcePackFile resourcePack : resourcePacks) { + if (resourcePack != null && resourcePack.manager == this) { + resourcePack.delete(); + resourcePackFiles.remove(resourcePack); + b = true; + } + } + return b; + } + public void enableResourcePack(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return; Map options = loadOptions(); From 00b377ad62a4fc94382ddfcfd8ce4310602d4919 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 18 Dec 2025 22:53:17 +0800 Subject: [PATCH 16/61] update --- .../ui/versions/ResourcePackListPage.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index adaac612b5..e2046c3746 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -186,16 +186,19 @@ private void setSelectedEnabled(List selectedItems, bool } private void removeSelected(List selectedItems) { - try { - if (resourcePackManager != null) { - if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { - refresh(); - } - } - } catch (IOException e) { - Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resource packs", e); - } + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), + () -> { + try { + if (resourcePackManager != null) { + if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { + refresh(); + } + } + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete resource packs", e); + } + }, null); } private static final class ResourcePackListPageSkin extends SkinBase { From 7d2ea79fe6a411e2b209b07869a90a873a73d2e4 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 19 Dec 2025 21:30:45 +0800 Subject: [PATCH 17/61] update i18n --- .../org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 4 ++-- HMCL/src/main/resources/assets/lang/I18N.properties | 4 +++- HMCL/src/main/resources/assets/lang/I18N_ar.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_es.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ja.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ru.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_uk.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_zh.properties | 4 +++- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 4 +++- 9 files changed, 21 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index e2046c3746..d3142230e0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -242,9 +242,9 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { control.removeSelected(listView.getSelectionModel().getSelectedItems()); }, null); }), - createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> + createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), - createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> + createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectAll()), diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 544c652594..8a7d9ecff8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -179,8 +179,10 @@ button.change_source=Change Download Source button.clear=Clear button.copy_and_exit=Copy and Exit button.delete=Delete +button.disable=Disable button.do_not_show_again=Don't show again button.edit=Edit +button.enable=Enable button.install=Install button.export=Export button.no=No @@ -1216,7 +1218,7 @@ resourcepack.download=Download resourcepack.download.title=Download Resource Pack - %1s resourcepack.manage=Resource Packs resourcepack.warning.invalid=Invalid pack metadata -resourcepack.warning.manipulate=Resource pack loading could be influenced by mods, causing unexpected behavior.\nYou may need to re-check after launching the game. +resourcepack.warning.manipulate=Resource pack loading could be influenced by mods, causing unexpected behavior.\nAre you sure to load/unload the resource pack? resourcepack.warning.missing_game_meta=Missing game metadata resourcepack.warning.missing_pack_meta=Missing pack metadata resourcepack.warning.too_new=For newer game versions diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index df5d2049a4..661d577acf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -179,8 +179,10 @@ button.change_source=تغيير مصدر التنزيل button.clear=مسح button.copy_and_exit=نسخ والخروج button.delete=حذف +button.disable=تعطيل button.do_not_show_again=لا تظهر مرة أخرى button.edit=تحرير +button.enable=تفعيل button.install=تثبيت button.export=تصدير button.no=لا diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index da5e1076af..68460acc95 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -180,8 +180,10 @@ button.change_source=Cambiar fuente de descarga button.clear=Limpiar button.copy_and_exit=Copiar y salir button.delete=Borrar +button.disable=Desactivar button.do_not_show_again=No volver a mostrar button.edit=Editar +button.enable=Activar button.install=Instalar button.export=Exportar button.no=No diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 9de9e11139..bf9f472a2c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -142,7 +142,9 @@ button.change_source=ダウンロードソースの変更 button.clear=クリア button.copy_and_exit=コピーして終了 button.delete=削除 +button.disable=無効にする button.edit=編集 +button.enable=有効にする button.install=インストール button.export=エクスポート button.no=いいえ diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index bfba621b2c..b8ca023337 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -181,7 +181,9 @@ button.change_source=Изменить источник скачивания button.clear=Очистить button.copy_and_exit=Скопировать и выйти button.delete=Удалить +button.disable=Отключить button.edit=Изменить +button.enable=Включить button.install=Установить button.export=Экспорт button.no=Нет diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index e935052b4c..4c23cb11ec 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -178,7 +178,9 @@ button.change_source=Змінити джерело завантаження button.clear=Очистити button.copy_and_exit=Копіювати та вийти button.delete=Видалити +button.disable=Вимкнути button.edit=Редагувати +button.enable=Увімкнути button.install=Встановити button.export=Експортувати button.no=Ні diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index d14e673b14..059bf3884c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -182,9 +182,11 @@ button.change_source=切換下載源 button.clear=清除 button.copy_and_exit=複製並退出 button.delete=刪除 +button.disable=停用 button.do_not_show_again=不再顯示 button.edit=編輯 button.install=安裝 +button.enable=啟用 button.export=匯出 button.no=否 button.ok=確定 @@ -1009,7 +1011,7 @@ resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包中繼資料無效 -resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n可能需要在遊戲內重新確認。 +resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要加載/卸載資源包嗎? resourcepack.warning.missing_game_meta=遊戲版本中繼資料缺失 resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 resourcepack.warning.too_new=為更新的遊戲版本製作 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 329c3a1244..3471abb24c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -190,9 +190,11 @@ button.change_source=切换下载源 button.clear=清除 button.copy_and_exit=复制并退出 button.delete=删除 +button.disable=禁用 button.do_not_show_again=不再显示 button.edit=修改 button.install=安装 +button.enable=启用 button.export=导出 button.no=否 button.ok=确定 @@ -1019,7 +1021,7 @@ resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 -resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n可能需要在游戏内重新确认。 +resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你確認要載入/解除安裝資源包嗎? resourcepack.warning.missing_game_meta=游戏版本元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 From 8ba09cf31be84d9a14f31ee650256d0189cb365c Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 20 Dec 2025 12:23:55 +0800 Subject: [PATCH 18/61] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=8C=85=E5=88=A0=E9=99=A4=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/versions/ResourcePackListPage.java | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index d3142230e0..11f865979f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -153,18 +153,6 @@ private void onDownload() { Controllers.navigate(Controllers.getDownloadPage()); } - private void onDelete(ResourcePackFile file) { - try { - if (resourcePackManager != null) { - resourcePackManager.removeResourcePacks(file); - refresh(); - } - } catch (IOException e) { - Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resource pack", e); - } - } - private void onOpenFolder() { if (resourcePackDirectory != null) { FXUtils.openFolder(resourcePackDirectory); @@ -434,9 +422,7 @@ private static final class ResourcePackListCell extends MDListCell listView, Holder getStyleClass().add("resource-pack-list-cell"); - this.page = page; - HBox root = new HBox(8); root.setPickOnBounds(false); root.setAlignment(Pos.CENTER_LEFT); @@ -478,13 +462,10 @@ public void fire() { btnReveal.getStyleClass().add("toggle-icon4"); btnReveal.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24)); - btnDelete.getStyleClass().add("toggle-icon4"); - btnDelete.setGraphic(FXUtils.limitingSize(SVG.DELETE_FOREVER.createIcon(24), 24, 24)); - btnInfo.getStyleClass().add("toggle-icon4"); btnInfo.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(24), 24, 24)); - root.getChildren().setAll(checkBox, imageView, content, btnReveal, btnDelete, btnInfo); + root.getChildren().setAll(checkBox, imageView, content, btnReveal, btnInfo); setSelectable(); @@ -515,10 +496,6 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(item))); - btnDelete.setOnAction(event -> - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), - () -> page.onDelete(file), null)); - if (booleanProperty != null) { checkBox.selectedProperty().unbindBidirectional(booleanProperty); } From 9f6f42bcd51b0fe7a3ab76eb5ee34d6951a3b7c2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 20 Dec 2025 13:05:05 +0800 Subject: [PATCH 19/61] =?UTF-8?q?=E6=B7=BB=E5=8A=A0cf/modrinth=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E6=A3=80=E6=B5=8B=20&=20fix=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/LocalizedRemoteModRepository.java | 5 +- .../hmcl/ui/versions/ModListPageSkin.java | 2 +- .../ui/versions/ResourcePackListPage.java | 62 ++++++++++++++++--- .../resources/assets/lang/I18N_zh.properties | 2 +- .../assets/lang/I18N_zh_CN.properties | 2 +- .../org/jackhuang/hmcl/mod/LocalModFile.java | 4 +- .../hmcl/mod/RemoteModRepository.java | 2 +- .../curse/CurseForgeRemoteModRepository.java | 3 +- .../modrinth/ModrinthModpackExportTask.java | 4 +- .../modrinth/ModrinthRemoteModRepository.java | 3 +- 10 files changed, 64 insertions(+), 25 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index 8a15898173..1e32436ee0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.ui.versions.ModTranslations; @@ -110,8 +109,8 @@ public Stream getCategories() throws IOException { } @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { - return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file); + public Optional getRemoteVersionByLocalFile(Path file) throws IOException { + return getBackedRemoteModRepository().getRemoteVersionByLocalFile(file); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index 33e92c9031..2c85ccdc6f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -463,7 +463,7 @@ final class ModInfoDialog extends JFXDialogLayout { RemoteModRepository repository = item.getValue(); JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile()); + Optional versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo().getFile()); if (versionOptional.isPresent()) { RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid()); FXUtils.runInFX(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 11f865979f..146819e585 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -22,6 +22,10 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Duration; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; +import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.resourcepack.ResourcePackFile; import org.jackhuang.hmcl.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.setting.Profile; @@ -35,6 +39,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.Holder; +import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; @@ -44,15 +49,14 @@ import java.lang.ref.WeakReference; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.Locale; -import java.util.Objects; +import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; 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.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -68,6 +72,9 @@ public final class ResourcePackListPage extends ListPageBase createDefaultSkin() { @Override public void loadVersion(Profile profile, String version) { + this.profile = profile; + this.instanceId = version; this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version); this.resourcePackDirectory = this.resourcePackManager.getResourcePackDirectory(); @@ -311,7 +320,7 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ResourcePackInfoDialog(selectedItem)); + Controllers.dialog(new ResourcePackInfoDialog(control, selectedItem)); } }); @@ -418,6 +427,8 @@ Image getIcon() { private static final class ResourcePackListCell extends MDListCell { private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); + private final ResourcePackListPage page; + private final JFXCheckBox checkBox; private final ImageView imageView = new ImageView(); private final TwoLineListItem content = new TwoLineListItem(); @@ -430,6 +441,7 @@ private static final class ResourcePackListCell extends MDListCell listView, Holder lastCell, ResourcePackListPage page) { super(listView, lastCell); + this.page = page; getStyleClass().add("resource-pack-list-cell"); @@ -494,7 +506,7 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getPath())); - btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(item))); + btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(this.page, item))); if (booleanProperty != null) { checkBox.selectedProperty().unbindBidirectional(booleanProperty); @@ -513,7 +525,9 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { private static final class ResourcePackInfoDialog extends JFXDialogLayout { - ResourcePackInfoDialog(ResourcePackInfoObject packInfoObject) { + ResourcePackInfoDialog(ResourcePackListPage page, ResourcePackInfoObject packInfoObject) { + ResourcePackFile pack = packInfoObject.getFile(); + HBox titleContainer = new HBox(); titleContainer.setSpacing(8); @@ -525,9 +539,9 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { FXUtils.limitSize(imageView, 40, 40); TwoLineListItem title = new TwoLineListItem(); - title.setTitle(packInfoObject.file.getName()); - title.setSubtitle(packInfoObject.file.getFileName()); - if (packInfoObject.file.getCompatibility() == ResourcePackFile.Compatibility.COMPATIBLE) { + title.setTitle(pack.getName()); + title.setSubtitle(pack.getFileName()); + if (pack.getCompatibility() == ResourcePackFile.Compatibility.COMPATIBLE) { title.addTag(i18n("resourcepack.compatible")); } else { title.addTagWarning(i18n(getWarningKey(packInfoObject.file.getCompatibility()))); @@ -536,7 +550,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title); setHeading(titleContainer); - Label description = new Label(Objects.requireNonNullElse(packInfoObject.file.getDescription(), "").toString()); + Label description = new Label(Objects.requireNonNullElse(pack.getDescription(), "").toString()); description.setWrapText(true); FXUtils.copyOnDoubleClick(description); @@ -553,6 +567,34 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { setBody(descriptionPane); + for (Pair item : Arrays.asList( + pair("mods.curseforge", CurseForgeRemoteModRepository.RESOURCE_PACKS), + pair("mods.modrinth", ModrinthRemoteModRepository.RESOURCE_PACKS) + )) { + RemoteModRepository repository = item.getValue(); + JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); + Task.runAsync(() -> { + Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getPath()); + if (versionOptional.isPresent()) { + RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid()); + FXUtils.runInFX(() -> { + button.setOnAction(e -> { + fireEvent(new DialogCloseEvent()); + Controllers.navigate(new DownloadPage( + repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), + remoteMod, + new Profile.ProfileVersion(page.profile, page.instanceId), + org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK + )); + }); + button.setDisable(false); + }); + } + }).start(); + button.setDisable(true); + getActions().add(button); + } + JFXButton okButton = new JFXButton(); okButton.getStyleClass().add("dialog-accept"); okButton.setText(i18n("button.ok")); diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 7e539a0cbb..e82ce2dc19 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1011,7 +1011,7 @@ resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包中繼資料無效 -resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要加載/卸載資源包嗎? +resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要載入/解除安裝資源包嗎? resourcepack.warning.missing_game_meta=遊戲版本中繼資料缺失 resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 resourcepack.warning.too_new=為更新的遊戲版本製作 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index a7ec1aa2ce..8492d038df 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1021,7 +1021,7 @@ resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 -resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你確認要載入/解除安裝資源包嗎? +resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你确认要加载/卸载安装包吗? resourcepack.warning.missing_game_meta=游戏版本元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index 5cb1a4403f..e835e8f16f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -175,8 +175,8 @@ public void disable() throws IOException { } public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException { - Optional currentVersion = repository.getRemoteVersionByLocalFile(this, file); - if (!currentVersion.isPresent()) return null; + Optional currentVersion = repository.getRemoteVersionByLocalFile(file); + if (currentVersion.isEmpty()) return null; List remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid()) .filter(version -> version.getGameVersions().contains(gameVersion)) .filter(version -> version.getLoaders().contains(getModLoaderType())) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index af6d8ad2ba..baa7ecad47 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -88,7 +88,7 @@ public int getTotalPages() { SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException; - Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException; + Optional getRemoteVersionByLocalFile(Path file) throws IOException; RemoteMod getModById(String id) throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 7bbaa76d0f..3e90fb2878 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -19,7 +19,6 @@ import com.google.gson.reflect.TypeToken; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.MurmurHash2; @@ -153,7 +152,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { + public Optional getRemoteVersionByLocalFile(Path file) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (InputStream stream = Files.newInputStream(file)) { byte[] buf = new byte[1024]; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index d2b2eddae5..0f7834b4b6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -61,14 +61,14 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) t Optional curseForgeVersion = Optional.empty(); try { - modrinthVersion = ModrinthRemoteModRepository.MODS.getRemoteVersionByLocalFile(localModFile, file); + modrinthVersion = ModrinthRemoteModRepository.MODS.getRemoteVersionByLocalFile(file); } catch (IOException e) { LOG.warning("Failed to get remote file from Modrinth for: " + file, e); } if (!info.isSkipCurseForgeRemoteFiles() && CurseForgeRemoteModRepository.isAvailable()) { try { - curseForgeVersion = CurseForgeRemoteModRepository.MODS.getRemoteVersionByLocalFile(localModFile, file); + curseForgeVersion = CurseForgeRemoteModRepository.MODS.getRemoteVersionByLocalFile(file); } catch (IOException e) { LOG.warning("Failed to get remote file from CurseForge for: " + file, e); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 574cf17b11..da173bf5b4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -20,7 +20,6 @@ import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; @@ -110,7 +109,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { + public Optional getRemoteVersionByLocalFile(Path file) throws IOException { String sha1 = DigestUtils.digestToString("SHA-1", file); try { From 9172c53fdd156e6ce4ef1ac658188f05dc1a5c1b Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 20 Dec 2025 17:01:00 +0800 Subject: [PATCH 20/61] Apply suggestions --- .../hmcl/ui/versions/ResourcePackListPage.java | 10 ++++++++-- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- .../main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 146819e585..07920be61e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -22,6 +22,7 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Duration; +import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; @@ -52,6 +53,7 @@ import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -240,7 +242,7 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { }, null); }), createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> @@ -372,9 +374,13 @@ private void search() { // Do we need to search in the background thread? for (ResourcePackInfoObject item : getSkinnable().getItems()) { ResourcePackFile resourcePack = item.getFile(); + var description = resourcePack.getDescription(); + var descriptionParts = description == null + ? Stream.empty() + : description.getParts().stream().map(LocalModFile.Description.Part::getText); if (predicate.test(resourcePack.getFileName()) || predicate.test(resourcePack.getName()) - || predicate.test(resourcePack.getFileName())) { + || descriptionParts.anyMatch(predicate)) { listView.getItems().add(item); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e82ce2dc19..926047f4b3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1011,7 +1011,7 @@ resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包中繼資料無效 -resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要載入/解除安裝資源包嗎? +resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要載入/卸載資源包嗎? resourcepack.warning.missing_game_meta=遊戲版本中繼資料缺失 resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 resourcepack.warning.too_new=為更新的遊戲版本製作 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 8492d038df..0275fe9298 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1021,7 +1021,7 @@ resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 -resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你确认要加载/卸载安装包吗? +resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你确认要加载/卸载资源包吗? resourcepack.warning.missing_game_meta=游戏版本元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 From 7ab6e6213c88f294ee78247c1d855e084c556bfd Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 20 Dec 2025 17:11:19 +0800 Subject: [PATCH 21/61] update i18n --- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 0434c41a99..df1a7e1b83 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1219,7 +1219,7 @@ resourcepack.download.title=Download Resource Pack - %1s resourcepack.manage=Resource Packs resourcepack.warning.invalid=Invalid pack metadata resourcepack.warning.manipulate=Resource pack loading could be influenced by mods, causing unexpected behavior.\nAre you sure to load/unload the resource pack? -resourcepack.warning.missing_game_meta=Missing game metadata +resourcepack.warning.missing_game_meta=Missing instance metadata resourcepack.warning.missing_pack_meta=Missing pack metadata resourcepack.warning.too_new=For newer game versions resourcepack.warning.too_old=For older game versions diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 926047f4b3..5bf5e77689 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1012,7 +1012,7 @@ resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包中繼資料無效 resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要載入/卸載資源包嗎? -resourcepack.warning.missing_game_meta=遊戲版本中繼資料缺失 +resourcepack.warning.missing_game_meta=目前實例中繼資料缺失 resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 resourcepack.warning.too_new=為更新的遊戲版本製作 resourcepack.warning.too_old=為更老的遊戲版本製作 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 0275fe9298..fa8ed0d464 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1022,7 +1022,7 @@ resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你确认要加载/卸载资源包吗? -resourcepack.warning.missing_game_meta=游戏版本元数据缺失 +resourcepack.warning.missing_game_meta=当前实例元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 resourcepack.warning.too_old=为更老的游戏版本打造 From c7010f9b787dabb3f81f8f7900c2392ded5a0723 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 11:07:19 +0800 Subject: [PATCH 22/61] Apply suggestions from code review Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5bf5e77689..1f0267bfa1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1011,7 +1011,7 @@ resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包中繼資料無效 -resourcepack.warning.manipulate=資源包的載入可能受到模組影響,實際效果可能與預期不符。\n你確認要載入/卸載資源包嗎? +resourcepack.warning.manipulate=資源包的載入可能會受到模組干擾,導致執行效果異常。\n你確定要啟用或停用此資源包嗎? resourcepack.warning.missing_game_meta=目前實例中繼資料缺失 resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 resourcepack.warning.too_new=為更新的遊戲版本製作 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index fa8ed0d464..446b6b686c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1021,7 +1021,7 @@ resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 -resourcepack.warning.manipulate=资源包的加载可能受到模组影响,实际效果可能与预期不符。\n你确认要加载/卸载资源包吗? +resourcepack.warning.manipulate=资源包的加载可能会受到模组干扰,导致运行效果异常。\n你确定要启用或禁用此资源包吗? resourcepack.warning.missing_game_meta=当前实例元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 From b0fca8d68bb49e977ef976c2cff1845fec7d2035 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 13:15:20 +0800 Subject: [PATCH 23/61] Apply suggestions from code review Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index df1a7e1b83..0fd3923bd4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1218,7 +1218,7 @@ resourcepack.download=Download resourcepack.download.title=Download Resource Pack - %1s resourcepack.manage=Resource Packs resourcepack.warning.invalid=Invalid pack metadata -resourcepack.warning.manipulate=Resource pack loading could be influenced by mods, causing unexpected behavior.\nAre you sure to load/unload the resource pack? +resourcepack.warning.manipulate=Resource pack activation could be influenced by mods, causing unexpected behavior.\nAre you sure to enable/disable the resource pack? resourcepack.warning.missing_game_meta=Missing instance metadata resourcepack.warning.missing_pack_meta=Missing pack metadata resourcepack.warning.too_new=For newer game versions From 78b5ce73489cb6b8920e7d89617855523973da40 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 17:36:33 +0800 Subject: [PATCH 24/61] =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8C=85=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...UpdatesTask.java => CheckUpdatesTask.java} | 17 ++-- .../hmcl/ui/versions/ModListPage.java | 9 +- .../ui/versions/ResourcePackListPage.java | 82 +++++++++++------ .../{ModUpdatesPage.java => UpdatesPage.java} | 58 ++++++------ .../jackhuang/hmcl/util/NativePatcher.java | 2 +- .../org/jackhuang/hmcl/mod/ILocalFile.java | 36 ++++++++ .../jackhuang/hmcl/mod/LocalFileManager.java | 83 +++++++++++++++++ .../org/jackhuang/hmcl/mod/LocalModFile.java | 41 +++------ .../org/jackhuang/hmcl/mod/ModManager.java | 89 +++++-------------- .../org/jackhuang/hmcl/mod/RemoteMod.java | 58 ++++++++++-- .../ResourcePackFile.java | 47 ++++++---- .../ResourcePackFolder.java | 9 +- .../ResourcePackManager.java | 55 ++++-------- .../ResourcePackZipFile.java | 21 ++++- 14 files changed, 379 insertions(+), 228 deletions(-) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/{ModCheckUpdatesTask.java => CheckUpdatesTask.java} (78%) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/{ModUpdatesPage.java => UpdatesPage.java} (86%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/{resourcepack => mod}/ResourcePackFile.java (72%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{resourcepack => mod}/ResourcePackFolder.java (86%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{resourcepack => mod}/ResourcePackManager.java (91%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/{resourcepack => mod}/ResourcePackZipFile.java (62%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java similarity index 78% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index 86806951c6..c41a347fcc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -17,19 +17,20 @@ */ package org.jackhuang.hmcl.ui.versions; -import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ILocalFile; import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.task.Task; import java.util.*; import java.util.stream.Collectors; -public class ModCheckUpdatesTask extends Task> { +public class CheckUpdatesTask extends Task> { private final String gameVersion; - private final Collection mods; - private final Collection>> dependents; + private final Collection mods; + private final Collection>> dependents; - public ModCheckUpdatesTask(String gameVersion, Collection mods) { + public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { this.gameVersion = gameVersion; this.mods = mods; @@ -37,7 +38,7 @@ public ModCheckUpdatesTask(String gameVersion, Collection mods) { .map(mod -> Arrays.stream(RemoteMod.Type.values()) .map(type -> - Task.supplyAsync(() -> mod.checkUpdates(gameVersion, type.getRemoteModRepository())) + Task.supplyAsync(() -> mod.checkUpdates(gameVersion, type.getRepoForType(repoType))) .setSignificance(TaskSignificance.MAJOR) .setName(String.format("%s (%s)", mod.getFileName(), type.name())).withCounter("update.checking") ) @@ -75,8 +76,8 @@ public void execute() throws Exception { .map(tasks -> tasks.stream() .filter(task -> task.getResult() != null) .map(Task::getResult) - .filter(modUpdate -> !modUpdate.getCandidates().isEmpty()) - .max(Comparator.comparing((LocalModFile.ModUpdate modUpdate) -> modUpdate.getCandidates().get(0).getDatePublished())) + .filter(modUpdate -> !modUpdate.candidates().isEmpty()) + .max(Comparator.comparing((ILocalFile.ModUpdate modUpdate) -> modUpdate.candidates().get(0).getDatePublished())) .orElse(null) ) .filter(Objects::nonNull) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index e91f1b3da9..44ee902835 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -103,8 +104,8 @@ private void loadMods(ModManager modManager) { CompletableFuture.supplyAsync(() -> { lock.lock(); try { - modManager.refreshMods(); - return modManager.getMods().stream().map(ModListPageSkin.ModInfoObject::new).toList(); + modManager.refresh(); + return modManager.getLocalFiles().stream().map(ModListPageSkin.ModInfoObject::new).toList(); } catch (IOException e) { throw new UncheckedIOException(e); } finally { @@ -234,7 +235,7 @@ public void checkUpdates() { .composeAsync(() -> { Optional gameVersion = profile.getRepository().getGameVersion(instanceId); if (gameVersion.isPresent()) { - return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods()); + return new CheckUpdatesTask(gameVersion.get(), modManager.getLocalFiles(), RemoteModRepository.Type.MOD); } return null; }) @@ -244,7 +245,7 @@ public void checkUpdates() { } else if (result.isEmpty()) { Controllers.dialog(i18n("mods.check_updates.empty")); } else { - Controllers.navigateForward(new ModUpdatesPage(modManager, result)); + Controllers.navigateForward(new UpdatesPage<>(modManager, result)); } }) .withStagesHint(Collections.singletonList("update.checking")), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 07920be61e..ed1528f0c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -27,8 +27,8 @@ import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; -import org.jackhuang.hmcl.resourcepack.ResourcePackFile; -import org.jackhuang.hmcl.resourcepack.ResourcePackManager; +import org.jackhuang.hmcl.mod.ResourcePackFile; +import org.jackhuang.hmcl.mod.ResourcePackManager; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; @@ -96,7 +97,7 @@ public void loadVersion(Profile profile, String version) { this.profile = profile; this.instanceId = version; this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version); - this.resourcePackDirectory = this.resourcePackManager.getResourcePackDirectory(); + this.resourcePackDirectory = this.resourcePackManager.getDirectory(); try { if (!Files.exists(resourcePackDirectory)) { @@ -118,8 +119,8 @@ public void refresh() { } setLoading(true); Task.supplyAsync(Schedulers.io(), () -> { - resourcePackManager.refreshResourcePacks(); - return resourcePackManager.getResourcePacks() + resourcePackManager.refresh(); + return resourcePackManager.getLocalFiles() .stream() .map(ResourcePackInfoObject::new) .toList(); @@ -185,19 +186,47 @@ private void setSelectedEnabled(List selectedItems, bool } private void removeSelected(List selectedItems) { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), - () -> { - try { - if (resourcePackManager != null) { - if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { - refresh(); + try { + if (resourcePackManager != null) { + if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { + refresh(); + } + } + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete resource packs", e); + } + } + + public void checkUpdates() { + Runnable action = () -> Controllers.taskDialog(Task + .composeAsync(() -> { + Optional gameVersion = profile.getRepository().getGameVersion(instanceId); + if (gameVersion.isPresent()) { + return new CheckUpdatesTask(gameVersion.get(), resourcePackManager.getLocalFiles(), RemoteModRepository.Type.RESOURCE_PACK); } - } - } catch (IOException e) { - Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resource packs", e); - } - }, null); + return null; + }) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null || result == null) { + Controllers.dialog(i18n("mods.check_updates.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); + } else if (result.isEmpty()) { + Controllers.dialog(i18n("mods.check_updates.empty")); + } else { + Controllers.navigateForward(new UpdatesPage<>(resourcePackManager, result)); + } + }) + .withStagesHint(Collections.singletonList("update.checking")), + i18n("mods.check_updates"), TaskCancellationAction.NORMAL); + + if (profile.getRepository().isModpack(instanceId)) { + Controllers.confirm( + i18n("mods.update_modpack_mod.warning"), null, + MessageDialogPane.MessageType.WARNING, + action, null); + } else { + action.run(); + } } private static final class ResourcePackListPageSkin extends SkinBase { @@ -283,6 +312,7 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), + createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, control::checkUpdates), createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) ); @@ -378,8 +408,8 @@ private void search() { var descriptionParts = description == null ? Stream.empty() : description.getParts().stream().map(LocalModFile.Description.Part::getText); - if (predicate.test(resourcePack.getFileName()) - || predicate.test(resourcePack.getName()) + if (predicate.test(resourcePack.getFileNameWithExtension()) + || predicate.test(resourcePack.getFileName()) || descriptionParts.anyMatch(predicate)) { listView.getItems().add(item); } @@ -417,7 +447,7 @@ Image getIcon() { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { image = new Image(inputStream, 64, 64, true, true); } catch (Exception e) { - LOG.warning("Failed to load resource pack icon " + file.getPath(), e); + LOG.warning("Failed to load resource pack icon " + file.getFile(), e); } } @@ -506,11 +536,11 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { ResourcePackFile file = item.getFile(); imageView.setImage(item.getIcon()); - content.setTitle(file.getName()); - content.setSubtitle(file.getFileName()); + content.setTitle(file.getFileName()); + content.setSubtitle(file.getFileNameWithExtension()); FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); - btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getPath())); + btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getFile())); btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(this.page, item))); @@ -545,8 +575,8 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { FXUtils.limitSize(imageView, 40, 40); TwoLineListItem title = new TwoLineListItem(); - title.setTitle(pack.getName()); - title.setSubtitle(pack.getFileName()); + title.setTitle(pack.getFileName()); + title.setSubtitle(pack.getFileNameWithExtension()); if (pack.getCompatibility() == ResourcePackFile.Compatibility.COMPATIBLE) { title.addTag(i18n("resourcepack.compatible")); } else { @@ -580,7 +610,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { RemoteModRepository repository = item.getValue(); JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getPath()); + Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile()); if (versionOptional.isPresent()) { RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid()); FXUtils.runInFX(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java similarity index 86% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java index abf8d68f6b..464a14bdf7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java @@ -29,9 +29,7 @@ import javafx.scene.control.TableView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.ModManager; -import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -59,14 +57,14 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class ModUpdatesPage extends BorderPane implements DecoratorPage { +public class UpdatesPage extends BorderPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("mods.check_updates"))); - private final ModManager modManager; + private final LocalFileManager modManager; private final ObservableList objects; @SuppressWarnings("unchecked") - public ModUpdatesPage(ModManager modManager, List updates) { + public UpdatesPage(LocalFileManager modManager, List updates) { this.modManager = modManager; getStyleClass().add("gray-background"); @@ -130,18 +128,18 @@ private void setupCellValueFactory(TableColumn column, F } private void updateMods() { - ModUpdateTask task = new ModUpdateTask( - modManager, + UpdateTask task = new UpdateTask( + modManager.getDirectory(), objects.stream() .filter(o -> o.enabled.get()) - .map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0))) - .collect(Collectors.toList())); + .map(object -> pair(object.data.localFile(), object.data.candidates().get(0))) + .toList()); Controllers.taskDialog( task.whenComplete(Schedulers.javafx(), exception -> { fireEvent(new PageCloseEvent()); if (!task.getFailedMods().isEmpty()) { Controllers.dialog(i18n("mods.check_updates.failed_download") + "\n" + - task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")), + task.getFailedMods().stream().map(ILocalFile::getFileName).collect(Collectors.joining("\n")), i18n("install.failed"), MessageDialogPane.MessageType.ERROR); } @@ -190,21 +188,21 @@ public ReadOnlyObjectWrapper stateProperty() { } private static final class ModUpdateObject { - final LocalModFile.ModUpdate data; + final ILocalFile.ModUpdate data; final BooleanProperty enabled = new SimpleBooleanProperty(); final StringProperty fileName = new SimpleStringProperty(); final StringProperty currentVersion = new SimpleStringProperty(); final StringProperty targetVersion = new SimpleStringProperty(); final StringProperty source = new SimpleStringProperty(); - public ModUpdateObject(LocalModFile.ModUpdate data) { + public ModUpdateObject(ILocalFile.ModUpdate data) { this.data = data; - enabled.set(!data.getLocalMod().getModManager().isDisabled(data.getLocalMod().getFile())); - fileName.set(data.getLocalMod().getFileName()); - currentVersion.set(data.getCurrentVersion().getVersion()); - targetVersion.set(data.getCandidates().get(0).getVersion()); - switch (data.getCurrentVersion().getSelf().getType()) { + enabled.set(!data.localFile().isDisabled()); + fileName.set(data.localFile().getFileName()); + currentVersion.set(data.currentVersion().getVersion()); + targetVersion.set(data.candidates().get(0).getVersion()); + switch (data.currentVersion().getSelf().getType()) { case CURSEFORGE: source.set(i18n("mods.curseforge")); break; @@ -274,19 +272,19 @@ public void setSource(String source) { } } - public static class ModUpdateTask extends Task { + public static class UpdateTask extends Task { private final Collection> dependents; - private final List failedMods = new ArrayList<>(); + private final List failedMods = new ArrayList<>(); - ModUpdateTask(ModManager modManager, List> mods) { + UpdateTask(Path modDirectory, List> mods) { setStage("mods.check_updates.confirm"); getProperties().put("total", mods.size()); this.dependents = new ArrayList<>(); - for (Pair mod : mods) { - LocalModFile local = mod.getKey(); + for (Pair mod : mods) { + ILocalFile local = mod.getKey(); RemoteMod.Version remote = mod.getValue(); - boolean isDisabled = local.getModManager().isDisabled(local.getFile()); + boolean isDisabled = local.isDisabled(); dependents.add(Task .runAsync(Schedulers.javafx(), () -> local.setOld(true)) @@ -297,7 +295,7 @@ public static class ModUpdateTask extends Task { var task = new FileDownloadTask( remote.getFile().getUrl(), - modManager.getModsDirectory().resolve(fileName)); + modDirectory.resolve(fileName)); task.setName(remote.getName()); return task; @@ -307,15 +305,21 @@ public static class ModUpdateTask extends Task { // restore state if failed local.setOld(false); if (isDisabled) - local.disable(); + local.markDisabled(); failedMods.add(local); + } else if (!local.keepOldFiles()) { + try { + local.delete(); + } catch (Exception e) { + // ignore + } } }) .withCounter("mods.check_updates.confirm")); } } - public List getFailedMods() { + public List getFailedMods() { return failedMods; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java index adb879e88b..24edd6326a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java @@ -164,7 +164,7 @@ public static Version patchNative(DefaultGameRepository repository, if (lwjglVersionChanged) { ModManager modManager = repository.getModManager(version.getId()); try { - for (LocalModFile mod : modManager.getMods()) { + for (LocalModFile mod : modManager.getLocalFiles()) { if ("sodium".equals(mod.getId())) { // https://github.com/CaffeineMC/sodium/issues/2561 javaArguments.add("-Dsodium.checks.issue2561=false"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java new file mode 100644 index 0000000000..c4d5347ffd --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java @@ -0,0 +1,36 @@ +package org.jackhuang.hmcl.mod; + +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public sealed interface ILocalFile permits LocalModFile, ResourcePackFile { + + Path getFile(); + + /// Without extension + String getFileName(); + + default boolean isDisabled() { + return FileUtils.getName(getFile()).endsWith(ModManager.DISABLED_EXTENSION); + } + + void markDisabled() throws IOException; + + void setOld(boolean old) throws IOException; + + boolean keepOldFiles(); + + void delete() throws IOException; + + @Nullable + ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException; + + record ModUpdate(ILocalFile localFile, RemoteMod.Version currentVersion, + List candidates) { + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java new file mode 100644 index 0000000000..4458816c9d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java @@ -0,0 +1,83 @@ +package org.jackhuang.hmcl.mod; + +import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.*; + +public abstract class LocalFileManager { + + public static final String DISABLED_EXTENSION = ".disabled"; + public static final String OLD_EXTENSION = ".old"; + + public static String getLocalFileName(Path file) { + return StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION); + } + + protected final Set localFiles = new LinkedHashSet<>(); + + protected final GameRepository repository; + protected final String id; + + public LocalFileManager(GameRepository gameRepository, String versionId) { + this.repository = gameRepository; + this.id = versionId; + } + + public GameRepository getRepository() { + return repository; + } + + public String getInstanceId() { + return id; + } + + public abstract Path getDirectory(); + + public abstract void refresh() throws IOException; + + public @Unmodifiable List getLocalFiles() throws IOException { + return List.copyOf(localFiles); + } + + public Path setOld(T modFile, boolean old) throws IOException { + Path newPath; + if (old) { + newPath = backupFile(modFile.getFile()); + localFiles.remove(modFile); + } else { + newPath = restoreFile(modFile.getFile()); + localFiles.add(modFile); + } + return newPath; + } + + private Path backupFile(Path file) throws IOException { + Path newPath = file.resolveSibling( + StringUtils.addSuffix( + StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION), + OLD_EXTENSION + ) + ); + if (Files.exists(file)) { + Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING); + } + return newPath; + } + + private Path restoreFile(Path file) throws IOException { + Path newPath = file.resolveSibling( + StringUtils.removeSuffix(FileUtils.getName(file), OLD_EXTENSION) + ); + if (Files.exists(file)) { + Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING); + } + return newPath; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index e835e8f16f..00ec278654 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -32,7 +32,7 @@ * * @author huangyuhui */ -public final class LocalModFile implements Comparable { +public final class LocalModFile implements Comparable, ILocalFile { private Path file; private final ModManager modManager; @@ -81,7 +81,7 @@ protected void invalidated() { } }; - fileName = FileUtils.getNameWithoutExtension(ModManager.getModName(file)); + fileName = FileUtils.getNameWithoutExtension(LocalFileManager.getLocalFileName(file)); if (isOld()) { mod.getOldFiles().add(this); @@ -150,10 +150,16 @@ public void setActive(boolean active) { activeProperty.set(active); } + @Override public String getFileName() { return fileName; } + @Override + public boolean keepOldFiles() { + return true; + } + public boolean isOld() { return modManager.isOld(file); } @@ -170,10 +176,15 @@ public void setOld(boolean old) throws IOException { } } - public void disable() throws IOException { + public void markDisabled() throws IOException { file = modManager.disableMod(file); } + public void delete() throws IOException { + modManager.removeMods(this); + } + + @Override public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException { Optional currentVersion = repository.getRemoteVersionByLocalFile(file); if (currentVersion.isEmpty()) return null; @@ -202,30 +213,6 @@ public int hashCode() { return Objects.hash(getFileName()); } - public static class ModUpdate { - private final LocalModFile localModFile; - private final RemoteMod.Version currentVersion; - private final List candidates; - - public ModUpdate(LocalModFile localModFile, RemoteMod.Version currentVersion, List candidates) { - this.localModFile = localModFile; - this.currentVersion = currentVersion; - this.candidates = candidates; - } - - public LocalModFile getLocalMod() { - return localModFile; - } - - public RemoteMod.Version getCurrentVersion() { - return currentVersion; - } - - public List getCandidates() { - return candidates; - } - } - public static class Description { private final List parts; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index 5296be3afc..6e5d8158dc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -35,7 +35,7 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ModManager { +public final class ModManager extends LocalFileManager { @FunctionalInterface private interface ModMetadataReader { LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException; @@ -61,28 +61,17 @@ private interface ModMetadataReader { READERS = map; } - private final GameRepository repository; - private final String id; - private final TreeSet localModFiles = new TreeSet<>(); private final HashMap, LocalMod> localMods = new HashMap<>(); private LibraryAnalyzer analyzer; private boolean loaded = false; public ModManager(GameRepository repository, String id) { - this.repository = repository; - this.id = id; + super(repository, id); } - public GameRepository getRepository() { - return repository; - } - - public String getInstanceId() { - return id; - } - - public Path getModsDirectory() { + @Override + public Path getDirectory() { return repository.getModsDirectory(id); } @@ -166,18 +155,19 @@ private void addModInfo(Path file) { } if (!modInfo.isOld()) { - localModFiles.add(modInfo); + localFiles.add(modInfo); } } - public void refreshMods() throws IOException { - localModFiles.clear(); + @Override + public void refresh() throws IOException { + localFiles.clear(); localMods.clear(); analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedPreservingPatchesVersion(id), null); - if (Files.isDirectory(getModsDirectory())) { - try (DirectoryStream modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) { + if (Files.isDirectory(getDirectory())) { + try (DirectoryStream modsDirectoryStream = Files.newDirectoryStream(getDirectory())) { for (Path subitem : modsDirectoryStream) { if (Files.isDirectory(subitem) && VersionNumber.isIntVersionNumber(FileUtils.getName(subitem))) { // If the folder name is game version, forge will search mod in this subdirectory @@ -195,10 +185,10 @@ public void refreshMods() throws IOException { loaded = true; } - public @Unmodifiable List getMods() throws IOException { + public @Unmodifiable List getLocalFiles() throws IOException { if (!loaded) - refreshMods(); - return List.copyOf(localModFiles); + refresh(); + return super.getLocalFiles(); } public void addMod(Path file) throws IOException { @@ -206,9 +196,9 @@ public void addMod(Path file) throws IOException { throw new IllegalArgumentException("File " + file + " is not a valid mod file."); if (!loaded) - refreshMods(); + refresh(); - Path modsDirectory = getModsDirectory(); + Path modsDirectory = getDirectory(); Files.createDirectories(modsDirectory); Path newFile = modsDirectory.resolve(file.getFileName()); @@ -227,7 +217,7 @@ public void rollback(LocalModFile from, LocalModFile to) throws IOException { if (!loaded) { throw new IllegalStateException("ModManager Not loaded"); } - if (!localModFiles.contains(from)) { + if (!localFiles.contains(from)) { throw new IllegalStateException("Rolling back an unknown mod " + from.getFileName()); } if (from.isOld()) { @@ -257,41 +247,6 @@ public void rollback(LocalModFile from, LocalModFile to) throws IOException { to.setActive(active); } - private Path backupMod(Path file) throws IOException { - Path newPath = file.resolveSibling( - StringUtils.addSuffix( - StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION), - OLD_EXTENSION - ) - ); - if (Files.exists(file)) { - Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING); - } - return newPath; - } - - private Path restoreMod(Path file) throws IOException { - Path newPath = file.resolveSibling( - StringUtils.removeSuffix(FileUtils.getName(file), OLD_EXTENSION) - ); - if (Files.exists(file)) { - Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING); - } - return newPath; - } - - public Path setOld(LocalModFile modFile, boolean old) throws IOException { - Path newPath; - if (old) { - newPath = backupMod(modFile.getFile()); - localModFiles.remove(modFile); - } else { - newPath = restoreMod(modFile.getFile()); - localModFiles.add(modFile); - } - return newPath; - } - public Path disableMod(Path file) throws IOException { if (isOld(file)) return file; // no need to disable an old mod. @@ -312,10 +267,6 @@ public Path enableMod(Path file) throws IOException { return enabled; } - public static String getModName(Path file) { - return StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION); - } - public boolean isOld(Path file) { return FileUtils.getName(file).endsWith(OLD_EXTENSION); } @@ -325,7 +276,7 @@ public boolean isDisabled(Path file) { } public static boolean isFileNameMod(Path file) { - String name = getModName(file); + String name = LocalFileManager.getLocalFileName(file); return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod"); } @@ -369,12 +320,12 @@ public static boolean isFileMod(Path modFile) { * @return true if the file exists */ public boolean hasSimpleMod(String fileName) { - return Files.exists(getModsDirectory().resolve(StringUtils.removeSuffix(fileName, DISABLED_EXTENSION))) - || Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION))); + return Files.exists(getDirectory().resolve(StringUtils.removeSuffix(fileName, DISABLED_EXTENSION))) + || Files.exists(getDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION))); } public Path getSimpleModPath(String fileName) { - return getModsDirectory().resolve(fileName); + return getDirectory().resolve(fileName); } public static final String DISABLED_EXTENSION = ".disabled"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index fdbc0724dc..53cbe69c39 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.time.Instant; @@ -193,17 +194,56 @@ public int hashCode() { } public enum Type { - CURSEFORGE(CurseForgeRemoteModRepository.MODS), - MODRINTH(ModrinthRemoteModRepository.MODS); - - private final RemoteModRepository remoteModRepository; - - public RemoteModRepository getRemoteModRepository() { - return this.remoteModRepository; + CURSEFORGE( + CurseForgeRemoteModRepository.MODS, + CurseForgeRemoteModRepository.RESOURCE_PACKS, + null, + CurseForgeRemoteModRepository.WORLDS, + CurseForgeRemoteModRepository.MODPACKS, + CurseForgeRemoteModRepository.CUSTOMIZATIONS + ), + MODRINTH( + ModrinthRemoteModRepository.MODS, + ModrinthRemoteModRepository.RESOURCE_PACKS, + ModrinthRemoteModRepository.SHADER_PACKS, + null, + ModrinthRemoteModRepository.MODPACKS, + null + ); + + public final RemoteModRepository modRepo; + public final RemoteModRepository resourcePackRepo; + public final RemoteModRepository shaderPackRepo; + public final RemoteModRepository worldRepo; + public final RemoteModRepository modpackRepo; + public final RemoteModRepository customizationRepo; + + @Nullable + public RemoteModRepository getRepoForType(RemoteModRepository.Type type) { + return switch (type) { + case MOD -> modRepo; + case RESOURCE_PACK -> resourcePackRepo; + case SHADER -> shaderPackRepo; + case WORLD -> worldRepo; + case MODPACK -> modpackRepo; + case CUSTOMIZATION -> customizationRepo; + }; } - Type(RemoteModRepository remoteModRepository) { - this.remoteModRepository = remoteModRepository; + Type( + RemoteModRepository modRepo, + RemoteModRepository resourcePackRepo, + RemoteModRepository shaderPackRepo, + RemoteModRepository worldRepo, + RemoteModRepository modpackRepo, + RemoteModRepository customizationRepo + ) { + this.modRepo = modRepo; + this.resourcePackRepo = resourcePackRepo; + this.shaderPackRepo = shaderPackRepo; + this.worldRepo = worldRepo; + this.modpackRepo = modpackRepo; + this.customizationRepo = customizationRepo; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java similarity index 72% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index 897d70705d..63b6334f34 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -1,8 +1,7 @@ -package org.jackhuang.hmcl.resourcepack; +package org.jackhuang.hmcl.mod; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Contract; @@ -14,9 +13,9 @@ import java.nio.file.Path; import java.util.Locale; -public sealed abstract class ResourcePackFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { +public sealed abstract class ResourcePackFile implements Comparable, ILocalFile permits ResourcePackFolder, ResourcePackZipFile { static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOException { - String fileName = path.getFileName().toString(); + String fileName = LocalFileManager.getLocalFileName(path); if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { return new ResourcePackZipFile(manager, path); } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { @@ -26,29 +25,31 @@ static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOE } protected final ResourcePackManager manager; - protected final Path path; + protected Path file; protected final String name; protected final String fileName; private ObjectProperty compatibility = null; - protected ResourcePackFile(ResourcePackManager manager, Path path) { + protected ResourcePackFile(ResourcePackManager manager, Path file) { this.manager = manager; - this.path = path; - this.fileName = FileUtils.getName(path); - this.name = FileUtils.getNameWithoutExtension(path); + this.file = file; + this.fileName = LocalFileManager.getLocalFileName(file); + this.name = FileUtils.getNameWithoutExtension(fileName); } - public Path getPath() { - return path; + @Override + public Path getFile() { + return file; } - public String getName() { + @Override + public String getFileName() { return name; } - public String getFileName() { - return getPath().getFileName().toString(); + public String getFileNameWithExtension() { + return getFile().getFileName().toString(); } public Compatibility getCompatibility() { @@ -70,6 +71,20 @@ public void setEnabled(boolean enabled) { } } + @Override + public boolean keepOldFiles() { + return false; + } + + @Override + public void setOld(boolean old) throws IOException { + this.file = manager.setOld(this, old); + } + + @Override + public void markDisabled() { + } + @Nullable @Contract(pure = true) public abstract PackMcMeta getMeta(); @@ -82,11 +97,9 @@ public LocalModFile.Description getDescription() { public abstract byte @Nullable [] getIcon(); - public abstract void delete() throws IOException; - @Override public int compareTo(@NotNull ResourcePackFile other) { - return this.getFileName().compareToIgnoreCase(other.getFileName()); + return this.getFileNameWithExtension().compareToIgnoreCase(other.getFileNameWithExtension()); } public enum Compatibility { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java similarity index 86% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java index da60950243..81026e3b6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java @@ -1,4 +1,4 @@ -package org.jackhuang.hmcl.resourcepack; +package org.jackhuang.hmcl.mod; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -48,6 +48,11 @@ public PackMcMeta getMeta() { @Override public void delete() throws IOException { - FileUtils.deleteDirectory(path); + FileUtils.deleteDirectory(file); + } + + @Override + public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) { + return null; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java similarity index 91% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index 71848c0f07..4c03a0240b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.resourcepack; +package org.jackhuang.hmcl.mod; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.game.GameRepository; @@ -39,7 +39,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ResourcePackManager { +public final class ResourcePackManager extends LocalFileManager { private static final List RESOURCE_PACK_VERSION_OLD = List.of( "13w24a", // 1 @@ -168,12 +168,9 @@ private static boolean isPackFormatInvalidate(int i, int j, int k) { } } - private final GameRepository repository; - private final String id; private final GameVersionNumber minecraftVersion; private final Path resourcePackDirectory; - private final TreeSet resourcePackFiles = new TreeSet<>(); private final Path optionsFile; private final @NotNull PackMcMeta.PackVersion requiredVersion; @@ -181,12 +178,12 @@ private static boolean isPackFormatInvalidate(int i, int j, int k) { private boolean loaded = false; public ResourcePackManager(GameRepository repository, String id) { - this.repository = repository; - this.id = id; + super(repository, id); this.minecraftVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(id)); this.resourcePackDirectory = this.repository.getResourcePackDirectory(this.id); this.optionsFile = repository.getRunDirectory(id).resolve("options.txt"); this.requiredVersion = getPackVersion(minecraftVersion, repository.getVersionJar(id)); + } @NotNull @@ -223,29 +220,23 @@ private void saveOptions(@NotNull Map options) { } } - public GameRepository getRepository() { - return repository; - } - - public String getInstanceId() { - return id; - } - public GameVersionNumber getMinecraftVersion() { return minecraftVersion; } - public Path getResourcePackDirectory() { + @Override + public Path getDirectory() { return resourcePackDirectory; } private void addResourcePackInfo(Path file) throws IOException { ResourcePackFile resourcePack = ResourcePackFile.parse(this, file); - if (resourcePack != null) resourcePackFiles.add(resourcePack); + if (resourcePack != null) localFiles.add(resourcePack); } - public void refreshResourcePacks() throws IOException { - resourcePackFiles.clear(); + @Override + public void refresh() throws IOException { + localFiles.clear(); if (Files.isDirectory(resourcePackDirectory)) { try (DirectoryStream directoryStream = Files.newDirectoryStream(resourcePackDirectory)) { @@ -257,15 +248,16 @@ public void refreshResourcePacks() throws IOException { loaded = true; } - public @Unmodifiable List getResourcePacks() throws IOException { + @Override + public @Unmodifiable List getLocalFiles() throws IOException { if (!loaded) - refreshResourcePacks(); - return List.copyOf(resourcePackFiles); + refresh(); + return super.getLocalFiles(); } public void importResourcePack(Path file) throws IOException { if (!loaded) - refreshResourcePacks(); + refresh(); Files.createDirectories(resourcePackDirectory); @@ -275,21 +267,12 @@ public void importResourcePack(Path file) throws IOException { addResourcePackInfo(newFile); } - public void removeResourcePacks(ResourcePackFile... resourcePacks) throws IOException { - for (ResourcePackFile resourcePack : resourcePacks) { - if (resourcePack != null && resourcePack.manager == this) { - resourcePack.delete(); - resourcePackFiles.remove(resourcePack); - } - } - } - public boolean removeResourcePacks(Iterable resourcePacks) throws IOException { boolean b = false; for (ResourcePackFile resourcePack : resourcePacks) { if (resourcePack != null && resourcePack.manager == this) { resourcePack.delete(); - resourcePackFiles.remove(resourcePack); + localFiles.remove(resourcePack); b = true; } } @@ -299,7 +282,7 @@ public boolean removeResourcePacks(Iterable resourcePacks) thr public void enableResourcePack(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return; Map options = loadOptions(); - String packId = "file/" + resourcePack.getFileName(); + String packId = "file/" + resourcePack.getFileNameWithExtension(); boolean b = false; List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); if (!resourcePacks.contains(packId)) { @@ -319,7 +302,7 @@ public void enableResourcePack(ResourcePackFile resourcePack) { public void disableResourcePack(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return; Map options = loadOptions(); - String packId = "file/" + resourcePack.getFileName(); + String packId = "file/" + resourcePack.getFileNameWithExtension(); boolean b = false; List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); if (resourcePacks.contains(packId)) { @@ -339,7 +322,7 @@ public void disableResourcePack(ResourcePackFile resourcePack) { public boolean isEnabled(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return false; Map options = loadOptions(); - String packId = "file/" + resourcePack.getFileName(); + String packId = "file/" + resourcePack.getFileNameWithExtension(); List resourcePacks = StringUtils.deserializeStringList(options.get("resourcePacks")); if (!resourcePacks.contains(packId)) return false; List incompatibleResourcePacks = StringUtils.deserializeStringList(options.get("incompatibleResourcePacks")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java similarity index 62% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java index 1ca3f33548..97546badfb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java @@ -1,4 +1,4 @@ -package org.jackhuang.hmcl.resourcepack; +package org.jackhuang.hmcl.mod; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -10,6 +10,10 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -56,7 +60,20 @@ public PackMcMeta getMeta() { @Override public void delete() throws IOException { - Files.deleteIfExists(path); + Files.deleteIfExists(file); + } + + @Override + public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException { + Optional currentVersion = repository.getRemoteVersionByLocalFile(file); + if (currentVersion.isEmpty()) return null; + List remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid()) + .filter(version -> version.getGameVersions().contains(gameVersion)) + .filter(version -> version.getDatePublished().compareTo(currentVersion.get().getDatePublished()) > 0) + .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) + .collect(Collectors.toList()); + if (remoteVersions.isEmpty()) return null; + return new ModUpdate(this, currentVersion.get(), remoteVersions); } } From 6709fbe99c39866927303199fd47cd26f627b669 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 17:38:04 +0800 Subject: [PATCH 25/61] update --- .../org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index c41a347fcc..e2ba324068 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -26,14 +26,9 @@ import java.util.stream.Collectors; public class CheckUpdatesTask extends Task> { - private final String gameVersion; - private final Collection mods; private final Collection>> dependents; public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { - this.gameVersion = gameVersion; - this.mods = mods; - dependents = mods.stream() .map(mod -> Arrays.stream(RemoteMod.Type.values()) From 07632bb95f246c20a1d36e0896cc9cd6e2e2e47e Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 17:46:15 +0800 Subject: [PATCH 26/61] update --- .../org/jackhuang/hmcl/ui/versions/UpdatesPage.java | 12 ++++++------ HMCL/src/main/resources/assets/lang/I18N.properties | 4 ++-- .../main/resources/assets/lang/I18N_zh.properties | 4 ++-- .../main/resources/assets/lang/I18N_zh_CN.properties | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java index 464a14bdf7..cce6b700e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java @@ -60,12 +60,12 @@ public class UpdatesPage extends BorderPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("mods.check_updates"))); - private final LocalFileManager modManager; + private final LocalFileManager localFileManager; private final ObservableList objects; @SuppressWarnings("unchecked") - public UpdatesPage(LocalFileManager modManager, List updates) { - this.modManager = modManager; + public UpdatesPage(LocalFileManager localFileManager, List updates) { + this.localFileManager = localFileManager; getStyleClass().add("gray-background"); @@ -112,7 +112,7 @@ public UpdatesPage(LocalFileManager modManager, List up exportListButton.setOnAction(e -> exportList()); JFXButton nextButton = FXUtils.newRaisedButton(i18n("mods.check_updates.confirm")); - nextButton.setOnAction(e -> updateMods()); + nextButton.setOnAction(e -> updateFiles()); JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel")); cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent())); @@ -127,9 +127,9 @@ private void setupCellValueFactory(TableColumn column, F column.setCellValueFactory(param -> mapper.apply(param.getValue())); } - private void updateMods() { + private void updateFiles() { UpdateTask task = new UpdateTask( - modManager.getDirectory(), + localFileManager.getDirectory(), objects.stream() .filter(o -> o.enabled.get()) .map(object -> pair(object.data.localFile(), object.data.candidates().get(0))) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 0fd3923bd4..064622c3a6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1062,11 +1062,11 @@ mods.category=Category mods.channel.alpha=Alpha mods.channel.beta=Beta mods.channel.release=Release -mods.check_updates=Mod update process +mods.check_updates=File update process mods.check_updates.button=Update mods.check_updates.confirm=Update mods.check_updates.current_version=Current Version -mods.check_updates.empty=All mods are up-to-date +mods.check_updates.empty=All files are up-to-date mods.check_updates.failed_check=Failed to check for updates. mods.check_updates.failed_download=Failed to download some files. mods.check_updates.file=File diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 1f0267bfa1..d6ede440f9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -859,11 +859,11 @@ mods.category=類別 mods.channel.alpha=Alpha mods.channel.beta=Beta mods.channel.release=Release -mods.check_updates=模組更新檢查 +mods.check_updates=檔案更新檢查 mods.check_updates.button=檢查更新 mods.check_updates.confirm=更新 mods.check_updates.current_version=目前版本 -mods.check_updates.empty=沒有需要更新的模組 +mods.check_updates.empty=沒有需要更新的檔案 mods.check_updates.failed_check=檢查更新失敗 mods.check_updates.failed_download=部分檔案下載失敗 mods.check_updates.file=檔案 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 446b6b686c..44d33a32f3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -869,11 +869,11 @@ mods.category=类别 mods.channel.alpha=快照版本 mods.channel.beta=测试版本 mods.channel.release=稳定版本 -mods.check_updates=模组更新检查 +mods.check_updates=文件更新检查 mods.check_updates.button=检查更新 mods.check_updates.confirm=更新 mods.check_updates.current_version=当前版本 -mods.check_updates.empty=没有需要更新的模组 +mods.check_updates.empty=没有需要更新的文件 mods.check_updates.failed_check=检查更新失败 mods.check_updates.failed_download=部分文件下载失败 mods.check_updates.file=文件 From 6a6a07dc16d1b55d492fd7b5f05b97556a271df6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 18:28:54 +0800 Subject: [PATCH 27/61] update --- HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index 6e5d8158dc..ac3e310373 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -276,7 +276,7 @@ public boolean isDisabled(Path file) { } public static boolean isFileNameMod(Path file) { - String name = LocalFileManager.getLocalFileName(file); + String name = getLocalFileName(file); return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod"); } From 827272e902c22951fd43811f1731195881ffaac2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 19:52:57 +0800 Subject: [PATCH 28/61] update --- .../jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 2 +- .../java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java | 3 ++- .../java/org/jackhuang/hmcl/mod/ResourcePackManager.java | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index ed1528f0c2..d713fefd3c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -84,7 +84,7 @@ public final class ResourcePackListPage extends ListPageBase file.getFileName().toString().endsWith(".zip"), this::addFiles); + FXUtils.applyDragListener(this, file -> Files.isDirectory(file) || file.getFileName().toString().endsWith(".zip"), this::addFiles); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java index cce6b700e8..396e686415 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java @@ -43,6 +43,7 @@ import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.CSVTable; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; @@ -310,7 +311,7 @@ public static class UpdateTask extends Task { } else if (!local.keepOldFiles()) { try { local.delete(); - } catch (Exception e) { + } catch (IOException e) { // ignore } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index 4c03a0240b..18e8dec8eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -262,7 +262,11 @@ public void importResourcePack(Path file) throws IOException { Files.createDirectories(resourcePackDirectory); Path newFile = resourcePackDirectory.resolve(file.getFileName()); - FileUtils.copyFile(file, newFile); + if (Files.isDirectory(file)) { + FileUtils.copyDirectory(file, newFile); + } else { + FileUtils.copyFile(file, newFile); + } addResourcePackInfo(newFile); } From 07a7b34e3bbf6705cdf9cd99f4b8aaa1fa582e53 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 21 Dec 2025 21:54:16 +0800 Subject: [PATCH 29/61] sync with #5016 --- .../hmcl/ui/versions/ResourcePackListPage.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index d713fefd3c..e3851b4223 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -6,7 +6,6 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -240,9 +239,6 @@ private static final class ResourcePackListPageSkin extends SkinBase holder; - private ResourcePackListPageSkin(ResourcePackListPage control) { super(control); @@ -255,12 +251,6 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { listView = new JFXListView<>(); - this.holder = FXUtils.onWeakChange(control.loadingProperty(), loading -> { - if (!loading) { - listView.scrollTo(0); - } - }); - { // Toolbar Selecting From 60ea149cd120c34ff04dfe57a92a842103df7274 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 06:56:17 +0800 Subject: [PATCH 30/61] update --- .../hmcl/ui/versions/CheckUpdatesTask.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index e2ba324068..f16cf06783 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -29,15 +29,21 @@ public class CheckUpdatesTask extends Task> { private final Collection>> dependents; public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { + Map repos = new LinkedHashMap<>(2); + for (RemoteMod.Type modType : RemoteMod.Type.values()) { + RemoteModRepository repo = modType.getRepoForType(repoType); + if (repo != null) { + repos.put(modType.name(), repo); + } + } dependents = mods.stream() .map(mod -> - Arrays.stream(RemoteMod.Type.values()) - .map(type -> - Task.supplyAsync(() -> mod.checkUpdates(gameVersion, type.getRepoForType(repoType))) + repos.entrySet().stream() + .map(entry -> + Task.supplyAsync(() -> mod.checkUpdates(gameVersion, entry.getValue())) .setSignificance(TaskSignificance.MAJOR) - .setName(String.format("%s (%s)", mod.getFileName(), type.name())).withCounter("update.checking") - ) - .collect(Collectors.toList()) + .setName(String.format("%s (%s)", mod.getFileName(), entry.getKey())).withCounter("update.checking") + ).toList() ) .collect(Collectors.toList()); From e94435189e02d11e151cb5f1a6a1c69b895a5b35 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 06:57:13 +0800 Subject: [PATCH 31/61] update --- .../src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index 00ec278654..fbb6f3ffa7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.util.io.FileUtils; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; @@ -181,7 +182,7 @@ public void markDisabled() throws IOException { } public void delete() throws IOException { - modManager.removeMods(this); + Files.deleteIfExists(file); } @Override From 29c34b9e871b2ed197166983825b8180c90e87ab Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 17:58:10 +0800 Subject: [PATCH 32/61] update --- .../src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java index 4458816c9d..d5c546eab5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java @@ -43,7 +43,7 @@ public String getInstanceId() { public abstract void refresh() throws IOException; public @Unmodifiable List getLocalFiles() throws IOException { - return List.copyOf(localFiles); + return localFiles.stream().sorted().toList(); } public Path setOld(T modFile, boolean old) throws IOException { From b42dac6212049ca287ef3680d8b74e3a371d56f7 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 18:03:38 +0800 Subject: [PATCH 33/61] update --- .../hmcl/ui/versions/CheckUpdatesTask.java | 10 ++--- .../hmcl/ui/versions/UpdatesPage.java | 25 ++++++----- .../org/jackhuang/hmcl/mod/ILocalFile.java | 36 --------------- .../org/jackhuang/hmcl/mod/LocalFile.java | 45 +++++++++++++++++++ .../jackhuang/hmcl/mod/LocalFileManager.java | 6 ++- .../org/jackhuang/hmcl/mod/LocalModFile.java | 8 +--- .../org/jackhuang/hmcl/mod/ModManager.java | 2 +- .../jackhuang/hmcl/mod/ResourcePackFile.java | 8 +--- 8 files changed, 73 insertions(+), 67 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index f16cf06783..16bd7016bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.ui.versions; -import org.jackhuang.hmcl.mod.ILocalFile; +import org.jackhuang.hmcl.mod.LocalFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.task.Task; @@ -25,10 +25,10 @@ import java.util.*; import java.util.stream.Collectors; -public class CheckUpdatesTask extends Task> { - private final Collection>> dependents; +public class CheckUpdatesTask extends Task> { + private final Collection>> dependents; - public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { + public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { Map repos = new LinkedHashMap<>(2); for (RemoteMod.Type modType : RemoteMod.Type.values()) { RemoteModRepository repo = modType.getRepoForType(repoType); @@ -78,7 +78,7 @@ public void execute() throws Exception { .filter(task -> task.getResult() != null) .map(Task::getResult) .filter(modUpdate -> !modUpdate.candidates().isEmpty()) - .max(Comparator.comparing((ILocalFile.ModUpdate modUpdate) -> modUpdate.candidates().get(0).getDatePublished())) + .max(Comparator.comparing((LocalFile.ModUpdate modUpdate) -> modUpdate.candidates().get(0).getDatePublished())) .orElse(null) ) .filter(Objects::nonNull) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java index 396e686415..a5701f5928 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java @@ -29,7 +29,10 @@ import javafx.scene.control.TableView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.mod.*; +import org.jackhuang.hmcl.mod.LocalFile; +import org.jackhuang.hmcl.mod.LocalFileManager; +import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -58,14 +61,14 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class UpdatesPage extends BorderPane implements DecoratorPage { +public class UpdatesPage extends BorderPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("mods.check_updates"))); private final LocalFileManager localFileManager; private final ObservableList objects; @SuppressWarnings("unchecked") - public UpdatesPage(LocalFileManager localFileManager, List updates) { + public UpdatesPage(LocalFileManager localFileManager, List updates) { this.localFileManager = localFileManager; getStyleClass().add("gray-background"); @@ -140,7 +143,7 @@ private void updateFiles() { fireEvent(new PageCloseEvent()); if (!task.getFailedMods().isEmpty()) { Controllers.dialog(i18n("mods.check_updates.failed_download") + "\n" + - task.getFailedMods().stream().map(ILocalFile::getFileName).collect(Collectors.joining("\n")), + task.getFailedMods().stream().map(LocalFile::getFileName).collect(Collectors.joining("\n")), i18n("install.failed"), MessageDialogPane.MessageType.ERROR); } @@ -189,14 +192,14 @@ public ReadOnlyObjectWrapper stateProperty() { } private static final class ModUpdateObject { - final ILocalFile.ModUpdate data; + final LocalFile.ModUpdate data; final BooleanProperty enabled = new SimpleBooleanProperty(); final StringProperty fileName = new SimpleStringProperty(); final StringProperty currentVersion = new SimpleStringProperty(); final StringProperty targetVersion = new SimpleStringProperty(); final StringProperty source = new SimpleStringProperty(); - public ModUpdateObject(ILocalFile.ModUpdate data) { + public ModUpdateObject(LocalFile.ModUpdate data) { this.data = data; enabled.set(!data.localFile().isDisabled()); @@ -275,15 +278,15 @@ public void setSource(String source) { public static class UpdateTask extends Task { private final Collection> dependents; - private final List failedMods = new ArrayList<>(); + private final List failedMods = new ArrayList<>(); - UpdateTask(Path modDirectory, List> mods) { + UpdateTask(Path modDirectory, List> mods) { setStage("mods.check_updates.confirm"); getProperties().put("total", mods.size()); this.dependents = new ArrayList<>(); - for (Pair mod : mods) { - ILocalFile local = mod.getKey(); + for (Pair mod : mods) { + LocalFile local = mod.getKey(); RemoteMod.Version remote = mod.getValue(); boolean isDisabled = local.isDisabled(); @@ -320,7 +323,7 @@ public static class UpdateTask extends Task { } } - public List getFailedMods() { + public List getFailedMods() { return failedMods; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java deleted file mode 100644 index c4d5347ffd..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ILocalFile.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jackhuang.hmcl.mod; - -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; - -public sealed interface ILocalFile permits LocalModFile, ResourcePackFile { - - Path getFile(); - - /// Without extension - String getFileName(); - - default boolean isDisabled() { - return FileUtils.getName(getFile()).endsWith(ModManager.DISABLED_EXTENSION); - } - - void markDisabled() throws IOException; - - void setOld(boolean old) throws IOException; - - boolean keepOldFiles(); - - void delete() throws IOException; - - @Nullable - ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException; - - record ModUpdate(ILocalFile localFile, RemoteMod.Version currentVersion, - List candidates) { - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java new file mode 100644 index 0000000000..4c49094ba1 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java @@ -0,0 +1,45 @@ +package org.jackhuang.hmcl.mod; + +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +/// Should implement `Comparable` +public sealed abstract class LocalFile permits LocalModFile, ResourcePackFile { + + private final boolean keepOldFiles; + + protected LocalFile(boolean keepOldFiles) { + this.keepOldFiles = keepOldFiles; + } + + public abstract Path getFile(); + + /// Without extension + public abstract String getFileName(); + + public boolean isDisabled() { + return FileUtils.getName(getFile()).endsWith(ModManager.DISABLED_EXTENSION); + } + + public abstract void markDisabled() throws IOException; + + public abstract void setOld(boolean old) throws IOException; + + public boolean keepOldFiles() { + return keepOldFiles; + } + + public abstract void delete() throws IOException; + + @Nullable + public abstract ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException; + + public record ModUpdate(LocalFile localFile, RemoteMod.Version currentVersion, + List candidates) { + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java index d5c546eab5..b115305e46 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java @@ -9,9 +9,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.*; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; -public abstract class LocalFileManager { +public abstract class LocalFileManager { public static final String DISABLED_EXTENSION = ".disabled"; public static final String OLD_EXTENSION = ".old"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index fbb6f3ffa7..0e30b28a8e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -33,7 +33,7 @@ * * @author huangyuhui */ -public final class LocalModFile implements Comparable, ILocalFile { +public final class LocalModFile extends LocalFile implements Comparable { private Path file; private final ModManager modManager; @@ -53,6 +53,7 @@ public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, } public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) { + super(true); this.modManager = modManager; this.mod = mod; this.file = file; @@ -156,11 +157,6 @@ public String getFileName() { return fileName; } - @Override - public boolean keepOldFiles() { - return true; - } - public boolean isOld() { return modManager.isOld(file); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index ac3e310373..59ec227ee9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -209,7 +209,7 @@ public void addMod(Path file) throws IOException { public void removeMods(LocalModFile... localModFiles) throws IOException { for (LocalModFile localModFile : localModFiles) { - Files.deleteIfExists(localModFile.getFile()); + localModFile.delete(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index 63b6334f34..c77f8f5539 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -13,7 +13,7 @@ import java.nio.file.Path; import java.util.Locale; -public sealed abstract class ResourcePackFile implements Comparable, ILocalFile permits ResourcePackFolder, ResourcePackZipFile { +public sealed abstract class ResourcePackFile extends LocalFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOException { String fileName = LocalFileManager.getLocalFileName(path); if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { @@ -32,6 +32,7 @@ static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOE private ObjectProperty compatibility = null; protected ResourcePackFile(ResourcePackManager manager, Path file) { + super(false); this.manager = manager; this.file = file; this.fileName = LocalFileManager.getLocalFileName(file); @@ -71,11 +72,6 @@ public void setEnabled(boolean enabled) { } } - @Override - public boolean keepOldFiles() { - return false; - } - @Override public void setOld(boolean old) throws IOException { this.file = manager.setOld(this, old); From 3204857df32103d78d3166aeda8805791429ffe9 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 18:13:28 +0800 Subject: [PATCH 34/61] update --- .../ui/versions/ResourcepackListPage.java | 632 ++++++++++++++++++ 1 file changed, 632 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java index e69de29bb2..aaaa470f43 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java @@ -0,0 +1,632 @@ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.*; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.util.Duration; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; +import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; +import org.jackhuang.hmcl.mod.ResourcePackFile; +import org.jackhuang.hmcl.mod.ResourcePackManager; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.ListPageBase; +import org.jackhuang.hmcl.ui.SVG; +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.Holder; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +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.Pair.pair; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class ResourcePackListPage extends ListPageBase implements VersionPage.VersionLoadable { + private static @Nullable String getWarningKey(ResourcePackFile.Compatibility compatibility) { + return switch (compatibility) { + case TOO_NEW -> "resourcepack.warning.too_new"; + case TOO_OLD -> "resourcepack.warning.too_old"; + case INVALID -> "resourcepack.warning.invalid"; + case MISSING_PACK_META -> "resourcepack.warning.missing_pack_meta"; + case MISSING_GAME_META -> "resourcepack.warning.missing_game_meta"; + default -> null; + }; + } + + private Profile profile; + private String instanceId; + + private Path resourcePackDirectory; + private ResourcePackManager resourcePackManager; + + private boolean warningShown = false; + + public ResourcePackListPage() { + FXUtils.applyDragListener(this, file -> Files.isDirectory(file) || file.getFileName().toString().endsWith(".zip"), this::addFiles); + } + + @Override + protected Skin createDefaultSkin() { + return new ResourcePackListPageSkin(this); + } + + @Override + public void loadVersion(Profile profile, String version) { + this.profile = profile; + this.instanceId = version; + this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version); + this.resourcePackDirectory = this.resourcePackManager.getDirectory(); + + try { + if (!Files.exists(resourcePackDirectory)) { + Files.createDirectories(resourcePackDirectory); + } + } catch (IOException e) { + LOG.warning("Failed to create resource pack directory" + resourcePackDirectory, e); + } + refresh(); + } + + public void refresh() { + if (resourcePackManager == null || !Files.isDirectory(resourcePackDirectory)) return; + setDisable(false); + if (resourcePackManager.getMinecraftVersion().compareTo(ResourcePackManager.LEAST_MC_VERSION) < 0) { + getItems().clear(); + setDisable(true); + return; + } + setLoading(true); + Task.supplyAsync(Schedulers.io(), () -> { + resourcePackManager.refresh(); + return resourcePackManager.getLocalFiles() + .stream() + .map(ResourcePackInfoObject::new) + .toList(); + }).whenComplete(Schedulers.javafx(), ((result, exception) -> { + if (exception == null) { + getItems().setAll(result); + } else { + LOG.warning("Failed to load resource packs", exception); + getItems().clear(); + } + setLoading(false); + })).start(); + } + + public void addFiles(List files) { + if (resourcePackManager == null) return; + + try { + for (Path file : files) { + resourcePackManager.importResourcePack(file); + } + } catch (IOException e) { + LOG.warning("Failed to add resource packs", e); + Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + + refresh(); + } + + public void onAddFiles() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("resourcepack.add")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); + List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); + if (files != null && !files.isEmpty()) { + addFiles(files); + } + } + + private void onDownload() { + Controllers.getDownloadPage().showResourcepackDownloads(); + Controllers.navigate(Controllers.getDownloadPage()); + } + + private void onOpenFolder() { + if (resourcePackDirectory != null) { + FXUtils.openFolder(resourcePackDirectory); + } + } + + private void setSelectedEnabled(List selectedItems, boolean enabled) { + if (!warningShown) { + Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), + () -> { + warningShown = true; + setSelectedEnabled(selectedItems, enabled); + }, null); + } else { + for (ResourcePackInfoObject item : selectedItems) { + item.enabledProperty().set(enabled); + } + } + } + + private void removeSelected(List selectedItems) { + try { + if (resourcePackManager != null) { + if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { + refresh(); + } + } + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete resource packs", e); + } + } + + public void checkUpdates() { + Runnable action = () -> Controllers.taskDialog(Task + .composeAsync(() -> { + Optional gameVersion = profile.getRepository().getGameVersion(instanceId); + if (gameVersion.isPresent()) { + return new CheckUpdatesTask(gameVersion.get(), resourcePackManager.getLocalFiles(), RemoteModRepository.Type.RESOURCE_PACK); + } + return null; + }) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null || result == null) { + Controllers.dialog(i18n("mods.check_updates.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); + } else if (result.isEmpty()) { + Controllers.dialog(i18n("mods.check_updates.empty")); + } else { + Controllers.navigateForward(new UpdatesPage<>(resourcePackManager, result)); + } + }) + .withStagesHint(Collections.singletonList("update.checking")), + i18n("mods.check_updates"), TaskCancellationAction.NORMAL); + + if (profile.getRepository().isModpack(instanceId)) { + Controllers.confirm( + i18n("mods.update_modpack_mod.warning"), null, + MessageDialogPane.MessageType.WARNING, + action, null); + } else { + action.run(); + } + } + + private static final class ResourcePackListPageSkin extends SkinBase { + private final JFXListView listView; + private final JFXTextField searchField = new JFXTextField(); + + private final TransitionPane toolbarPane = new TransitionPane(); + private final HBox searchBar = new HBox(); + private final HBox toolbarNormal = new HBox(); + private final HBox toolbarSelecting = new HBox(); + + private boolean isSearching; + + private ResourcePackListPageSkin(ResourcePackListPage control) { + super(control); + + StackPane pane = new StackPane(); + pane.setPadding(new Insets(10)); + pane.getStyleClass().addAll("notice-pane"); + + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + + listView = new JFXListView<>(); + + { + + // Toolbar Selecting + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + control.removeSelected(listView.getSelectionModel().getSelectedItems()); + }, null); + }), + createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), + createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> + control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + + // Search Bar + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(new Insets(0, 5, 0, 5)); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> search()); + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + pause.setRate(1); + pause.playFromStart(); + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); + + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + // Toolbar Normal + toolbarNormal.setAlignment(Pos.CENTER_LEFT); + toolbarNormal.setPickOnBounds(false); + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), + createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), + createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, control::checkUpdates), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), + selectedItem -> { + if (selectedItem == null) + changeToolbar(isSearching ? searchBar : toolbarNormal); + else + changeToolbar(toolbarSelecting); + }); + root.getContent().add(toolbarPane); + + // Clear selection when pressing ESC + root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(control.loadingProperty()); + + listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), control.getItems()); + + listView.setOnContextMenuRequested(event -> { + ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ResourcePackInfoDialog(control, selectedItem)); + } + }); + + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().setAll(root); + getChildren().setAll(pane); + } + + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } + } + } + + private void search() { + isSearching = true; + + Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); + + String queryString = searchField.getText(); + if (StringUtils.isBlank(queryString)) { + listView.getItems().setAll(getSkinnable().getItems()); + } else { + listView.getItems().clear(); + + Predicate<@Nullable String> predicate; + if (queryString.startsWith("regex:")) { + try { + Pattern pattern = Pattern.compile(queryString.substring("regex:".length())); + predicate = s -> s != null && pattern.matcher(s).find(); + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return; + } + } else { + String lowerQueryString = queryString.toLowerCase(Locale.ROOT); + predicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerQueryString); + } + + // Do we need to search in the background thread? + for (ResourcePackInfoObject item : getSkinnable().getItems()) { + ResourcePackFile resourcePack = item.getFile(); + var description = resourcePack.getDescription(); + var descriptionParts = description == null + ? Stream.empty() + : description.getParts().stream().map(LocalModFile.Description.Part::getText); + if (predicate.test(resourcePack.getFileNameWithExtension()) + || predicate.test(resourcePack.getFileName()) + || descriptionParts.anyMatch(predicate)) { + listView.getItems().add(item); + } + } + } + } + } + + public static class ResourcePackInfoObject { + private final ResourcePackFile file; + private final BooleanProperty enabled; + private WeakReference iconCache; + + public ResourcePackInfoObject(ResourcePackFile file) { + this.file = file; + this.enabled = new SimpleBooleanProperty(this, "enabled", file.isEnabled()); + this.enabled.addListener(__ -> file.setEnabled(enabled.get())); + } + + public ResourcePackFile getFile() { + return file; + } + + public BooleanProperty enabledProperty() { + return enabled; + } + + Image getIcon() { + Image image = null; + if (iconCache != null && (image = iconCache.get()) != null) { + return image; + } + byte[] iconData = file.getIcon(); + if (iconData != null) { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { + image = new Image(inputStream, 64, 64, true, true); + } catch (Exception e) { + LOG.warning("Failed to load resource pack icon " + file.getFile(), e); + } + } + + if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 || + (Math.abs(image.getWidth() - image.getHeight()) >= 1)) { + image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); + } + iconCache = new WeakReference<>(image); + return image; + } + } + + private static final class ResourcePackListCell extends MDListCell { + private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); + + private final ResourcePackListPage page; + + private final JFXCheckBox checkBox; + private final ImageView imageView = new ImageView(); + private final TwoLineListItem content = new TwoLineListItem(); + private final JFXButton btnReveal = new JFXButton(); + private final JFXButton btnInfo = new JFXButton(); + + private Tooltip warningTooltip = null; + + private BooleanProperty booleanProperty = null; + + public ResourcePackListCell(JFXListView listView, ResourcePackListPage page) { + super(listView); + this.page = page; + + getStyleClass().add("resource-pack-list-cell"); + + HBox root = new HBox(8); + root.setPickOnBounds(false); + root.setAlignment(Pos.CENTER_LEFT); + + checkBox = new JFXCheckBox() { + @Override + public void fire() { + if (!page.warningShown) { + Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), + () -> { + super.fire(); + page.warningShown = true; + }, null); + } else { + super.fire(); + } + } + }; + + imageView.setFitWidth(24); + imageView.setFitHeight(24); + imageView.setPreserveRatio(true); + + HBox.setHgrow(content, Priority.ALWAYS); + content.setMouseTransparent(true); + + btnReveal.getStyleClass().add("toggle-icon4"); + btnReveal.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24)); + + btnInfo.getStyleClass().add("toggle-icon4"); + btnInfo.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(24), 24, 24)); + + root.getChildren().setAll(checkBox, imageView, content, btnReveal, btnInfo); + + setSelectable(); + + StackPane.setMargin(root, new Insets(8)); + getContainer().getChildren().add(root); + } + + @Override + protected void updateControl(ResourcePackInfoObject item, boolean empty) { + pseudoClassStateChanged(WARNING, false); + if (warningTooltip != null) { + Tooltip.uninstall(this, warningTooltip); + warningTooltip = null; + } + + if (empty || item == null) { + return; + } + + ResourcePackFile file = item.getFile(); + imageView.setImage(item.getIcon()); + + content.setTitle(file.getFileName()); + content.setSubtitle(file.getFileNameWithExtension()); + + FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); + btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getFile())); + + btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(this.page, item))); + + if (booleanProperty != null) { + checkBox.selectedProperty().unbindBidirectional(booleanProperty); + } + checkBox.selectedProperty().bindBidirectional(booleanProperty = item.enabledProperty()); + + { + String warningKey = getWarningKey(file.getCompatibility()); + if (warningKey != null) { + pseudoClassStateChanged(WARNING, true); + FXUtils.installFastTooltip(this, warningTooltip = new Tooltip(i18n(warningKey))); + } + } + } + } + + private static final class ResourcePackInfoDialog extends JFXDialogLayout { + + ResourcePackInfoDialog(ResourcePackListPage page, ResourcePackInfoObject packInfoObject) { + ResourcePackFile pack = packInfoObject.getFile(); + + HBox titleContainer = new HBox(); + titleContainer.setSpacing(8); + + Stage stage = Controllers.getStage(); + maxWidthProperty().bind(stage.widthProperty().multiply(0.7)); + + ImageView imageView = new ImageView(); + imageView.setImage(packInfoObject.getIcon()); + FXUtils.limitSize(imageView, 40, 40); + + TwoLineListItem title = new TwoLineListItem(); + title.setTitle(pack.getFileName()); + title.setSubtitle(pack.getFileNameWithExtension()); + if (pack.getCompatibility() == ResourcePackFile.Compatibility.COMPATIBLE) { + title.addTag(i18n("resourcepack.compatible")); + } else { + title.addTagWarning(i18n(getWarningKey(packInfoObject.file.getCompatibility()))); + } + + titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title); + setHeading(titleContainer); + + Label description = new Label(Objects.requireNonNullElse(pack.getDescription(), "").toString()); + description.setWrapText(true); + FXUtils.copyOnDoubleClick(description); + + ScrollPane descriptionPane = new ScrollPane(description); + FXUtils.smoothScrolling(descriptionPane); + descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + descriptionPane.setFitToWidth(true); + description.heightProperty().addListener((obs, oldVal, newVal) -> { + double maxHeight = stage.getHeight() * 0.5; + double targetHeight = Math.min(newVal.doubleValue(), maxHeight); + descriptionPane.setPrefViewportHeight(targetHeight); + }); + + setBody(descriptionPane); + + for (Pair item : Arrays.asList( + pair("mods.curseforge", CurseForgeRemoteModRepository.RESOURCE_PACKS), + pair("mods.modrinth", ModrinthRemoteModRepository.RESOURCE_PACKS) + )) { + RemoteModRepository repository = item.getValue(); + JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); + Task.runAsync(() -> { + Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile()); + if (versionOptional.isPresent()) { + RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid()); + FXUtils.runInFX(() -> { + button.setOnAction(e -> { + fireEvent(new DialogCloseEvent()); + Controllers.navigate(new DownloadPage( + repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), + remoteMod, + new Profile.ProfileVersion(page.profile, page.instanceId), + org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK + )); + }); + button.setDisable(false); + }); + } + }).start(); + button.setDisable(true); + getActions().add(button); + } + + JFXButton okButton = new JFXButton(); + okButton.getStyleClass().add("dialog-accept"); + okButton.setText(i18n("button.ok")); + okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + getActions().add(okButton); + + onEscPressed(this, okButton::fire); + } + } +} From 423a021af25424b8afdea39766a43f9aaf383c83 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 18:16:18 +0800 Subject: [PATCH 35/61] update --- .../ui/versions/ResourcePackListPage.java | 8 +- .../ui/versions/ResourcepackListPage.java | 632 ------------------ 2 files changed, 3 insertions(+), 637 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index e3851b4223..775898eda5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -38,7 +38,6 @@ 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.Holder; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; @@ -333,8 +332,7 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { center.getStyleClass().add("large-spinner-pane"); center.loadingProperty().bind(control.loadingProperty()); - Holder lastCell = new Holder<>(); - listView.setCellFactory(x -> new ResourcePackListCell(listView, lastCell, control)); + listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); Bindings.bindContent(listView.getItems(), control.getItems()); @@ -465,8 +463,8 @@ private static final class ResourcePackListCell extends MDListCell listView, Holder lastCell, ResourcePackListPage page) { - super(listView, lastCell); + public ResourcePackListCell(JFXListView listView, ResourcePackListPage page) { + super(listView); this.page = page; getStyleClass().add("resource-pack-list-cell"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java deleted file mode 100644 index aaaa470f43..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java +++ /dev/null @@ -1,632 +0,0 @@ -package org.jackhuang.hmcl.ui.versions; - -import com.jfoenix.controls.*; -import javafx.animation.PauseTransition; -import javafx.application.Platform; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.css.PseudoClass; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import javafx.util.Duration; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.RemoteModRepository; -import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; -import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; -import org.jackhuang.hmcl.mod.ResourcePackFile; -import org.jackhuang.hmcl.mod.ResourcePackManager; -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.ListPageBase; -import org.jackhuang.hmcl.ui.SVG; -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.Holder; -import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.Nullable; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -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.Pair.pair; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class ResourcePackListPage extends ListPageBase implements VersionPage.VersionLoadable { - private static @Nullable String getWarningKey(ResourcePackFile.Compatibility compatibility) { - return switch (compatibility) { - case TOO_NEW -> "resourcepack.warning.too_new"; - case TOO_OLD -> "resourcepack.warning.too_old"; - case INVALID -> "resourcepack.warning.invalid"; - case MISSING_PACK_META -> "resourcepack.warning.missing_pack_meta"; - case MISSING_GAME_META -> "resourcepack.warning.missing_game_meta"; - default -> null; - }; - } - - private Profile profile; - private String instanceId; - - private Path resourcePackDirectory; - private ResourcePackManager resourcePackManager; - - private boolean warningShown = false; - - public ResourcePackListPage() { - FXUtils.applyDragListener(this, file -> Files.isDirectory(file) || file.getFileName().toString().endsWith(".zip"), this::addFiles); - } - - @Override - protected Skin createDefaultSkin() { - return new ResourcePackListPageSkin(this); - } - - @Override - public void loadVersion(Profile profile, String version) { - this.profile = profile; - this.instanceId = version; - this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version); - this.resourcePackDirectory = this.resourcePackManager.getDirectory(); - - try { - if (!Files.exists(resourcePackDirectory)) { - Files.createDirectories(resourcePackDirectory); - } - } catch (IOException e) { - LOG.warning("Failed to create resource pack directory" + resourcePackDirectory, e); - } - refresh(); - } - - public void refresh() { - if (resourcePackManager == null || !Files.isDirectory(resourcePackDirectory)) return; - setDisable(false); - if (resourcePackManager.getMinecraftVersion().compareTo(ResourcePackManager.LEAST_MC_VERSION) < 0) { - getItems().clear(); - setDisable(true); - return; - } - setLoading(true); - Task.supplyAsync(Schedulers.io(), () -> { - resourcePackManager.refresh(); - return resourcePackManager.getLocalFiles() - .stream() - .map(ResourcePackInfoObject::new) - .toList(); - }).whenComplete(Schedulers.javafx(), ((result, exception) -> { - if (exception == null) { - getItems().setAll(result); - } else { - LOG.warning("Failed to load resource packs", exception); - getItems().clear(); - } - setLoading(false); - })).start(); - } - - public void addFiles(List files) { - if (resourcePackManager == null) return; - - try { - for (Path file : files) { - resourcePackManager.importResourcePack(file); - } - } catch (IOException e) { - LOG.warning("Failed to add resource packs", e); - Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - } - - refresh(); - } - - public void onAddFiles() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(i18n("resourcepack.add")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); - List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); - if (files != null && !files.isEmpty()) { - addFiles(files); - } - } - - private void onDownload() { - Controllers.getDownloadPage().showResourcepackDownloads(); - Controllers.navigate(Controllers.getDownloadPage()); - } - - private void onOpenFolder() { - if (resourcePackDirectory != null) { - FXUtils.openFolder(resourcePackDirectory); - } - } - - private void setSelectedEnabled(List selectedItems, boolean enabled) { - if (!warningShown) { - Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), - () -> { - warningShown = true; - setSelectedEnabled(selectedItems, enabled); - }, null); - } else { - for (ResourcePackInfoObject item : selectedItems) { - item.enabledProperty().set(enabled); - } - } - } - - private void removeSelected(List selectedItems) { - try { - if (resourcePackManager != null) { - if (resourcePackManager.removeResourcePacks(selectedItems.stream().map(ResourcePackInfoObject::getFile).toList())) { - refresh(); - } - } - } catch (IOException e) { - Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); - LOG.warning("Failed to delete resource packs", e); - } - } - - public void checkUpdates() { - Runnable action = () -> Controllers.taskDialog(Task - .composeAsync(() -> { - Optional gameVersion = profile.getRepository().getGameVersion(instanceId); - if (gameVersion.isPresent()) { - return new CheckUpdatesTask(gameVersion.get(), resourcePackManager.getLocalFiles(), RemoteModRepository.Type.RESOURCE_PACK); - } - return null; - }) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null || result == null) { - Controllers.dialog(i18n("mods.check_updates.failed_check"), i18n("message.failed"), MessageDialogPane.MessageType.ERROR); - } else if (result.isEmpty()) { - Controllers.dialog(i18n("mods.check_updates.empty")); - } else { - Controllers.navigateForward(new UpdatesPage<>(resourcePackManager, result)); - } - }) - .withStagesHint(Collections.singletonList("update.checking")), - i18n("mods.check_updates"), TaskCancellationAction.NORMAL); - - if (profile.getRepository().isModpack(instanceId)) { - Controllers.confirm( - i18n("mods.update_modpack_mod.warning"), null, - MessageDialogPane.MessageType.WARNING, - action, null); - } else { - action.run(); - } - } - - private static final class ResourcePackListPageSkin extends SkinBase { - private final JFXListView listView; - private final JFXTextField searchField = new JFXTextField(); - - private final TransitionPane toolbarPane = new TransitionPane(); - private final HBox searchBar = new HBox(); - private final HBox toolbarNormal = new HBox(); - private final HBox toolbarSelecting = new HBox(); - - private boolean isSearching; - - private ResourcePackListPageSkin(ResourcePackListPage control) { - super(control); - - StackPane pane = new StackPane(); - pane.setPadding(new Insets(10)); - pane.getStyleClass().addAll("notice-pane"); - - ComponentList root = new ComponentList(); - root.getStyleClass().add("no-padding"); - - listView = new JFXListView<>(); - - { - - // Toolbar Selecting - toolbarSelecting.getChildren().setAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - control.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }), - createToolbarButton2(i18n("button.enable"), SVG.CHECK, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), true)), - createToolbarButton2(i18n("button.disable"), SVG.CLOSE, () -> - control.setSelectedEnabled(listView.getSelectionModel().getSelectedItems(), false)), - createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> - listView.getSelectionModel().selectAll()), - createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> - listView.getSelectionModel().clearSelection()) - ); - - // Search Bar - searchBar.setAlignment(Pos.CENTER); - searchBar.setPadding(new Insets(0, 5, 0, 5)); - searchField.setPromptText(i18n("search")); - HBox.setHgrow(searchField, Priority.ALWAYS); - PauseTransition pause = new PauseTransition(Duration.millis(100)); - pause.setOnFinished(e -> search()); - searchField.textProperty().addListener((observable, oldValue, newValue) -> { - pause.setRate(1); - pause.playFromStart(); - }); - - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, - () -> { - changeToolbar(toolbarNormal); - - isSearching = false; - searchField.clear(); - Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); - }); - - onEscPressed(searchField, closeSearchBar::fire); - - searchBar.getChildren().setAll(searchField, closeSearchBar); - - // Toolbar Normal - toolbarNormal.setAlignment(Pos.CENTER_LEFT); - toolbarNormal.setPickOnBounds(false); - toolbarNormal.getChildren().setAll( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), - createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles), - createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), - createToolbarButton2(i18n("mods.check_updates.button"), SVG.UPDATE, control::checkUpdates), - createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), - createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) - ); - - FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), - selectedItem -> { - if (selectedItem == null) - changeToolbar(isSearching ? searchBar : toolbarNormal); - else - changeToolbar(toolbarSelecting); - }); - root.getContent().add(toolbarPane); - - // Clear selection when pressing ESC - root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.getCode() == KeyCode.ESCAPE) { - if (listView.getSelectionModel().getSelectedItem() != null) { - listView.getSelectionModel().clearSelection(); - e.consume(); - } - } - }); - } - - { - SpinnerPane center = new SpinnerPane(); - ComponentList.setVgrow(center, Priority.ALWAYS); - center.getStyleClass().add("large-spinner-pane"); - center.loadingProperty().bind(control.loadingProperty()); - - listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - Bindings.bindContent(listView.getItems(), control.getItems()); - - listView.setOnContextMenuRequested(event -> { - ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); - if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { - listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ResourcePackInfoDialog(control, selectedItem)); - } - }); - - ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); - - center.setContent(listView); - root.getContent().add(center); - } - - pane.getChildren().setAll(root); - getChildren().setAll(pane); - } - - private void changeToolbar(HBox newToolbar) { - Node oldToolbar = toolbarPane.getCurrentNode(); - if (newToolbar != oldToolbar) { - toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); - if (newToolbar == searchBar) { - Platform.runLater(searchField::requestFocus); - } - } - } - - private void search() { - isSearching = true; - - Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); - - String queryString = searchField.getText(); - if (StringUtils.isBlank(queryString)) { - listView.getItems().setAll(getSkinnable().getItems()); - } else { - listView.getItems().clear(); - - Predicate<@Nullable String> predicate; - if (queryString.startsWith("regex:")) { - try { - Pattern pattern = Pattern.compile(queryString.substring("regex:".length())); - predicate = s -> s != null && pattern.matcher(s).find(); - } catch (Throwable e) { - LOG.warning("Illegal regular expression", e); - return; - } - } else { - String lowerQueryString = queryString.toLowerCase(Locale.ROOT); - predicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerQueryString); - } - - // Do we need to search in the background thread? - for (ResourcePackInfoObject item : getSkinnable().getItems()) { - ResourcePackFile resourcePack = item.getFile(); - var description = resourcePack.getDescription(); - var descriptionParts = description == null - ? Stream.empty() - : description.getParts().stream().map(LocalModFile.Description.Part::getText); - if (predicate.test(resourcePack.getFileNameWithExtension()) - || predicate.test(resourcePack.getFileName()) - || descriptionParts.anyMatch(predicate)) { - listView.getItems().add(item); - } - } - } - } - } - - public static class ResourcePackInfoObject { - private final ResourcePackFile file; - private final BooleanProperty enabled; - private WeakReference iconCache; - - public ResourcePackInfoObject(ResourcePackFile file) { - this.file = file; - this.enabled = new SimpleBooleanProperty(this, "enabled", file.isEnabled()); - this.enabled.addListener(__ -> file.setEnabled(enabled.get())); - } - - public ResourcePackFile getFile() { - return file; - } - - public BooleanProperty enabledProperty() { - return enabled; - } - - Image getIcon() { - Image image = null; - if (iconCache != null && (image = iconCache.get()) != null) { - return image; - } - byte[] iconData = file.getIcon(); - if (iconData != null) { - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { - image = new Image(inputStream, 64, 64, true, true); - } catch (Exception e) { - LOG.warning("Failed to load resource pack icon " + file.getFile(), e); - } - } - - if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 || - (Math.abs(image.getWidth() - image.getHeight()) >= 1)) { - image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); - } - iconCache = new WeakReference<>(image); - return image; - } - } - - private static final class ResourcePackListCell extends MDListCell { - private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); - - private final ResourcePackListPage page; - - private final JFXCheckBox checkBox; - private final ImageView imageView = new ImageView(); - private final TwoLineListItem content = new TwoLineListItem(); - private final JFXButton btnReveal = new JFXButton(); - private final JFXButton btnInfo = new JFXButton(); - - private Tooltip warningTooltip = null; - - private BooleanProperty booleanProperty = null; - - public ResourcePackListCell(JFXListView listView, ResourcePackListPage page) { - super(listView); - this.page = page; - - getStyleClass().add("resource-pack-list-cell"); - - HBox root = new HBox(8); - root.setPickOnBounds(false); - root.setAlignment(Pos.CENTER_LEFT); - - checkBox = new JFXCheckBox() { - @Override - public void fire() { - if (!page.warningShown) { - Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), - () -> { - super.fire(); - page.warningShown = true; - }, null); - } else { - super.fire(); - } - } - }; - - imageView.setFitWidth(24); - imageView.setFitHeight(24); - imageView.setPreserveRatio(true); - - HBox.setHgrow(content, Priority.ALWAYS); - content.setMouseTransparent(true); - - btnReveal.getStyleClass().add("toggle-icon4"); - btnReveal.setGraphic(FXUtils.limitingSize(SVG.FOLDER.createIcon(24), 24, 24)); - - btnInfo.getStyleClass().add("toggle-icon4"); - btnInfo.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(24), 24, 24)); - - root.getChildren().setAll(checkBox, imageView, content, btnReveal, btnInfo); - - setSelectable(); - - StackPane.setMargin(root, new Insets(8)); - getContainer().getChildren().add(root); - } - - @Override - protected void updateControl(ResourcePackInfoObject item, boolean empty) { - pseudoClassStateChanged(WARNING, false); - if (warningTooltip != null) { - Tooltip.uninstall(this, warningTooltip); - warningTooltip = null; - } - - if (empty || item == null) { - return; - } - - ResourcePackFile file = item.getFile(); - imageView.setImage(item.getIcon()); - - content.setTitle(file.getFileName()); - content.setSubtitle(file.getFileNameWithExtension()); - - FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); - btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getFile())); - - btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(this.page, item))); - - if (booleanProperty != null) { - checkBox.selectedProperty().unbindBidirectional(booleanProperty); - } - checkBox.selectedProperty().bindBidirectional(booleanProperty = item.enabledProperty()); - - { - String warningKey = getWarningKey(file.getCompatibility()); - if (warningKey != null) { - pseudoClassStateChanged(WARNING, true); - FXUtils.installFastTooltip(this, warningTooltip = new Tooltip(i18n(warningKey))); - } - } - } - } - - private static final class ResourcePackInfoDialog extends JFXDialogLayout { - - ResourcePackInfoDialog(ResourcePackListPage page, ResourcePackInfoObject packInfoObject) { - ResourcePackFile pack = packInfoObject.getFile(); - - HBox titleContainer = new HBox(); - titleContainer.setSpacing(8); - - Stage stage = Controllers.getStage(); - maxWidthProperty().bind(stage.widthProperty().multiply(0.7)); - - ImageView imageView = new ImageView(); - imageView.setImage(packInfoObject.getIcon()); - FXUtils.limitSize(imageView, 40, 40); - - TwoLineListItem title = new TwoLineListItem(); - title.setTitle(pack.getFileName()); - title.setSubtitle(pack.getFileNameWithExtension()); - if (pack.getCompatibility() == ResourcePackFile.Compatibility.COMPATIBLE) { - title.addTag(i18n("resourcepack.compatible")); - } else { - title.addTagWarning(i18n(getWarningKey(packInfoObject.file.getCompatibility()))); - } - - titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title); - setHeading(titleContainer); - - Label description = new Label(Objects.requireNonNullElse(pack.getDescription(), "").toString()); - description.setWrapText(true); - FXUtils.copyOnDoubleClick(description); - - ScrollPane descriptionPane = new ScrollPane(description); - FXUtils.smoothScrolling(descriptionPane); - descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - descriptionPane.setFitToWidth(true); - description.heightProperty().addListener((obs, oldVal, newVal) -> { - double maxHeight = stage.getHeight() * 0.5; - double targetHeight = Math.min(newVal.doubleValue(), maxHeight); - descriptionPane.setPrefViewportHeight(targetHeight); - }); - - setBody(descriptionPane); - - for (Pair item : Arrays.asList( - pair("mods.curseforge", CurseForgeRemoteModRepository.RESOURCE_PACKS), - pair("mods.modrinth", ModrinthRemoteModRepository.RESOURCE_PACKS) - )) { - RemoteModRepository repository = item.getValue(); - JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); - Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile()); - if (versionOptional.isPresent()) { - RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid()); - FXUtils.runInFX(() -> { - button.setOnAction(e -> { - fireEvent(new DialogCloseEvent()); - Controllers.navigate(new DownloadPage( - repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), - remoteMod, - new Profile.ProfileVersion(page.profile, page.instanceId), - org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK - )); - }); - button.setDisable(false); - }); - } - }).start(); - button.setDisable(true); - getActions().add(button); - } - - JFXButton okButton = new JFXButton(); - okButton.getStyleClass().add("dialog-accept"); - okButton.setText(i18n("button.ok")); - okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); - getActions().add(okButton); - - onEscPressed(this, okButton::fire); - } - } -} From 194ce5f6ae8cade7b95f59b1e040bd137468838b Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 24 Dec 2025 22:37:43 +0800 Subject: [PATCH 36/61] Apply suggestions --- .../ui/versions/ResourcePackListPage.java | 2 +- .../org/jackhuang/hmcl/mod/LocalFile.java | 2 +- .../org/jackhuang/hmcl/mod/LocalModFile.java | 2 + .../hmcl/mod/ResourcePackManager.java | 40 +++++++++---------- .../hmcl/mod/modinfo/PackMcMeta.java | 2 +- .../org/jackhuang/hmcl/util/StringUtils.java | 23 ++++++----- .../jackhuang/hmcl/util/StringUtilsTest.java | 28 +++++++++++++ 7 files changed, 65 insertions(+), 34 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 775898eda5..1e650be4a4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -102,7 +102,7 @@ public void loadVersion(Profile profile, String version) { Files.createDirectories(resourcePackDirectory); } } catch (IOException e) { - LOG.warning("Failed to create resource pack directory" + resourcePackDirectory, e); + LOG.warning("Failed to create resource pack directory: " + resourcePackDirectory, e); } refresh(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java index 4c49094ba1..3690b32237 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java @@ -22,7 +22,7 @@ protected LocalFile(boolean keepOldFiles) { public abstract String getFileName(); public boolean isDisabled() { - return FileUtils.getName(getFile()).endsWith(ModManager.DISABLED_EXTENSION); + return FileUtils.getName(getFile()).endsWith(LocalFileManager.DISABLED_EXTENSION); } public abstract void markDisabled() throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index 0e30b28a8e..3b77022399 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -173,10 +173,12 @@ public void setOld(boolean old) throws IOException { } } + @Override public void markDisabled() throws IOException { file = modManager.disableMod(file); } + @Override public void delete() throws IOException { Files.deleteIfExists(file); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index 18e8dec8eb..f8be5faf89 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -120,7 +120,7 @@ public static VersionRange getResourcePackVersionRangeNe return VersionRange.empty(); } - if (!packFormatUnspecified && isPackFormatInvalidate(minMajor, maxMajor, packInfo.packFormat())) { + if (!packFormatUnspecified && isPackFormatInvalid(minMajor, maxMajor, packInfo.packFormat())) { return VersionRange.empty(); } } else { @@ -135,7 +135,7 @@ public static VersionRange getResourcePackVersionRangeNe return VersionRange.empty(); } if (packFormatUnspecified) return VersionRange.empty(); - if (isPackFormatInvalidate(minMajor, maxMajor, packInfo.packFormat())) return VersionRange.empty(); + if (isPackFormatInvalid(minMajor, maxMajor, packInfo.packFormat())) return VersionRange.empty(); } return VersionRange.between(packInfo.minPackVersion(), packInfo.maxPackVersion()); @@ -147,20 +147,20 @@ public static VersionRange getResourcePackVersionRangeNe return VersionRange.empty(); } else { if (packFormatUnspecified) return VersionRange.empty(); - if (isPackFormatInvalidate(min, max, packInfo.packFormat())) return VersionRange.empty(); + if (isPackFormatInvalid(min, max, packInfo.packFormat())) return VersionRange.empty(); } return VersionRange.between(supportedFormats.getMin(), supportedFormats.getMax()); } else if (!packFormatUnspecified) { int packFormat = packInfo.packFormat(); PackMcMeta.PackVersion packVersion = new PackMcMeta.PackVersion(packFormat, 0); - return packFormat > 64 ? VersionRange.empty() : VersionRange.between(packVersion, packVersion); + return packFormat > 64 ? VersionRange.empty() : VersionRange.only(packVersion); } return VersionRange.empty(); } @Contract(pure = true) - private static boolean isPackFormatInvalidate(int i, int j, int k) { + private static boolean isPackFormatInvalid(int i, int j, int k) { if (k >= i && k <= j) { return k < 15; } else { @@ -272,55 +272,55 @@ public void importResourcePack(Path file) throws IOException { } public boolean removeResourcePacks(Iterable resourcePacks) throws IOException { - boolean b = false; + boolean modified = false; for (ResourcePackFile resourcePack : resourcePacks) { if (resourcePack != null && resourcePack.manager == this) { resourcePack.delete(); localFiles.remove(resourcePack); - b = true; + modified = true; } } - return b; + return modified; } public void enableResourcePack(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return; Map options = loadOptions(); String packId = "file/" + resourcePack.getFileNameWithExtension(); - boolean b = false; - List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); + boolean modified = false; + List resourcePacks = new ArrayList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); if (!resourcePacks.contains(packId)) { resourcePacks.add(packId); options.put("resourcePacks", StringUtils.serializeStringList(resourcePacks)); - b = true; + modified = true; } - List incompatibleResourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("incompatibleResourcePacks"))); + List incompatibleResourcePacks = new ArrayList<>(StringUtils.deserializeStringList(options.get("incompatibleResourcePacks"))); if (!incompatibleResourcePacks.contains(packId) && isIncompatible(resourcePack)) { incompatibleResourcePacks.add(packId); options.put("incompatibleResourcePacks", StringUtils.serializeStringList(incompatibleResourcePacks)); - b = true; + modified = true; } - if (b) saveOptions(options); + if (modified) saveOptions(options); } public void disableResourcePack(ResourcePackFile resourcePack) { if (resourcePack.manager != this) return; Map options = loadOptions(); String packId = "file/" + resourcePack.getFileNameWithExtension(); - boolean b = false; - List resourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); + boolean modified = false; + List resourcePacks = new ArrayList<>(StringUtils.deserializeStringList(options.get("resourcePacks"))); if (resourcePacks.contains(packId)) { resourcePacks.remove(packId); options.put("resourcePacks", StringUtils.serializeStringList(resourcePacks)); - b = true; + modified = true; } - List incompatibleResourcePacks = new LinkedList<>(StringUtils.deserializeStringList(options.get("incompatibleResourcePacks"))); + List incompatibleResourcePacks = new ArrayList<>(StringUtils.deserializeStringList(options.get("incompatibleResourcePacks"))); if (incompatibleResourcePacks.contains(packId)) { incompatibleResourcePacks.remove(packId); options.put("incompatibleResourcePacks", StringUtils.serializeStringList(incompatibleResourcePacks)); - b = true; + modified = true; } - if (b) saveOptions(options); + if (modified) saveOptions(options); } public boolean isEnabled(ResourcePackFile resourcePack) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index 71debe32a6..c6cf875d38 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -82,7 +82,7 @@ public static SupportedFormats fromJson(JsonElement element) { } } } catch (NumberFormatException e) { - LOG.warning("Failed to parse datapack version component as a number. Value: " + element, e); + LOG.warning("Failed to parse pack version component as a number. Value: " + element, e); } return UNSPECIFIED; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 5932965e01..547f34b3a2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.util; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jetbrains.annotations.Contract; import java.io.PrintWriter; @@ -24,7 +25,6 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * @author huangyuhui @@ -535,22 +535,23 @@ public static boolean isAlphabeticOrNumber(String str) { /// Turns `List.of("a", "b", "c")` into `["a", "b", "c"]` @Contract(pure = true) public static String serializeStringList(List list) { - if (list == null || list.isEmpty()) return "[]"; - return list.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")); + if (list == null) return "[]"; + try { + return JsonUtils.UGLY_GSON.toJson(list.stream().filter(Objects::nonNull).toList(), JsonUtils.listTypeOf(String.class).getType()); + } catch (Exception e) { + return "[]"; + } } /// Turns `["a", "b", "c"]` into `List.of("a", "b", "c")` @Contract(pure = true) public static List deserializeStringList(String list) { if (list == null || list.isBlank()) return List.of(); - list = list.trim(); - if (list.length() < 4 || !list.startsWith("[") || !list.endsWith("]")) return List.of(); - return Arrays.stream(list.substring(1, list.length() - 1).split(",")) - .map(String::trim) - .filter(s -> s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) - .map(s -> s.substring(1, s.length() - 1)) - .filter(StringUtils::isNotBlank) - .toList(); + try { + return JsonUtils.fromNonNullJson(list, JsonUtils.listTypeOf(String.class)); + } catch (Exception e) { + return List.of(); + } } public static class LevCalculator { diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/StringUtilsTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/StringUtilsTest.java index 46017d1f84..1ef52560e7 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/StringUtilsTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/StringUtilsTest.java @@ -19,6 +19,9 @@ import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -47,4 +50,29 @@ public void testNormalizeWhitespaces() { assertEquals("a b c", StringUtils.normalizeWhitespaces(" a \t b c ")); assertEquals("a b c", StringUtils.normalizeWhitespaces(" a \t b c ")); } + + @Test + public void testSerializeStringList() { + assertEquals("[]", StringUtils.serializeStringList(null)); + assertEquals("[]", StringUtils.serializeStringList(List.of())); + assertEquals("[]", StringUtils.serializeStringList(Arrays.asList((String) null))); + assertEquals("[\"hello\"]", StringUtils.serializeStringList(List.of("hello"))); + assertEquals("[\"hello\"]", StringUtils.serializeStringList(Arrays.asList("hello", null))); + assertEquals("[\"he\\\"llo\"]", StringUtils.serializeStringList(Arrays.asList("he\"llo", null))); + assertEquals("[\"hello\",\"world\"]", StringUtils.serializeStringList(List.of("hello", "world"))); + } + + @Test + public void testDeserializeStringList() { + assertEquals(List.of(), StringUtils.deserializeStringList(null)); + assertEquals(List.of(), StringUtils.deserializeStringList("[]")); + assertEquals(List.of(), StringUtils.deserializeStringList("[ ]")); + assertEquals(List.of(), StringUtils.deserializeStringList("[\"]")); + assertEquals(List.of(), StringUtils.deserializeStringList("[\"he\"llo\"]")); + assertEquals(Arrays.asList((String) null), StringUtils.deserializeStringList("[null]")); + assertEquals(List.of("hello"), StringUtils.deserializeStringList("[\"hello\"]")); + assertEquals(List.of("he\"llo"), StringUtils.deserializeStringList("[\"he\\\"llo\"]")); + assertEquals(List.of("hello", "world"), StringUtils.deserializeStringList("[\"hello\",\"world\"]")); + assertEquals(List.of("hello", "world"), StringUtils.deserializeStringList("[\"hello\",\n\"world\"]")); + } } From 79c04a174b94b0e561ea966b9ad09cc891d57932 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 25 Dec 2025 21:33:29 +0800 Subject: [PATCH 37/61] update --- .../jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 2 +- .../java/org/jackhuang/hmcl/mod/ResourcePackManager.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 1e650be4a4..a7e077827f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -110,7 +110,7 @@ public void loadVersion(Profile profile, String version) { public void refresh() { if (resourcePackManager == null || !Files.isDirectory(resourcePackDirectory)) return; setDisable(false); - if (resourcePackManager.getMinecraftVersion().compareTo(ResourcePackManager.LEAST_MC_VERSION) < 0) { + if (!ResourcePackManager.isMcVersionSupported(resourcePackManager.getMinecraftVersion())) { getItems().clear(); setDisable(true); return; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index f8be5faf89..475966a96b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -49,7 +49,12 @@ public final class ResourcePackManager extends LocalFileManager Date: Thu, 25 Dec 2025 21:40:13 +0800 Subject: [PATCH 38/61] update --- .../hmcl/mod/ResourcePackManagerTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java new file mode 100644 index 0000000000..9207a81a80 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java @@ -0,0 +1,27 @@ +package org.jackhuang.hmcl.mod; + +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResourcePackManagerTest { + + @Test + void testIsMcVersionSupported() { + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("26.1-snapshot-1"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("25w14craftmine"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("1.21"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("1.16.5"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("1.13-pre3"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("17w48a"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("13w24a"))); + assertTrue(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("1.6.1"))); + + assertFalse(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("13w23a"))); + assertFalse(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("1.6"))); + assertFalse(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("13w23a"))); + assertFalse(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("b1.1-1"))); + } +} \ No newline at end of file From ff7f5978fd106926619f3031156cce77bc5a23c4 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 25 Dec 2025 22:23:51 +0800 Subject: [PATCH 39/61] update --- .../java/org/jackhuang/hmcl/mod/ResourcePackFile.java | 8 +++----- .../java/org/jackhuang/hmcl/mod/ResourcePackManager.java | 4 ++-- .../org/jackhuang/hmcl/util/versioning/VersionRange.java | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index c77f8f5539..8c2eb1e539 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -1,7 +1,5 @@ package org.jackhuang.hmcl.mod; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Contract; @@ -29,7 +27,7 @@ static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOE protected final String name; protected final String fileName; - private ObjectProperty compatibility = null; + private Compatibility compatibility = null; protected ResourcePackFile(ResourcePackManager manager, Path file) { super(false); @@ -55,9 +53,9 @@ public String getFileNameWithExtension() { public Compatibility getCompatibility() { if (compatibility == null) { - compatibility = new SimpleObjectProperty<>(this, "compatibility", manager.getCompatibility(this)); + compatibility = manager.getCompatibility(this); } - return compatibility.get(); + return compatibility; } public boolean isEnabled() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index 475966a96b..ee54ac56ec 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -98,7 +98,7 @@ public static VersionRange getResourcePackVersionRangeOl return VersionRange.empty(); } if (supportedFormatsUnspecified) { - return VersionRange.only(new PackMcMeta.PackVersion(packInfo.packFormat(), 0)); + return VersionRange.is(new PackMcMeta.PackVersion(packInfo.packFormat(), 0)); } return VersionRange.between(packInfo.supportedFormats().getMin(), packInfo.supportedFormats().getMax()); } @@ -159,7 +159,7 @@ public static VersionRange getResourcePackVersionRangeNe } else if (!packFormatUnspecified) { int packFormat = packInfo.packFormat(); PackMcMeta.PackVersion packVersion = new PackMcMeta.PackVersion(packFormat, 0); - return packFormat > 64 ? VersionRange.empty() : VersionRange.only(packVersion); + return packFormat > 64 ? VersionRange.empty() : VersionRange.is(packVersion); } return VersionRange.empty(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java index 1e5b4db1d5..17845fecb1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionRange.java @@ -33,7 +33,7 @@ public static > VersionRange atMost(T maximum) { return new VersionRange<>(null, maximum); } - public static > VersionRange only(T version) { + public static > VersionRange is(T version) { assert version != null; return new VersionRange<>(version, version); } From a9150242d254a54707b57bfc0d2790584b588144 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 25 Dec 2025 22:24:29 +0800 Subject: [PATCH 40/61] update --- .../hmcl/ui/versions/CheckUpdatesTask.java | 10 ++++----- .../hmcl/ui/versions/UpdatesPage.java | 22 +++++++++---------- .../{LocalFile.java => LocalAddonFile.java} | 6 ++--- .../jackhuang/hmcl/mod/LocalFileManager.java | 2 +- .../org/jackhuang/hmcl/mod/LocalModFile.java | 2 +- .../jackhuang/hmcl/mod/ResourcePackFile.java | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/mod/{LocalFile.java => LocalAddonFile.java} (81%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index 16bd7016bb..af192f6141 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.ui.versions; -import org.jackhuang.hmcl.mod.LocalFile; +import org.jackhuang.hmcl.mod.LocalAddonFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.task.Task; @@ -25,10 +25,10 @@ import java.util.*; import java.util.stream.Collectors; -public class CheckUpdatesTask extends Task> { - private final Collection>> dependents; +public class CheckUpdatesTask extends Task> { + private final Collection>> dependents; - public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { + public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { Map repos = new LinkedHashMap<>(2); for (RemoteMod.Type modType : RemoteMod.Type.values()) { RemoteModRepository repo = modType.getRepoForType(repoType); @@ -78,7 +78,7 @@ public void execute() throws Exception { .filter(task -> task.getResult() != null) .map(Task::getResult) .filter(modUpdate -> !modUpdate.candidates().isEmpty()) - .max(Comparator.comparing((LocalFile.ModUpdate modUpdate) -> modUpdate.candidates().get(0).getDatePublished())) + .max(Comparator.comparing((LocalAddonFile.ModUpdate modUpdate) -> modUpdate.candidates().get(0).getDatePublished())) .orElse(null) ) .filter(Objects::nonNull) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java index a5701f5928..98a31fabf4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/UpdatesPage.java @@ -29,7 +29,7 @@ import javafx.scene.control.TableView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import org.jackhuang.hmcl.mod.LocalFile; +import org.jackhuang.hmcl.mod.LocalAddonFile; import org.jackhuang.hmcl.mod.LocalFileManager; import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.RemoteMod; @@ -61,14 +61,14 @@ import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class UpdatesPage extends BorderPane implements DecoratorPage { +public class UpdatesPage extends BorderPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("mods.check_updates"))); private final LocalFileManager localFileManager; private final ObservableList objects; @SuppressWarnings("unchecked") - public UpdatesPage(LocalFileManager localFileManager, List updates) { + public UpdatesPage(LocalFileManager localFileManager, List updates) { this.localFileManager = localFileManager; getStyleClass().add("gray-background"); @@ -143,7 +143,7 @@ private void updateFiles() { fireEvent(new PageCloseEvent()); if (!task.getFailedMods().isEmpty()) { Controllers.dialog(i18n("mods.check_updates.failed_download") + "\n" + - task.getFailedMods().stream().map(LocalFile::getFileName).collect(Collectors.joining("\n")), + task.getFailedMods().stream().map(LocalAddonFile::getFileName).collect(Collectors.joining("\n")), i18n("install.failed"), MessageDialogPane.MessageType.ERROR); } @@ -192,14 +192,14 @@ public ReadOnlyObjectWrapper stateProperty() { } private static final class ModUpdateObject { - final LocalFile.ModUpdate data; + final LocalAddonFile.ModUpdate data; final BooleanProperty enabled = new SimpleBooleanProperty(); final StringProperty fileName = new SimpleStringProperty(); final StringProperty currentVersion = new SimpleStringProperty(); final StringProperty targetVersion = new SimpleStringProperty(); final StringProperty source = new SimpleStringProperty(); - public ModUpdateObject(LocalFile.ModUpdate data) { + public ModUpdateObject(LocalAddonFile.ModUpdate data) { this.data = data; enabled.set(!data.localFile().isDisabled()); @@ -278,15 +278,15 @@ public void setSource(String source) { public static class UpdateTask extends Task { private final Collection> dependents; - private final List failedMods = new ArrayList<>(); + private final List failedMods = new ArrayList<>(); - UpdateTask(Path modDirectory, List> mods) { + UpdateTask(Path modDirectory, List> mods) { setStage("mods.check_updates.confirm"); getProperties().put("total", mods.size()); this.dependents = new ArrayList<>(); - for (Pair mod : mods) { - LocalFile local = mod.getKey(); + for (Pair mod : mods) { + LocalAddonFile local = mod.getKey(); RemoteMod.Version remote = mod.getValue(); boolean isDisabled = local.isDisabled(); @@ -323,7 +323,7 @@ public static class UpdateTask extends Task { } } - public List getFailedMods() { + public List getFailedMods() { return failedMods; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java similarity index 81% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java index 3690b32237..fcd2de7149 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java @@ -8,11 +8,11 @@ import java.util.List; /// Should implement `Comparable` -public sealed abstract class LocalFile permits LocalModFile, ResourcePackFile { +public sealed abstract class LocalAddonFile permits LocalModFile, ResourcePackFile { private final boolean keepOldFiles; - protected LocalFile(boolean keepOldFiles) { + protected LocalAddonFile(boolean keepOldFiles) { this.keepOldFiles = keepOldFiles; } @@ -38,7 +38,7 @@ public boolean keepOldFiles() { @Nullable public abstract ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException; - public record ModUpdate(LocalFile localFile, RemoteMod.Version currentVersion, + public record ModUpdate(LocalAddonFile localFile, RemoteMod.Version currentVersion, List candidates) { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java index b115305e46..ffa8a956ea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalFileManager.java @@ -13,7 +13,7 @@ import java.util.List; import java.util.Set; -public abstract class LocalFileManager { +public abstract class LocalFileManager { public static final String DISABLED_EXTENSION = ".disabled"; public static final String OLD_EXTENSION = ".old"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index 3b77022399..a60e95fca0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -33,7 +33,7 @@ * * @author huangyuhui */ -public final class LocalModFile extends LocalFile implements Comparable { +public final class LocalModFile extends LocalAddonFile implements Comparable { private Path file; private final ModManager modManager; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index 8c2eb1e539..0b8648c9ca 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -11,7 +11,7 @@ import java.nio.file.Path; import java.util.Locale; -public sealed abstract class ResourcePackFile extends LocalFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { +public sealed abstract class ResourcePackFile extends LocalAddonFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOException { String fileName = LocalFileManager.getLocalFileName(path); if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { From e1d8f5fdac5696ced5b5ea43efe91230aa4783f4 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 26 Dec 2025 18:10:14 +0800 Subject: [PATCH 41/61] using config --- .../java/org/jackhuang/hmcl/setting/GlobalConfig.java | 11 +++++++++++ .../hmcl/ui/versions/ResourcePackListPage.java | 11 +++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java index 1c43366e4f..a5d1e2b80f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java @@ -148,6 +148,17 @@ public ObservableSet getDisabledJava() { return disabledJava; } + @SerializedName("resourcePackWarningShown") + private final BooleanProperty resourcePackWarningShown = new SimpleBooleanProperty(false); + + public boolean isResourcePackWarningShown() { + return resourcePackWarningShown.get(); + } + + public void onResourcePackWarningShown() { + resourcePackWarningShown.set(true); + } + static final class Adapter extends ObservableSetting.Adapter { @Override protected GlobalConfig createInstance() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index a7e077827f..2435c126cc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.mod.ResourcePackFile; import org.jackhuang.hmcl.mod.ResourcePackManager; +import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -79,8 +80,6 @@ public final class ResourcePackListPage extends ListPageBase Files.isDirectory(file) || file.getFileName().toString().endsWith(".zip"), this::addFiles); } @@ -170,10 +169,10 @@ private void onOpenFolder() { } private void setSelectedEnabled(List selectedItems, boolean enabled) { - if (!warningShown) { + if (!ConfigHolder.globalConfig().isResourcePackWarningShown()) { Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), () -> { - warningShown = true; + ConfigHolder.globalConfig().onResourcePackWarningShown(); setSelectedEnabled(selectedItems, enabled); }, null); } else { @@ -476,11 +475,11 @@ public ResourcePackListCell(JFXListView listView, Resour checkBox = new JFXCheckBox() { @Override public void fire() { - if (!page.warningShown) { + if (!ConfigHolder.globalConfig().isResourcePackWarningShown()) { Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), () -> { super.fire(); - page.warningShown = true; + ConfigHolder.globalConfig().onResourcePackWarningShown(); }, null); } else { super.fire(); From f10867176c95a730b39be4ac602c5a71afd42d39 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 26 Dec 2025 18:13:02 +0800 Subject: [PATCH 42/61] update --- .../java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java index 9207a81a80..c4815e457e 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/ResourcePackManagerTest.java @@ -24,4 +24,4 @@ void testIsMcVersionSupported() { assertFalse(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("13w23a"))); assertFalse(ResourcePackManager.isMcVersionSupported(GameVersionNumber.asGameVersion("b1.1-1"))); } -} \ No newline at end of file +} From 87bb56d7e41aa67ac6f6332693c93eea05a087b9 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 26 Dec 2025 18:26:18 +0800 Subject: [PATCH 43/61] update --- .../hmcl/ui/versions/ResourcePackListPage.java | 12 ++++++++++-- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- .../main/resources/assets/lang/I18N_zh.properties | 2 +- .../main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 2435c126cc..ed307b3e0c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -170,7 +170,11 @@ private void onOpenFolder() { private void setSelectedEnabled(List selectedItems, boolean enabled) { if (!ConfigHolder.globalConfig().isResourcePackWarningShown()) { - Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), + Controllers.confirmWithCountdown( + i18n("resourcepack.warning.manipulate"), + i18n("message.warning"), + 5, + MessageDialogPane.MessageType.WARNING, () -> { ConfigHolder.globalConfig().onResourcePackWarningShown(); setSelectedEnabled(selectedItems, enabled); @@ -476,7 +480,11 @@ public ResourcePackListCell(JFXListView listView, Resour @Override public void fire() { if (!ConfigHolder.globalConfig().isResourcePackWarningShown()) { - Controllers.confirm(i18n("resourcepack.warning.manipulate"), i18n("message.warning"), + Controllers.confirmWithCountdown( + i18n("resourcepack.warning.manipulate"), + i18n("message.warning"), + 5, + MessageDialogPane.MessageType.WARNING, () -> { super.fire(); ConfigHolder.globalConfig().onResourcePackWarningShown(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 55c1349c8f..e9de3571cc 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1219,7 +1219,7 @@ resourcepack.download=Download resourcepack.download.title=Download Resource Pack - %1s resourcepack.manage=Resource Packs resourcepack.warning.invalid=Invalid pack metadata -resourcepack.warning.manipulate=Resource pack activation could be influenced by mods, causing unexpected behavior.\nAre you sure to enable/disable the resource pack? +resourcepack.warning.manipulate=Resource pack activation could be influenced by mods, so the actual state may be different from what is set here.\n\nAre you sure to enable/disable the resource pack? resourcepack.warning.missing_game_meta=Missing instance metadata resourcepack.warning.missing_pack_meta=Missing pack metadata resourcepack.warning.too_new=For newer game versions diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0a6fe4f97e..4dabe8ae09 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1012,7 +1012,7 @@ resourcepack.download=下載資源包 resourcepack.download.title=資源包下載 - %1s resourcepack.manage=資源包管理 resourcepack.warning.invalid=資源包中繼資料無效 -resourcepack.warning.manipulate=資源包的載入可能會受到模組干擾,導致執行效果異常。\n你確定要啟用或停用此資源包嗎? +resourcepack.warning.manipulate=資源包的載入可能會受到模組干擾,實際效果可能與此處設定的不同。\n\n你確定要啟用或停用此資源包嗎? resourcepack.warning.missing_game_meta=目前實例中繼資料缺失 resourcepack.warning.missing_pack_meta=資源包中繼資料缺失 resourcepack.warning.too_new=為更新的遊戲版本製作 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 9c07a1ea13..ebb25ea3cf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1022,7 +1022,7 @@ resourcepack.download=下载资源包 resourcepack.download.title=资源包下载 - %1s resourcepack.manage=资源包管理 resourcepack.warning.invalid=资源包元数据无效 -resourcepack.warning.manipulate=资源包的加载可能会受到模组干扰,导致运行效果异常。\n你确定要启用或禁用此资源包吗? +resourcepack.warning.manipulate=资源包的加载可能会受到模组干扰,实际效果可能与此处设置的不同。\n\n你确定要启用或禁用此资源包吗? resourcepack.warning.missing_game_meta=当前实例元数据缺失 resourcepack.warning.missing_pack_meta=资源包元数据缺失 resourcepack.warning.too_new=为更新的游戏版本打造 From 4951406b91308cf22def38b1f5a9c0adbdda3af8 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 26 Dec 2025 22:44:48 +0800 Subject: [PATCH 44/61] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=8C=85=E3=80=81=E5=85=89=E5=BD=B1=E5=8C=85=E7=AD=89=E7=9A=84?= =?UTF-8?q?=E6=8E=A8=E8=8D=90=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/DownloadPage.java | 16 +++++++++++++--- .../java/org/jackhuang/hmcl/mod/RemoteMod.java | 8 +++++++- .../org/jackhuang/hmcl/mod/curse/CurseAddon.java | 5 +++-- .../mod/curse/CurseForgeRemoteModRepository.java | 6 +++--- .../modrinth/ModrinthRemoteModRepository.java | 9 +++++---- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 9f582aec3c..513700cba8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -289,13 +289,23 @@ protected ModDownloadPageSkin(DownloadPage control) { resolve: for (RemoteMod.Version modVersion : modVersions) { - for (ModLoaderType loader : modVersion.getLoaders()) { - if (targetLoaders.contains(loader)) { + if (getSkinnable().type == RemoteModRepository.Type.MOD) { + for (ModLoaderType loader : modVersion.getLoaders()) { + if (targetLoaders.contains(loader)) { + list.getContent().addAll( + ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), + new ModItem(modVersion, control) + ); + break resolve; + } + } + } else { + if (modVersion.getRepoType() == getSkinnable().type) { list.getContent().addAll( ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), new ModItem(modVersion, control) ); - break resolve; + break; } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index 53cbe69c39..aef4855cb4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -269,8 +269,9 @@ public static class Version { private final List dependencies; private final List gameVersions; private final List loaders; + private final RemoteModRepository.Type repoType; - public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { + public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders, RemoteModRepository.Type repoType) { this.self = self; this.modid = modid; this.name = name; @@ -282,6 +283,7 @@ public Version(IVersion self, String modid, String name, String version, String this.dependencies = dependencies; this.gameVersions = gameVersions; this.loaders = loaders; + this.repoType = repoType; } public IVersion getSelf() { @@ -327,6 +329,10 @@ public List getGameVersions() { public List getLoaders() { return loaders; } + + public RemoteModRepository.Type getRepoType() { + return repoType; + } } public static class File { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 6d6c44ebfe..84eac18346 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -553,7 +553,7 @@ public RemoteMod.Type getType() { return RemoteMod.Type.CURSEFORGE; } - public RemoteMod.Version toVersion() { + public RemoteMod.Version toVersion(RemoteModRepository.Type repoType) { RemoteMod.VersionType versionType; switch (getReleaseType()) { case 1: @@ -592,7 +592,8 @@ public RemoteMod.Version toVersion() { else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT); else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGED); else return Stream.empty(); - }).collect(Collectors.toList()) + }).collect(Collectors.toList()), + repoType ); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 3e90fb2878..c2ed2153e5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -180,7 +180,7 @@ public Optional getRemoteVersionByLocalFile(Path file) throws return Optional.empty(); } - return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion()); + return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion(type)); } @Override @@ -194,7 +194,7 @@ public RemoteMod getModById(String id) throws IOException { public RemoteMod.File getModFile(String modId, String fileId) throws IOException { Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))) .getJson(Response.typeOf(CurseAddon.LatestFile.class)); - return response.getData().toVersion().getFile(); + return response.getData().toVersion(type).getFile(); } @Override @@ -202,7 +202,7 @@ public Stream getRemoteVersionsById(String id) throws IOExcep Response> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", pair("pageSize", "10000"))) .getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class))); - return response.getData().stream().map(CurseAddon.LatestFile::toVersion); + return response.getData().stream().map(file -> file.toVersion(type)); } public List getCategoriesImpl() throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index da173bf5b4..a4abc45b0e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -116,7 +116,7 @@ public Optional getRemoteVersionByLocalFile(Path file) throws ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1, pair("algorithm", "sha1")) .getJson(ProjectVersion.class); - return mod.toVersion(); + return mod.toVersion(type); } catch (ResponseCodeException e) { if (e.getResponseCode() == 404) { return Optional.empty(); @@ -145,7 +145,7 @@ public Stream getRemoteVersionsById(String id) throws IOExcep id = StringUtils.removePrefix(id, "local-"); List versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version") .getJson(listTypeOf(ProjectVersion.class)); - return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); + return versions.stream().map(projVersion -> projVersion.toVersion(type)).flatMap(Lang::toStream); } public List getCategoriesImpl() throws IOException { @@ -492,7 +492,7 @@ public RemoteMod.Type getType() { return RemoteMod.Type.MODRINTH; } - public Optional toVersion() { + public Optional toVersion(RemoteModRepository.Type repoType) { RemoteMod.VersionType type; if ("release".equals(versionType)) { type = RemoteMod.VersionType.Release; @@ -536,7 +536,8 @@ public Optional toVersion() { 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()) + }).collect(Collectors.toList()), + repoType )); } } From 59d0dec0201e3099ddcef2a9922619f4756b89a3 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 26 Dec 2025 23:18:36 +0800 Subject: [PATCH 45/61] update --- .../org/jackhuang/hmcl/ui/versions/DownloadPage.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 513700cba8..a384070aab 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -300,13 +300,11 @@ protected ModDownloadPageSkin(DownloadPage control) { } } } else { - if (modVersion.getRepoType() == getSkinnable().type) { - list.getContent().addAll( - ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), - new ModItem(modVersion, control) - ); - break; - } + list.getContent().addAll( + ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), + new ModItem(modVersion, control) + ); + break; } } } From 186ae00f95cf8d5efda99052c13c487cb19d1efe Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 27 Dec 2025 10:37:17 +0800 Subject: [PATCH 46/61] update --- .../src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index 0b8648c9ca..f8fb15336a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -1,6 +1,7 @@ package org.jackhuang.hmcl.mod; import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -34,7 +35,7 @@ protected ResourcePackFile(ResourcePackManager manager, Path file) { this.manager = manager; this.file = file; this.fileName = LocalFileManager.getLocalFileName(file); - this.name = FileUtils.getNameWithoutExtension(fileName); + this.name = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(fileName)); } @Override From cf84e5f71b917a1216d86698346edf77b0bd71ef Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 27 Dec 2025 12:12:20 +0800 Subject: [PATCH 47/61] update --- .../src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java | 7 +------ .../java/org/jackhuang/hmcl/mod/curse/CurseAddon.java | 5 ++--- .../hmcl/mod/curse/CurseForgeRemoteModRepository.java | 6 +++--- .../hmcl/mod/modrinth/ModrinthRemoteModRepository.java | 9 ++++----- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index aef4855cb4..c9b1fe8ad4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -269,9 +269,8 @@ public static class Version { private final List dependencies; private final List gameVersions; private final List loaders; - private final RemoteModRepository.Type repoType; - public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders, RemoteModRepository.Type repoType) { + public Version(IVersion self, String modid, String name, String version, String changelog, Instant datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { this.self = self; this.modid = modid; this.name = name; @@ -283,7 +282,6 @@ public Version(IVersion self, String modid, String name, String version, String this.dependencies = dependencies; this.gameVersions = gameVersions; this.loaders = loaders; - this.repoType = repoType; } public IVersion getSelf() { @@ -330,9 +328,6 @@ public List getLoaders() { return loaders; } - public RemoteModRepository.Type getRepoType() { - return repoType; - } } public static class File { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 84eac18346..6d6c44ebfe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -553,7 +553,7 @@ public RemoteMod.Type getType() { return RemoteMod.Type.CURSEFORGE; } - public RemoteMod.Version toVersion(RemoteModRepository.Type repoType) { + public RemoteMod.Version toVersion() { RemoteMod.VersionType versionType; switch (getReleaseType()) { case 1: @@ -592,8 +592,7 @@ public RemoteMod.Version toVersion(RemoteModRepository.Type repoType) { else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT); else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGED); else return Stream.empty(); - }).collect(Collectors.toList()), - repoType + }).collect(Collectors.toList()) ); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index c2ed2153e5..3e90fb2878 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -180,7 +180,7 @@ public Optional getRemoteVersionByLocalFile(Path file) throws return Optional.empty(); } - return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion(type)); + return Optional.of(response.getData().getExactMatches().get(0).getFile().toVersion()); } @Override @@ -194,7 +194,7 @@ public RemoteMod getModById(String id) throws IOException { public RemoteMod.File getModFile(String modId, String fileId) throws IOException { Response response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))) .getJson(Response.typeOf(CurseAddon.LatestFile.class)); - return response.getData().toVersion(type).getFile(); + return response.getData().toVersion().getFile(); } @Override @@ -202,7 +202,7 @@ public Stream getRemoteVersionsById(String id) throws IOExcep Response> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files", pair("pageSize", "10000"))) .getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class))); - return response.getData().stream().map(file -> file.toVersion(type)); + return response.getData().stream().map(CurseAddon.LatestFile::toVersion); } public List getCategoriesImpl() throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index a4abc45b0e..ca06cb6991 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -116,7 +116,7 @@ public Optional getRemoteVersionByLocalFile(Path file) throws ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1, pair("algorithm", "sha1")) .getJson(ProjectVersion.class); - return mod.toVersion(type); + return mod.toVersion(); } catch (ResponseCodeException e) { if (e.getResponseCode() == 404) { return Optional.empty(); @@ -145,7 +145,7 @@ public Stream getRemoteVersionsById(String id) throws IOExcep id = StringUtils.removePrefix(id, "local-"); List versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version") .getJson(listTypeOf(ProjectVersion.class)); - return versions.stream().map(projVersion -> projVersion.toVersion(type)).flatMap(Lang::toStream); + return versions.stream().map(projVersion -> projVersion.toVersion()).flatMap(Lang::toStream); } public List getCategoriesImpl() throws IOException { @@ -492,7 +492,7 @@ public RemoteMod.Type getType() { return RemoteMod.Type.MODRINTH; } - public Optional toVersion(RemoteModRepository.Type repoType) { + public Optional toVersion() { RemoteMod.VersionType type; if ("release".equals(versionType)) { type = RemoteMod.VersionType.Release; @@ -536,8 +536,7 @@ public Optional toVersion(RemoteModRepository.Type repoType) 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()), - repoType + }).collect(Collectors.toList()) )); } } From 97d7d0f4ee58466be754f84c20148db81cfb1ef5 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 27 Dec 2025 12:14:55 +0800 Subject: [PATCH 48/61] update --- HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index c9b1fe8ad4..53cbe69c39 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -327,7 +327,6 @@ public List getGameVersions() { public List getLoaders() { return loaders; } - } public static class File { From 4bf35f2d0c72e56e3dfee12dafbd0dda4052da71 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 27 Dec 2025 12:34:56 +0800 Subject: [PATCH 49/61] update --- .../src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java index fcd2de7149..8e611952cf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalAddonFile.java @@ -7,7 +7,7 @@ import java.nio.file.Path; import java.util.List; -/// Should implement `Comparable` +/// Sub-classes should implement `Comparable` public sealed abstract class LocalAddonFile permits LocalModFile, ResourcePackFile { private final boolean keepOldFiles; From 4fdab0aae3ca3b7a5cb48c6f54cb49df2c962d7b Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 28 Dec 2025 11:19:11 +0800 Subject: [PATCH 50/61] update --- .../java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java | 4 ++-- .../main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java | 2 +- .../org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index af192f6141..70cca66e4f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -25,10 +25,10 @@ import java.util.*; import java.util.stream.Collectors; -public class CheckUpdatesTask extends Task> { +public class CheckUpdatesTask extends Task> { private final Collection>> dependents; - public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { + public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModRepository.Type repoType) { Map repos = new LinkedHashMap<>(2); for (RemoteMod.Type modType : RemoteMod.Type.values()) { RemoteModRepository repo = modType.getRepoForType(repoType); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 44ee902835..5f26fa7aff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -235,7 +235,7 @@ public void checkUpdates() { .composeAsync(() -> { Optional gameVersion = profile.getRepository().getGameVersion(instanceId); if (gameVersion.isPresent()) { - return new CheckUpdatesTask(gameVersion.get(), modManager.getLocalFiles(), RemoteModRepository.Type.MOD); + return new CheckUpdatesTask<>(gameVersion.get(), modManager.getLocalFiles(), RemoteModRepository.Type.MOD); } return null; }) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index ed307b3e0c..47a6a8daf9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -204,7 +204,7 @@ public void checkUpdates() { .composeAsync(() -> { Optional gameVersion = profile.getRepository().getGameVersion(instanceId); if (gameVersion.isPresent()) { - return new CheckUpdatesTask(gameVersion.get(), resourcePackManager.getLocalFiles(), RemoteModRepository.Type.RESOURCE_PACK); + return new CheckUpdatesTask<>(gameVersion.get(), resourcePackManager.getLocalFiles(), RemoteModRepository.Type.RESOURCE_PACK); } return null; }) From ca167cbb854733bbdba33dc638dbda4a651e8afa Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 28 Dec 2025 11:50:11 +0800 Subject: [PATCH 51/61] update --- .../java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index 70cca66e4f..98fdbc2099 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -43,12 +43,12 @@ public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModReposit Task.supplyAsync(() -> mod.checkUpdates(gameVersion, entry.getValue())) .setSignificance(TaskSignificance.MAJOR) .setName(String.format("%s (%s)", mod.getFileName(), entry.getKey())).withCounter("update.checking") - ).toList() + ).collect(Collectors.toList()) ) .collect(Collectors.toList()); setStage("update.checking"); - getProperties().put("total", dependents.size() * RemoteMod.Type.values().length); + getProperties().put("total", dependents.size() * repos.size()); } @Override From 6ab182c3eb43446563e6e97b3a49272567c2be45 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 28 Dec 2025 11:50:51 +0800 Subject: [PATCH 52/61] update --- .../java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java index 98fdbc2099..f838191ec5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/CheckUpdatesTask.java @@ -43,7 +43,8 @@ public CheckUpdatesTask(String gameVersion, Collection mods, RemoteModReposit Task.supplyAsync(() -> mod.checkUpdates(gameVersion, entry.getValue())) .setSignificance(TaskSignificance.MAJOR) .setName(String.format("%s (%s)", mod.getFileName(), entry.getKey())).withCounter("update.checking") - ).collect(Collectors.toList()) + ) + .collect(Collectors.toList()) ) .collect(Collectors.toList()); From c3ae0c1a4940039c9a5062c0b490a69614aeb6e0 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 28 Dec 2025 12:01:45 +0800 Subject: [PATCH 53/61] update --- .../org/jackhuang/hmcl/mod/ResourcePackFile.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index f8fb15336a..d50df652b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -14,7 +14,7 @@ public sealed abstract class ResourcePackFile extends LocalAddonFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOException { - String fileName = LocalFileManager.getLocalFileName(path); + String fileName = path.getFileName().toString(); if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { return new ResourcePackZipFile(manager, path); } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { @@ -25,8 +25,8 @@ static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOE protected final ResourcePackManager manager; protected Path file; - protected final String name; protected final String fileName; + protected final String fileNameWithExtension; private Compatibility compatibility = null; @@ -34,8 +34,8 @@ protected ResourcePackFile(ResourcePackManager manager, Path file) { super(false); this.manager = manager; this.file = file; - this.fileName = LocalFileManager.getLocalFileName(file); - this.name = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(fileName)); + this.fileNameWithExtension = file.getFileName().toString(); + this.fileName = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(fileNameWithExtension)); } @Override @@ -45,11 +45,11 @@ public Path getFile() { @Override public String getFileName() { - return name; + return fileName; } public String getFileNameWithExtension() { - return getFile().getFileName().toString(); + return fileNameWithExtension; } public Compatibility getCompatibility() { @@ -94,7 +94,7 @@ public LocalModFile.Description getDescription() { @Override public int compareTo(@NotNull ResourcePackFile other) { - return this.getFileNameWithExtension().compareToIgnoreCase(other.getFileNameWithExtension()); + return this.fileNameWithExtension.compareTo(other.fileNameWithExtension); } public enum Compatibility { From d9a9dbb8950e771f7c3d559d6931e76bd1b59071 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 28 Dec 2025 12:02:42 +0800 Subject: [PATCH 54/61] update --- .../src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index d50df652b8..c305ae144e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -35,7 +35,7 @@ protected ResourcePackFile(ResourcePackManager manager, Path file) { this.manager = manager; this.file = file; this.fileNameWithExtension = file.getFileName().toString(); - this.fileName = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(fileNameWithExtension)); + this.fileName = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(file)); } @Override From 701b83935db4a34d12d529703f082483b5088653 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 28 Dec 2025 13:12:01 +0800 Subject: [PATCH 55/61] =?UTF-8?q?=E6=B8=85=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/mod/ModLoaderType.java | 3 +-- .../org/jackhuang/hmcl/mod/ModManager.java | 4 +--- .../jackhuang/hmcl/mod/ResourcePackFile.java | 2 +- .../hmcl/mod/modinfo/PackMcMeta.java | 21 ------------------- 4 files changed, 3 insertions(+), 27 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java index f4b8feacd8..641b15cec6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java @@ -24,6 +24,5 @@ public enum ModLoaderType { NEO_FORGED, FABRIC, QUILT, - LITE_LOADER, - PACK; + LITE_LOADER } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index 59ec227ee9..6a655d3b09 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -50,11 +50,9 @@ private interface ModMetadataReader { pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGED), pair(ForgeOldModMetadata::fromFile, ModLoaderType.FORGE), pair(FabricModMetadata::fromFile, ModLoaderType.FABRIC), - pair(QuiltModMetadata::fromFile, ModLoaderType.QUILT), - pair(PackMcMeta::fromFile, ModLoaderType.PACK) + pair(QuiltModMetadata::fromFile, ModLoaderType.QUILT) ); - map.put("zip", zipReaders); map.put("jar", zipReaders); map.put("litemod", List.of(pair(LiteModMetadata::fromFile, ModLoaderType.LITE_LOADER))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index c305ae144e..75959233f2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -34,8 +34,8 @@ protected ResourcePackFile(ResourcePackManager manager, Path file) { super(false); this.manager = manager; this.file = file; - this.fileNameWithExtension = file.getFileName().toString(); this.fileName = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(file)); + this.fileNameWithExtension = file.getFileName().toString(); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index c6cf875d38..139daeeeac 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -21,21 +21,13 @@ import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.ModLoaderType; -import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonSerializable; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.Validation; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; -import java.io.IOException; import java.lang.reflect.Type; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -225,17 +217,4 @@ public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationC } } - public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException { - Path mcmod = fs.getPath("pack.mcmeta"); - if (Files.notExists(mcmod)) - throw new IOException("File " + modFile + " is not a resource pack."); - PackMcMeta metadata = JsonUtils.fromNonNullJson(Files.readString(mcmod), PackMcMeta.class); - return new LocalModFile( - modManager, - modManager.getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.PACK), - modFile, - FileUtils.getNameWithoutExtension(modFile), - metadata.pack.description, - "", "", "", "", ""); - } } From 73662a66af99173695a9e94bab22b9413843cbf7 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 1 Jan 2026 22:57:11 +0800 Subject: [PATCH 56/61] Apply suggestions --- .../java/org/jackhuang/hmcl/mod/LocalModFile.java | 2 ++ .../org/jackhuang/hmcl/mod/ResourcePackZipFile.java | 13 ++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index a60e95fca0..4b17d9fa86 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -100,6 +100,7 @@ public LocalMod getMod() { return mod; } + @Override public Path getFile() { return file; } @@ -161,6 +162,7 @@ public boolean isOld() { return modManager.isOld(file); } + @Override public void setOld(boolean old) throws IOException { file = modManager.setOld(this, old); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java index 97546badfb..ddb1ee5fcf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java @@ -24,28 +24,27 @@ final class ResourcePackZipFile extends ResourcePackFile { public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException { super(manager, path); - PackMcMeta meta = null; - byte[] icon = null; + PackMcMeta metaTemp = null; + byte[] iconTemp = null; try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(path))) { try { - meta = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class); + metaTemp = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class); } catch (Exception e) { LOG.warning("Failed to parse resource pack meta", e); } - this.meta = meta; var iconEntry = zipFileTree.getEntry("/pack.png"); if (iconEntry != null) { try (InputStream is = zipFileTree.getInputStream(iconEntry)) { - icon = is.readAllBytes(); + iconTemp = is.readAllBytes(); } catch (Exception e) { LOG.warning("Failed to load resource pack icon", e); } } } - - this.icon = icon; + this.meta = metaTemp; + this.icon = iconTemp; } @Override From c4b4d2f9d7b86bff9a9526fc829f10c6507c9a92 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 2 Jan 2026 21:20:14 +0800 Subject: [PATCH 57/61] update --- .../java/org/jackhuang/hmcl/ui/download/DownloadPage.java | 3 +-- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 2 +- .../src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java | 2 +- .../java/org/jackhuang/hmcl/mod/RemoteModRepository.java | 1 - .../hmcl/mod/modrinth/ModrinthRemoteModRepository.java | 6 +++--- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 2959886e94..85d2b8440a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -25,7 +25,6 @@ import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; -import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -103,7 +102,7 @@ public DownloadPage(String uploadVersion) { })); modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(FOR_MOD, true))); resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(FOR_RESOURCE_PACK, true))); - shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(ModrinthRemoteModRepository.SHADER_PACKS, FOR_SHADER, true))); + shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(FOR_SHADER, true))); worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS))); tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 4cc56fcb57..f0f74fa06f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -365,7 +365,7 @@ private static final class DependencyModItem extends StackPane { DownloadCallback callback = switch (type) { case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD; case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK; - case SHADER -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; + case SHADER_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; default -> null; }; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index 53cbe69c39..35b846804b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -223,7 +223,7 @@ public RemoteModRepository getRepoForType(RemoteModRepository.Type type) { return switch (type) { case MOD -> modRepo; case RESOURCE_PACK -> resourcePackRepo; - case SHADER -> shaderPackRepo; + case SHADER_PACK -> shaderPackRepo; case WORLD -> worldRepo; case MODPACK -> modpackRepo; case CUSTOMIZATION -> customizationRepo; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index b90b566009..964baf3e90 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -34,7 +34,6 @@ enum Type { RESOURCE_PACK, SHADER_PACK, WORLD, - SHADER, CUSTOMIZATION } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index ca06cb6991..66239c70a4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -59,7 +59,7 @@ private ModrinthRemoteModRepository(String projectType) { this.type = switch (projectType) { case "modpack" -> Type.MODPACK; case "resourcepack" -> Type.RESOURCE_PACK; - case "shader" -> Type.SHADER; + case "shader" -> Type.SHADER_PACK; default -> Type.MOD; }; } @@ -317,7 +317,7 @@ public RemoteMod toMod() { RemoteModRepository.Type type = switch (projectType) { case "modpack" -> RemoteModRepository.Type.MODPACK; case "resourcepack" -> RemoteModRepository.Type.RESOURCE_PACK; - case "shader" -> RemoteModRepository.Type.SHADER; + case "shader" -> RemoteModRepository.Type.SHADER_PACK; default -> RemoteModRepository.Type.MOD; }; return new RemoteMod( @@ -703,7 +703,7 @@ public RemoteMod toMod() { RemoteModRepository.Type type = switch (projectType) { case "modpack" -> RemoteModRepository.Type.MODPACK; case "resourcepack" -> RemoteModRepository.Type.RESOURCE_PACK; - case "shader" -> RemoteModRepository.Type.SHADER; + case "shader" -> RemoteModRepository.Type.SHADER_PACK; default -> RemoteModRepository.Type.MOD; }; return new RemoteMod( From 6fd1a566b6a85bcf1bbf0db3a15a228b79eb4cba Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 3 Jan 2026 18:43:58 +0800 Subject: [PATCH 58/61] update --- .../java/org/jackhuang/hmcl/mod/ResourcePackFile.java | 11 ++++++----- .../org/jackhuang/hmcl/mod/ResourcePackFolder.java | 2 +- .../org/jackhuang/hmcl/mod/ResourcePackManager.java | 3 ++- .../org/jackhuang/hmcl/mod/ResourcePackZipFile.java | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java index 75959233f2..a3f366a375 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFile.java @@ -14,15 +14,16 @@ public sealed abstract class ResourcePackFile extends LocalAddonFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { static ResourcePackFile parse(ResourcePackManager manager, Path path) throws IOException { - String fileName = path.getFileName().toString(); - if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { - return new ResourcePackZipFile(manager, path); - } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { - return new ResourcePackFolder(manager, path); + if (isFileResourcePack(path)) { + return Files.isRegularFile(path) ? new ResourcePackZipFile(manager, path) : new ResourcePackFolder(manager, path); } return null; } + public static boolean isFileResourcePack(Path file) { + return Files.exists(file) && (file.toString().toLowerCase(Locale.ROOT).endsWith(".zip") || Files.isRegularFile(file.resolve("pack.mcmeta"))); + } + protected final ResourcePackManager manager; protected Path file; protected final String fileName; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java index 81026e3b6d..51e618ce99 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java @@ -22,7 +22,7 @@ public ResourcePackFolder(ResourcePackManager manager, Path path) { try { meta = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class); } catch (Exception e) { - LOG.warning("Failed to parse resource pack meta", e); + LOG.error("Failed to parse resource pack meta", e); } this.meta = meta; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index ee54ac56ec..d0c86cb11b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -261,9 +261,10 @@ public void refresh() throws IOException { } public void importResourcePack(Path file) throws IOException { + if (!ResourcePackFile.isFileResourcePack(file)) return; + if (!loaded) refresh(); - Files.createDirectories(resourcePackDirectory); Path newFile = resourcePackDirectory.resolve(file.getFileName()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java index ddb1ee5fcf..90df66efe3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java @@ -31,7 +31,7 @@ public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOExce try { metaTemp = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class); } catch (Exception e) { - LOG.warning("Failed to parse resource pack meta", e); + LOG.error("Failed to parse resource pack meta", e); } var iconEntry = zipFileTree.getEntry("/pack.png"); From 106b5bbfcb7a8ad62eb8d0b79b523cb4386391d2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 3 Jan 2026 19:15:53 +0800 Subject: [PATCH 59/61] update --- .../ui/versions/ResourcePackListPage.java | 26 +++++++++++-------- .../resources/assets/lang/I18N.properties | 2 +- .../resources/assets/lang/I18N_zh.properties | 2 +- .../assets/lang/I18N_zh_CN.properties | 2 +- .../hmcl/mod/ResourcePackFolder.java | 2 +- .../hmcl/mod/ResourcePackManager.java | 25 ++++++++++-------- .../hmcl/mod/ResourcePackZipFile.java | 2 +- 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index 47a6a8daf9..d6d3792b76 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -21,13 +21,9 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.util.Duration; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; -import org.jackhuang.hmcl.mod.ResourcePackFile; -import org.jackhuang.hmcl.mod.ResourcePackManager; import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; @@ -81,7 +77,7 @@ public final class ResourcePackListPage extends ListPageBase Files.isDirectory(file) || file.getFileName().toString().endsWith(".zip"), this::addFiles); + FXUtils.applyDragListener(this, ResourcePackFile::isFileResourcePack, this::addFiles); } @Override @@ -135,13 +131,21 @@ public void refresh() { public void addFiles(List files) { if (resourcePackManager == null) return; - try { - for (Path file : files) { + List failures = new ArrayList<>(); + for (Path file : files) { + try { resourcePackManager.importResourcePack(file); + } catch (Exception e) { + LOG.warning("Failed to add resource pack", e); + failures.add(file); } - } catch (IOException e) { - LOG.warning("Failed to add resource packs", e); - Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + if (!failures.isEmpty()) { + StringBuilder failure = new StringBuilder(i18n("resourcepack.add.failed")); + for (Path file: failures) { + failure.append(System.lineSeparator()).append(file.toString()); + } + Controllers.dialog(failure.toString(), i18n("message.error"), MessageDialogPane.MessageType.ERROR); } refresh(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index fe27c0a8c8..4e912fbb94 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1213,7 +1213,7 @@ repositories.chooser.title=Choose download source for JavaFX resourcepack=Resource Packs resourcepack.add=Add -resourcepack.add.failed=Failed to add resource pack +resourcepack.add.failed=Failed to add resource packs: resourcepack.compatible=Compatible resourcepack.delete.failed=Failed to delete resource pack resourcepack.download=Download diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index b6248feaaf..2927dedcc6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1005,7 +1005,7 @@ repositories.chooser.title=選取 JavaFX 下載源 resourcepack=資源包 resourcepack.add=新增資源包 -resourcepack.add.failed=新增資源包失敗 +resourcepack.add.failed=新增資源包失敗: resourcepack.compatible=適用於此版本 resourcepack.delete.failed=刪除資源包失敗 resourcepack.download=下載資源包 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 41e543de30..97baae23e5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1015,7 +1015,7 @@ repositories.chooser.title=选择 JavaFX 下载源 resourcepack=资源包 resourcepack.add=添加资源包 -resourcepack.add.failed=添加资源包失败 +resourcepack.add.failed=添加资源包失败: resourcepack.compatible=适用于此版本 resourcepack.delete.failed=删除资源包失败 resourcepack.download=下载资源包 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java index 51e618ce99..81026e3b6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackFolder.java @@ -22,7 +22,7 @@ public ResourcePackFolder(ResourcePackManager manager, Path path) { try { meta = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class); } catch (Exception e) { - LOG.error("Failed to parse resource pack meta", e); + LOG.warning("Failed to parse resource pack meta", e); } this.meta = meta; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java index d0c86cb11b..5cef1fba6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackManager.java @@ -260,21 +260,24 @@ public void refresh() throws IOException { return super.getLocalFiles(); } - public void importResourcePack(Path file) throws IOException { - if (!ResourcePackFile.isFileResourcePack(file)) return; - - if (!loaded) - refresh(); - Files.createDirectories(resourcePackDirectory); + public void importResourcePack(Path file) throws IOException, IllegalArgumentException { + if (ResourcePackFile.isFileResourcePack(file)) { + if (!loaded) + refresh(); + Files.createDirectories(resourcePackDirectory); + + Path newFile = resourcePackDirectory.resolve(file.getFileName()); + if (Files.isDirectory(file)) { + FileUtils.copyDirectory(file, newFile); + } else { + FileUtils.copyFile(file, newFile); + } - Path newFile = resourcePackDirectory.resolve(file.getFileName()); - if (Files.isDirectory(file)) { - FileUtils.copyDirectory(file, newFile); + addResourcePackInfo(newFile); } else { - FileUtils.copyFile(file, newFile); + throw new IllegalArgumentException("File '" + file + "' is not a resource pack"); } - addResourcePackInfo(newFile); } public boolean removeResourcePacks(Iterable resourcePacks) throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java index 90df66efe3..ddb1ee5fcf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ResourcePackZipFile.java @@ -31,7 +31,7 @@ public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOExce try { metaTemp = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class); } catch (Exception e) { - LOG.error("Failed to parse resource pack meta", e); + LOG.warning("Failed to parse resource pack meta", e); } var iconEntry = zipFileTree.getEntry("/pack.png"); From add29f882c32327e74f973a0af60f0426237b5b0 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 4 Jan 2026 18:16:04 +0800 Subject: [PATCH 60/61] update --- .../hmcl/ui/download/DownloadPage.java | 2 +- .../hmcl/ui/versions/DownloadPage.java | 54 +++++++++---------- .../ui/versions/ResourcePackListPage.java | 2 +- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index b594860e86..13cb7fdec1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -202,7 +202,7 @@ public void showModpackDownloads() { tab.select(modpackTab, false); } - public void showResourcepackDownloads() { + public void showResourcePackDownloads() { tab.select(resourcePackTab, false); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 58301299b4..04f5a8269e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -78,14 +78,10 @@ public class DownloadPage extends Control implements DecoratorPage { private SimpleMultimap> versions; public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { - this(page, addon, version, callback, null); - } - - public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback, @Nullable RemoteModRepository.Type type) { this.page = page; this.repository = page.repository; this.addon = addon; - this.type = Objects.requireNonNullElse(type, repository.getType()); + this.type = Objects.requireNonNullElse(addon.getRepositoryType(), repository.getType()); this.translations = ModTranslations.getTranslationsByRepositoryType(this.type); this.mod = translations.getModByCurseForgeId(addon.getSlug()); this.version = version; @@ -165,13 +161,13 @@ public void setFailed(boolean failed) { public void download(RemoteMod mod, RemoteMod.Version file) { if (this.callback == null) { - saveAs(mod, file); + saveAs(file); } else { this.callback.download(version.getProfile(), version.getVersion(), mod, file); } } - public void saveAs(RemoteMod mod, RemoteMod.Version file) { + public void saveAs(RemoteMod.Version file) { String extension = StringUtils.substringAfterLast(file.getFile().getFilename(), '.'); FileChooser fileChooser = new FileChooser(); @@ -302,7 +298,7 @@ protected ModDownloadPageSkin(DownloadPage control) { } else { list.getContent().addAll( ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)), - new ModItem(modVersion, control) + new ModItem(control.addon, modVersion, control) ); break; } @@ -311,26 +307,24 @@ protected ModDownloadPageSkin(DownloadPage control) { } } - for (String gameVersion : control.versions.keys().stream() + control.versions.keys().stream() .sorted(Collections.reverseOrder(GameVersionNumber::compare)) - .collect(Collectors.toList())) { - List versions = control.versions.get(gameVersion); - if (versions == null || versions.isEmpty()) { - continue; - } - - ComponentList sublist = new ComponentList(() -> { - ArrayList items = new ArrayList<>(versions.size()); - for (RemoteMod.Version v : versions) { - items.add(new ModItem(control.addon, v, control)); - } - return items; - }); - sublist.getStyleClass().add("no-padding"); - sublist.setTitle("Minecraft " + gameVersion); - - list.getContent().add(sublist); - } + .forEach(gameVersion -> { + List versions = control.versions.get(gameVersion); + if (versions == null || versions.isEmpty()) { + return; + } + ComponentList sublist = new ComponentList(() -> { + ArrayList items = new ArrayList<>(versions.size()); + for (RemoteMod.Version v : versions) { + items.add(new ModItem(control.addon, v, control)); + } + return items; + }); + sublist.getStyleClass().add("no-padding"); + sublist.setTitle("Minecraft " + gameVersion); + list.getContent().add(sublist); + }); }); } @@ -366,13 +360,13 @@ private static final class DependencyModItem extends StackPane { case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD; case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK; case SHADER_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; - default -> null; + default -> null; // Dependencies should not be modpacks, worlds or customized stuff }; RipplerContainer container = new RipplerContainer(pane); FXUtils.onClicked(container, () -> { fireEvent(new DialogCloseEvent()); - Controllers.navigate(new DownloadPage(page, addon, version, callback, type)); + Controllers.navigate(new DownloadPage(page, addon, version, callback)); }); getChildren().setAll(container); @@ -517,7 +511,7 @@ public ModVersion(RemoteMod mod, RemoteMod.Version version, DownloadPage selfPag if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { fireEvent(new DialogCloseEvent()); } - selfPage.saveAs(mod, version); + selfPage.saveAs(version); }); JFXButton cancelButton = new JFXButton(i18n("button.cancel")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java index d6d3792b76..e06270c864 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java @@ -162,7 +162,7 @@ public void onAddFiles() { } private void onDownload() { - Controllers.getDownloadPage().showResourcepackDownloads(); + Controllers.getDownloadPage().showResourcePackDownloads(); Controllers.navigate(Controllers.getDownloadPage()); } From 7765f0c3b847f7b3f64df7026d974011ecb83f3c Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 13 Jan 2026 21:53:14 +0800 Subject: [PATCH 61/61] update --- .../hmcl/mod/modinfo/PackMcMeta.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index ebcf669514..139daeeeac 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -20,24 +20,14 @@ import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; -import kala.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.ModLoaderType; -import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonSerializable; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.Validation; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.tree.ZipFileTree; import org.jetbrains.annotations.NotNull; -import java.io.IOException; import java.lang.reflect.Type; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -227,17 +217,4 @@ public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationC } } - public static LocalModFile fromFile(ModManager modManager, Path modFile, ZipFileTree tree) throws IOException, JsonParseException { - ZipArchiveEntry mcmod = tree.getEntry("pack.mcmeta"); - if (mcmod == null) - throw new IOException("File " + modFile + " is not a resource pack."); - PackMcMeta metadata = JsonUtils.fromNonNullJsonFully(tree.getInputStream(mcmod), PackMcMeta.class); - return new LocalModFile( - modManager, - modManager.getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.PACK), - modFile, - FileUtils.getNameWithoutExtension(modFile), - metadata.pack.description, - "", "", "", "", ""); - } }