From f2145bbb95bc9cabb26fd690a83b0d995ff4edc9 Mon Sep 17 00:00:00 2001 From: xiaomao Date: Fri, 24 Jul 2026 14:18:12 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=A8=A1=E7=BB=84/?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E5=8C=85/=E5=85=89=E5=BD=B1=E5=B7=B2?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/DownloadListPage.java | 85 ++++++++++++++++++- .../hmcl/ui/versions/DownloadPage.java | 48 ++++++++++- .../resources/assets/lang/I18N.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../hmcl/game/DefaultGameRepository.java | 5 ++ .../jackhuang/hmcl/game/GameRepository.java | 6 ++ 6 files changed, 144 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index b54d3c1199a..9582e990209 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -48,6 +48,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -59,6 +60,9 @@ import org.jetbrains.annotations.Nullable; import java.net.URI; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; @@ -236,6 +240,65 @@ protected Skin createDefaultSkin() { return new ModDownloadListPageSkin(this); } + private static void checkInstalledInList(DownloadListPage page, RemoteAddon addon, java.util.function.Consumer callback) { + HMCLGameRepository.InstanceReference ref = page.getInstanceReference(); + System.out.println("[DEBUG] checkInstalledInList: addon=" + addon.slug() + ", repoType=" + page.repository.getType() + ", ref=" + ref); + if (ref.repository() == null || ref.instanceId() == null) { + System.out.println("[DEBUG] checkInstalledInList: ref is null, returning false"); + callback.accept(false); + return; + } + + Path targetDir; + switch (page.repository.getType()) { + case MOD: + targetDir = ref.repository().getModsDirectory(ref.instanceId()); + break; + case RESOURCE_PACK: + targetDir = ref.repository().getResourcePackDirectory(ref.instanceId()); + break; + case SHADER_PACK: + targetDir = ref.repository().getShaderPackDirectory(ref.instanceId()); + break; + default: + System.out.println("[DEBUG] checkInstalledInList: unsupported type, returning false"); + callback.accept(false); + return; + } + + System.out.println("[DEBUG] checkInstalledInList: targetDir=" + targetDir); + Path finalTargetDir = targetDir; + Task.supplyAsync(() -> { + if (!Files.isDirectory(finalTargetDir)) { + System.out.println("[DEBUG] checkInstalledInList: directory does not exist: " + finalTargetDir); + return false; + } + String slug = addon.slug().toLowerCase(); + // Also match slugs where hyphens/underscores are removed (e.g. slug "fresh-animations" matches filename "FreshAnimations_v1.10.4.zip") + String slugNormalized = slug.replaceAll("[^a-z0-9]", ""); + try (var list = Files.list(finalTargetDir)) { + boolean found = list.map(Path::getFileName) + .map(Path::toString) + .anyMatch(name -> { + String lowerName = name.toLowerCase(); + boolean match = lowerName.contains(slug) || lowerName.replaceAll("[^a-z0-9]", "").contains(slugNormalized); + if (match) System.out.println("[DEBUG] checkInstalledInList: matched " + name + " for slug " + slug); + return match; + }); + System.out.println("[DEBUG] checkInstalledInList: result=" + found + " for slug=" + slug + " (normalized=" + slugNormalized + ")"); + return found; + } catch (IOException e) { + System.out.println("[DEBUG] checkInstalledInList: IOException=" + e.getMessage()); + return false; + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) { + System.out.println("[DEBUG] checkInstalledInList: exception=" + exception.getMessage()); + } + callback.accept(result != null && result); + }).start(); + } + private static class ModDownloadListPageSkin extends SkinBase { private final JFXListView listView = new JFXListView<>(); private final RemoteImageLoader iconLoader; @@ -548,6 +611,7 @@ protected ModDownloadListPageSkin(DownloadListPage control) { private final TwoLineListItem content = new TwoLineListItem(); private final ImageContainer imageContainer = new ImageContainer(40); + private final Label installedLabel = new Label(); { setPadding(PADDING); @@ -559,7 +623,14 @@ protected ModDownloadListPageSkin(DownloadListPage control) { imageContainer.setMouseTransparent(true); - container.getChildren().setAll(imageContainer, content); + installedLabel.setGraphic(SVG.CHECK_CIRCLE.createIcon(16)); + installedLabel.setText(i18n("addon.installed")); + installedLabel.getStyleClass().add("label-installed"); + installedLabel.setVisible(false); + installedLabel.setManaged(false); + installedLabel.setMouseTransparent(true); + + container.getChildren().setAll(imageContainer, content, installedLabel); HBox.setHgrow(content, Priority.ALWAYS); this.graphic = new RipplerContainer(container); @@ -597,6 +668,18 @@ protected void updateItem(RemoteAddon item, boolean empty) { content.addTag(getSkinnable().getLocalizedCategory(category, null)); } iconLoader.load(imageContainer.imageProperty(), item.iconUrl()); + + // Check installed status + installedLabel.setVisible(false); + installedLabel.setManaged(false); + RemoteAddon currentItem = item; + checkInstalledInList(getSkinnable(), item, installed -> { + if (getItem() == currentItem) { + installedLabel.setVisible(installed); + installedLabel.setManaged(installed); + } + }); + setGraphic(wrapper); } } 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 5a8b4f42816..bf5d54ea8e8 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 @@ -49,6 +49,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.stream.Stream; @@ -422,6 +423,14 @@ private static final class AddonItem extends StackPane { VBox pane = new VBox(8); pane.setPadding(new Insets(8, 0, 8, 0)); + Label installedLabel = new Label(); + installedLabel.setGraphic(SVG.CHECK_CIRCLE.createIcon(16)); + installedLabel.setText(i18n("addon.installed")); + installedLabel.getStyleClass().add("label-installed"); + installedLabel.setVisible(false); + installedLabel.setManaged(false); + installedLabel.setMouseTransparent(true); + { HBox descPane = new HBox(8); descPane.setPadding(new Insets(0, 8, 0, 8)); @@ -476,12 +485,18 @@ private static final class AddonItem extends StackPane { } } - descPane.getChildren().setAll(graphicPane, content); + descPane.getChildren().setAll(graphicPane, content, installedLabel); } pane.getChildren().add(descPane); } + // Check if the file is already installed in the target instance + checkInstalled(selfPage, dataItem, installed -> { + installedLabel.setVisible(installed); + installedLabel.setManaged(installed); + }); + RipplerContainer container = new RipplerContainer(pane); FXUtils.onClicked(container, () -> Controllers.dialog(new AddonVersion(mod, dataItem, selfPage))); getChildren().setAll(container); @@ -489,6 +504,37 @@ private static final class AddonItem extends StackPane { // Workaround for https://github.com/HMCL-dev/HMCL/issues/2129 this.setMinHeight(50); } + + private static void checkInstalled(DownloadPage selfPage, RemoteAddon.Version dataItem, java.util.function.Consumer callback) { + HMCLGameRepository.InstanceReference ref = selfPage.instanceReference; + if (ref.repository() == null || ref.instanceId() == null) { + callback.accept(false); + return; + } + + Path targetDir; + switch (selfPage.type) { + case MOD: + targetDir = ref.repository().getModsDirectory(ref.instanceId()); + break; + case RESOURCE_PACK: + targetDir = ref.repository().getResourcePackDirectory(ref.instanceId()); + break; + case SHADER_PACK: + targetDir = ref.repository().getShaderPackDirectory(ref.instanceId()); + break; + default: + callback.accept(false); + return; + } + + Path targetFile = targetDir.resolve(dataItem.file().filename()); + + Task.supplyAsync(() -> Files.exists(targetFile)) + .whenComplete(Schedulers.javafx(), (exists, exception) -> { + callback.accept(exists != null && exists); + }).start(); + } } private static final class AddonVersion extends JFXDialogLayout { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index cca6305dbc1..6e5c42deef2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -198,6 +198,7 @@ addon.dependency.incompatible=Incompatible Addons (Installing these addons at th addon.dependency.broken=Broken Dependencies (This addon existed before, but it does not exist anymore. Try using another download source.) addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (Snapshots) +addon.installed=Installed addon.modrinth=Modrinth archive.author=Author(s) 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 cfaf2b7d6f4..bec3dc2e7bb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -198,6 +198,7 @@ addon.dependency.incompatible=不兼容的附加内容 (同时与正在下载的 addon.dependency.broken=损坏的前置内容 (该前置内容曾经在该附加内容下载源上存在过,但现在被删除了,换个下载源试试吧) addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (快照) +addon.installed=已安装 addon.modrinth=Modrinth archive.author=作者 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 5246c715909..f214db561cb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -151,6 +151,11 @@ public Path getResourcePackDirectory(String id) { return getRunDirectory(id).resolve("resourcepacks"); } + @Override + public Path getShaderPackDirectory(String id) { + return getRunDirectory(id).resolve("shaderpacks"); + } + @Override public Path getVersionRoot(String id) { return getBaseDirectory().resolve("versions/" + id); 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 1b7cc27dcd6..aff94ce6c5a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -143,6 +143,12 @@ default Task refreshVersionsAsync() { /// @return the resource pack directory Path getResourcePackDirectory(String id); + /// Get the directory for placing shader packs. + /// + /// @param id instance id + /// @return the shader pack directory + Path getShaderPackDirectory(String id); + /** * Get minecraft jar * From 4d03b566509621f0226d9776d80e026f0bf0b97f Mon Sep 17 00:00:00 2001 From: xiaomao Date: Fri, 24 Jul 2026 14:53:17 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 127f2d56688..3c858cc1cef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -196,6 +196,7 @@ addon.dependency.incompatible=不相容附加內容 (與正在下載的檔案同 addon.dependency.broken=損壞的相依內容 (該相依內容曾經存在於附加內容下載源中,但現在已被刪除,請嘗試其他下載源) addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (快照) +addon.installed=已安裝 addon.modrinth=Modrinth archive.author=作者 From ca73ae6d94afbbc9734377873c5e4129dd93bd5d Mon Sep 17 00:00:00 2001 From: xiaomao Date: Fri, 24 Jul 2026 14:53:38 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 9582e990209..04392af3658 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -273,14 +273,14 @@ private static void checkInstalledInList(DownloadListPage page, RemoteAddon addo System.out.println("[DEBUG] checkInstalledInList: directory does not exist: " + finalTargetDir); return false; } - String slug = addon.slug().toLowerCase(); + String slug = addon.slug().toLowerCase(Locale.ROOT); // Also match slugs where hyphens/underscores are removed (e.g. slug "fresh-animations" matches filename "FreshAnimations_v1.10.4.zip") String slugNormalized = slug.replaceAll("[^a-z0-9]", ""); try (var list = Files.list(finalTargetDir)) { boolean found = list.map(Path::getFileName) .map(Path::toString) .anyMatch(name -> { - String lowerName = name.toLowerCase(); + String lowerName = name.toLowerCase(Locale.ROOT); boolean match = lowerName.contains(slug) || lowerName.replaceAll("[^a-z0-9]", "").contains(slugNormalized); if (match) System.out.println("[DEBUG] checkInstalledInList: matched " + name + " for slug " + slug); return match; From d713d69486c66e0f71a8a3e52465015a8d7fdcef Mon Sep 17 00:00:00 2001 From: xiaomao Date: Mon, 27 Jul 2026 08:34:12 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=A4=9A=E4=BD=99System.?= =?UTF-8?q?out.println?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/versions/DownloadListPage.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 04392af3658..cd79e1cbc06 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -242,9 +242,7 @@ protected Skin createDefaultSkin() { private static void checkInstalledInList(DownloadListPage page, RemoteAddon addon, java.util.function.Consumer callback) { HMCLGameRepository.InstanceReference ref = page.getInstanceReference(); - System.out.println("[DEBUG] checkInstalledInList: addon=" + addon.slug() + ", repoType=" + page.repository.getType() + ", ref=" + ref); if (ref.repository() == null || ref.instanceId() == null) { - System.out.println("[DEBUG] checkInstalledInList: ref is null, returning false"); callback.accept(false); return; } @@ -261,16 +259,13 @@ private static void checkInstalledInList(DownloadListPage page, RemoteAddon addo targetDir = ref.repository().getShaderPackDirectory(ref.instanceId()); break; default: - System.out.println("[DEBUG] checkInstalledInList: unsupported type, returning false"); callback.accept(false); return; } - System.out.println("[DEBUG] checkInstalledInList: targetDir=" + targetDir); Path finalTargetDir = targetDir; Task.supplyAsync(() -> { if (!Files.isDirectory(finalTargetDir)) { - System.out.println("[DEBUG] checkInstalledInList: directory does not exist: " + finalTargetDir); return false; } String slug = addon.slug().toLowerCase(Locale.ROOT); @@ -282,18 +277,14 @@ private static void checkInstalledInList(DownloadListPage page, RemoteAddon addo .anyMatch(name -> { String lowerName = name.toLowerCase(Locale.ROOT); boolean match = lowerName.contains(slug) || lowerName.replaceAll("[^a-z0-9]", "").contains(slugNormalized); - if (match) System.out.println("[DEBUG] checkInstalledInList: matched " + name + " for slug " + slug); return match; }); - System.out.println("[DEBUG] checkInstalledInList: result=" + found + " for slug=" + slug + " (normalized=" + slugNormalized + ")"); return found; } catch (IOException e) { - System.out.println("[DEBUG] checkInstalledInList: IOException=" + e.getMessage()); return false; } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { - System.out.println("[DEBUG] checkInstalledInList: exception=" + exception.getMessage()); } callback.accept(result != null && result); }).start();