Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
92d175b
update
CiiLu Sep 13, 2025
b99520d
update
CiiLu Sep 13, 2025
3866eb1
i18n
CiiLu Sep 19, 2025
69510ac
checkstyle
CiiLu Sep 19, 2025
2f71a16
Apply suggestions from code review
CiiLu Sep 19, 2025
cad53a1
Update ResourcepackZipFile.java
CiiLu Sep 24, 2025
1d4fe22
Update ResourcepackListPage.java
CiiLu Sep 24, 2025
0d1c22e
Update DefaultGameRepository.java
CiiLu Sep 24, 2025
aaab964
Merge branch 'HMCL-dev:main' into rp
CiiLu Sep 24, 2025
553b499
Merge remote-tracking branch 'origin/main' into rp
CiiLu Sep 24, 2025
e042b8f
update
CiiLu Sep 24, 2025
1a56e85
Merge remote-tracking branch 'origin/rp' into rp
CiiLu Sep 24, 2025
8d5708b
update
CiiLu Sep 25, 2025
ff58841
update
CiiLu Sep 26, 2025
7857ad4
update
CiiLu Sep 26, 2025
416e07f
update
Glavo Sep 27, 2025
e5ed6d0
update
Glavo Sep 27, 2025
f540134
update
CiiLu Sep 27, 2025
0aa3097
update
CiiLu Sep 27, 2025
6028071
update
CiiLu Sep 27, 2025
45f243d
update
CiiLu Sep 27, 2025
9f8999d
Revert "update"
Glavo Oct 10, 2025
9506910
Update HMCL/src/main/resources/assets/lang/I18N.properties
CiiLu Oct 10, 2025
2e84869
Update HMCL/src/main/resources/assets/lang/I18N.properties
CiiLu Oct 11, 2025
d6cc5f9
Update ResourcepackZipFile.java
CiiLu Oct 11, 2025
66ff6b4
update
CiiLu Oct 12, 2025
c9dc5f9
Merge remote-tracking branch 'origin/rp' into rp
CiiLu Oct 12, 2025
f358c17
Merge branch 'HMCL-dev:main' into rp
CiiLu Oct 12, 2025
d8c589a
update
CiiLu Oct 12, 2025
67c0ef6
update
CiiLu Oct 12, 2025
05ed1a8
update
CiiLu Oct 12, 2025
b84b2ad
Update ResourcepackZipFile.java
CiiLu Oct 12, 2025
0d63ba2
update
Glavo Oct 18, 2025
53ffe6f
Merge remote-tracking branch 'upstream/main' into rp
Glavo Oct 18, 2025
4a476dc
update
Glavo Oct 18, 2025
4288254
Merge branch 'HMCL-dev:main' into rp
CiiLu Nov 2, 2025
3e64bb6
update
CiiLu Nov 2, 2025
dcdbcb1
update
Glavo Nov 8, 2025
bc4eda9
Delete HMCL/src/main/resources/assets/img/unknown_pack.png
CiiLu Nov 15, 2025
8914f01
Merge branch 'HMCL-dev:main' into rp
CiiLu Nov 15, 2025
62c7f59
update
CiiLu Nov 15, 2025
1630114
fix: Checkstyle
CiiLu Nov 15, 2025
aed44b8
Merge branch 'main' into rp
CiiLu Nov 22, 2025
0c942df
Update VersionPage.java
CiiLu Nov 22, 2025
eb314c3
Update VersionPage.java
CiiLu Nov 22, 2025
b8529eb
update
Glavo Dec 6, 2025
566b971
update
Glavo Dec 6, 2025
d759d86
Merge remote-tracking branch 'origin/main' into rp
Glavo Dec 6, 2025
cf0a10b
update
Glavo Dec 6, 2025
e00b6e3
update
Glavo Dec 6, 2025
a3e3f40
update
Glavo Dec 6, 2025
775cc49
update
Glavo Dec 6, 2025
0af1558
update
Glavo Dec 6, 2025
0389149
update
Glavo Dec 6, 2025
143d09f
update
Glavo Dec 6, 2025
1e61502
update
Glavo Dec 6, 2025
72f9aa9
update
Glavo Dec 6, 2025
b65b113
update
Glavo Dec 6, 2025
92aa203
update
Glavo Dec 6, 2025
67a91a0
update
Glavo Dec 6, 2025
1aa5e5c
update
Glavo Dec 6, 2025
4125a83
update
Glavo Dec 6, 2025
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 @@ -200,6 +200,10 @@ public void showModpackDownloads() {
tab.select(modpackTab, false);
}

public void showResourcepackDownloads() {
tab.select(resourcePackTab, false);
}

