Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -236,6 +240,56 @@ protected Skin<?> createDefaultSkin() {
return new ModDownloadListPageSkin(this);
}

private static void checkInstalledInList(DownloadListPage page, RemoteAddon addon, java.util.function.Consumer<Boolean> 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<DownloadListPage> {
private final JFXListView<RemoteAddon> listView = new JFXListView<>();
private final RemoteImageLoader iconLoader;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -476,19 +485,56 @@ 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);

// 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<Boolean> 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 {
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=作者
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=作者
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ default Task<Void> 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
*
Expand Down