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..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 @@ -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,56 @@ 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(); + if (ref.repository() == null || ref.instanceId() == null) { + 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: + callback.accept(false); + return; + } + + Path finalTargetDir = targetDir; + Task.supplyAsync(() -> { + if (!Files.isDirectory(finalTargetDir)) { + return false; + } + 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(Locale.ROOT); + boolean match = lowerName.contains(slug) || lowerName.replaceAll("[^a-z0-9]", "").contains(slugNormalized); + return match; + }); + return found; + } catch (IOException e) { + return false; + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) { + } + callback.accept(result != null && result); + }).start(); + } + private static class ModDownloadListPageSkin extends SkinBase { private final JFXListView listView = new JFXListView<>(); private final RemoteImageLoader iconLoader; @@ -548,6 +602,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 +614,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 +659,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 16964d04b1b..def78063834 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.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index de6bde9270d..bd8f192b976 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=作者 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 f78404b6d92..6e2c6aaa94e 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 *