public DownloadListPage showModDownloads() {
tab.select(modTab, false);
return modTab.getNode();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package org.jackhuang.hmcl.ui.versions;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Skin;
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;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
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.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.io.FileUtils;

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.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<ResourcepackListPage.ResourcepackInfoObject> implements VersionPage.VersionLoadable {
private Path resourcepackDirectory;

public ResourcepackListPage() {
FXUtils.applyDragListener(this, 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.resourcepackDirectory = profile.getRepository().getResourcepacksDirectory(version);

try {
if (!Files.exists(resourcepackDirectory)) {
Files.createDirectories(resourcepackDirectory);
}
} catch (IOException e) {
LOG.warning("Failed to create resourcepack directory" + resourcepackDirectory, e);
}
refresh();
}

public void refresh() {
if (resourcepackDirectory == null || !Files.isDirectory(resourcepackDirectory)) return;
setLoading(true);
Task.supplyAsync(Schedulers.io(), () -> {
try (Stream<Path> 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();
}
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
getItems().setAll(result);
} else {
LOG.warning("Failed to load resourcepacks", exception);
getItems().clear();
}
setLoading(false);
})).start();
}

public void addFiles(List<Path> files) {
if (resourcepackDirectory == null) return;

try {
for (Path file : files) {
Path target = resourcepackDirectory.resolve(file.getFileName());
if (!Files.exists(target)) {
Files.copy(file, target);
}
}
} catch (IOException e) {
LOG.warning("Failed to add resourcepacks", 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<Path> 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 static final class ResourcepackListPageSkin extends SkinBase<ResourcepackListPage> {
private final JFXListView<ResourcepackInfoObject> listView;

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<>();

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<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ResourcepackListCell(listView, lastCell, control));
Bindings.bindContent(listView.getItems(), control.getItems());

center.setContent(listView);
root.getContent().add(center);

pane.getChildren().setAll(root);
getChildren().setAll(pane);
}
}

public static class ResourcepackInfoObject {
private final ResourcepackFile file;
private WeakReference<Image> iconCache;

public ResourcepackInfoObject(ResourcepackFile file) {
this.file = file;
}

public ResourcepackFile getFile() {
return file;
}

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 resourcepack icon " + file.getPath(), 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<ResourcepackInfoObject> {
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;

public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, Holder<Object> lastCell, ResourcepackListPage page) {
super(listView, lastCell);

this.page = page;

BorderPane root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));

HBox left = new HBox(8);
left.setAlignment(Pos.CENTER);
FXUtils.limitSize(imageView, 32, 32);
left.getChildren().add(imageView);
left.setPadding(new Insets(0, 8, 0, 0));
FXUtils.setLimitWidth(left, 48);
root.setLeft(left);

HBox.setHgrow(content, Priority.ALWAYS);
root.setCenter(content);

btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());

btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());

HBox right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
right.getChildren().setAll(btnReveal, btnDelete);
root.setRight(right);

getContainer().getChildren().add(new RipplerContainer(root));
}

@Override
protected void updateControl(ResourcepackListPage.ResourcepackInfoObject item, boolean empty) {
if (empty || item == null) {
return;
}

ResourcepackFile file = item.getFile();
imageView.setImage(item.getIcon());

content.setTitle(file.getName());
LocalModFile.Description description = file.getDescription();
content.setSubtitle(description != null ? description.toString() : "");

FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getPath()));

btnDelete.setOnAction(event ->
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
() -> onDelete(file), null));
}

private void onDelete(ResourcepackFile file) {
try {
if (Files.isDirectory(file.getPath())) {
FileUtils.deleteDirectory(file.getPath());
} else {
Files.delete(file.getPath());
}
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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
private final TabHeader.Tab<ResourcepackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
Expand All @@ -67,10 +68,11 @@ public VersionPage() {
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));

tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab);
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
tab.select(versionSettingsTab);

addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
Expand Down Expand Up @@ -130,6 +132,8 @@ public void loadVersion(String version, Profile profile) {
installerListTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(profile, version);
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(profile, version);
if (schematicsTab.isInitialized())
Expand Down Expand Up @@ -238,6 +242,7 @@ protected Skin(VersionPage control) {
.addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL)
.addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL)
.addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL)
.addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE)
.addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC)
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL);
VBox.setVgrow(sideBar, Priority.ALWAYS);
Expand Down
5 changes: 5 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,11 @@ repositories.chooser=HMCL requires JavaFX to work.\n\
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.title=Download Resource Pack - %1s

reveal.in_file_manager=Reveal in File Manager
Expand Down
5 changes: 5 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,11 @@ repositories.chooser=缺少 JavaFX 執行環境。HMCL 需要 JavaFX 才能正
repositories.chooser.title=選取 JavaFX 下載源

resourcepack=資源包
resourcepack.add=新增資源包
resourcepack.manage=資源包管理
resourcepack.download=下載資源包
resourcepack.add.failed=新增資源包失敗
resourcepack.delete.failed=刪除資源包失敗
resourcepack.download.title=資源包下載 - %1s

reveal.in_file_manager=在檔案管理員中查看
Expand Down
5 changes: 5 additions & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,11 @@ repositories.chooser=缺少 JavaFX 运行环境。HMCL 需要 JavaFX 才能正
repositories.chooser.title=选择 JavaFX 下载源

resourcepack=资源包
resourcepack.add=添加资源包
resourcepack.manage=资源包管理
resourcepack.download=下载资源包
resourcepack.add.failed=添加资源包失败
resourcepack.delete.failed=删除资源包失败
resourcepack.download.title=资源包下载 - %1s

reveal.in_file_manager=在文件管理器中查看
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,8 @@ public String toString() {
.append("baseDirectory", baseDirectory)
.toString();
}

public Path getResourcepacksDirectory(String id) {
return getRunDirectory(id).resolve("resourcepacks");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ private Optional<Pack> loadSinglePackFromZipFile(Path path) {
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
try {
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class);
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.getPackInfo().getDescription(), this));
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.pack().description(), this));
} catch (JsonParseException e) {
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
} catch (IOException e) {
Expand Down
Loading