Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
37 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
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 @@ -213,6 +213,10 @@ public void showModpackDownloads() {
tab.select(modpackTab);
}

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

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

import com.jfoenix.controls.JFXButton;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
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.stage.FileChooser;
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public class ResourcepackListPage extends ListPageBase<ResourcepackListPage.ResourcepackItem> implements VersionPage.VersionLoadable {
private Path resourcepackDirectory;

public ResourcepackListPage() {
FXUtils.applyDragListener(this, file -> file.isFile() && file.getName().endsWith(".zip"), files -> addFiles(files.stream().map(File::toPath).collect(Collectors.toList())));
}

private static Node createIcon(Path img) {
ImageView imageView = new ImageView();
imageView.setFitWidth(32);
imageView.setFitHeight(32);

if (Files.exists(img)) {
try (InputStream is = Files.newInputStream(img)) {
Image image = new Image(is);
imageView.setImage(image);
} catch (IOException ignored) {
}
}

if (imageView.getImage() == null) {
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
}

return imageView;
}

@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.error("Failed to create resourcepack directory", e);
}
refresh();
}

public void refresh() {
Task.runAsync(Schedulers.javafx(), this::load).whenComplete(Schedulers.javafx(), (result, exception) -> setLoading(false)).start();
setLoading(true);
}

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) {
Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
LOG.warning("Failed to add resourcepacks", e);
}
}

public void onAddFiles() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("resourcepack.add"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
List<File> files = fileChooser.showOpenMultipleDialog(Controllers.getStage());
if (files != null && !files.isEmpty()) {
addFiles(files.stream().map(File::toPath).collect(Collectors.toList()));
}
}

private void load() {
itemsProperty().clear();
if (resourcepackDirectory == null || !Files.exists(resourcepackDirectory)) return;

try (Stream<Path> stream = Files.list(resourcepackDirectory)) {
stream.forEach(path -> {
try {
itemsProperty().add(new ResourcepackItem(ResourcepackFile.parse(path)));
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourcepackFile.parse() can return null, but this code doesn't check for null before creating ResourcepackItem. Add a null check to prevent NullPointerException.

Suggested change
itemsProperty().add(new ResourcepackItem(ResourcepackFile.parse(path)));
ResourcepackFile resourcepackFile = ResourcepackFile.parse(path);
if (resourcepackFile != null) {
itemsProperty().add(new ResourcepackItem(resourcepackFile));
}

Copilot uses AI. Check for mistakes.
} catch (Exception e) {
LOG.warning("Failed to load resourcepacks " + path.getFileName(), e);
}
});
} catch (IOException e) {
LOG.warning("Failed to list resourcepacks directory", e);
}

itemsProperty().sort(Comparator.comparing(item -> item.getFile().getName()));
}

private void onDownload() {
runInFX(() -> {
Controllers.getDownloadPage().showResourcepackDownloads();
Controllers.navigate(Controllers.getDownloadPage());
});
}

private static class ResourcepackListPageSkin extends ToolbarListPageSkin<ResourcepackListPage> {
protected ResourcepackListPageSkin(ResourcepackListPage control) {
super(control);
}

@Override
protected List<Node> initializeToolbar(ResourcepackListPage skinnable) {
return Arrays.asList(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, skinnable::onAddFiles), createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, skinnable::onDownload));
}
}

public class ResourcepackItem extends Control {
private final ResourcepackFile file;
// final JFXCheckBox checkBox = new JFXCheckBox();

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

@Override
protected Skin<?> createDefaultSkin() {
return new ResourcepackItemSkin(this);
}

public void onDelete() {
try {
if (file.getFile().isDirectory()) {
FileUtils.deleteDirectory(file.getFile());
} else {
Files.delete(file.getFile().toPath());
}
ResourcepackListPage.this.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);
}
}

public void onReveal() {
FXUtils.showFileInExplorer(file.getFile().toPath());
}

public ResourcepackFile getFile() {
return file;
}
}

private class ResourcepackItemSkin extends SkinBase<ResourcepackItem> {
public ResourcepackItemSkin(ResourcepackItem item) {
super(item);
BorderPane root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));

HBox left = new HBox(8);
left.setAlignment(Pos.CENTER);
left.getChildren().addAll(createIcon(item.getFile().getIcon()));
// left.getChildren().addAll(item.checkBox, createIcon(item.getFile().getIcon()));
left.setPadding(new Insets(0, 8, 0, 0));
// FXUtils.setLimitWidth(left, 64);
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented-out code blocks. These appear to be related to unused checkbox functionality and should be cleaned up.

Copilot uses AI. Check for mistakes.
FXUtils.setLimitWidth(left, 48);
root.setLeft(left);

TwoLineListItem center = new TwoLineListItem();
// center.setPadding(new Insets(0, 0, 0, 8));
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented-out code. If this padding adjustment was intentional, either apply it or remove the comment entirely.

Suggested change
// center.setPadding(new Insets(0, 0, 0, 8));

Copilot uses AI. Check for mistakes.
center.setTitle(item.getFile().getName());
center.setSubtitle(item.getFile().getDescription());
root.setCenter(center);

HBox right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnReveal = new JFXButton();
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), -1));
btnReveal.setOnAction(event -> item.onReveal());

JFXButton btnDelete = new JFXButton();
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon(Theme.blackFill(), -1));
btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), item::onDelete, null));
right.getChildren().setAll(btnReveal, btnDelete);
root.setRight(right);

this.getChildren().add(new RipplerContainer(root));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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 @@ -73,8 +74,9 @@ public VersionPage() {
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new));

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

addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);

Expand Down Expand Up @@ -139,6 +141,8 @@ public void loadVersion(String version, Profile profile) {
worldListTab.getNode().loadVersion(profile, version);
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(profile, version);
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
}

Expand Down Expand Up @@ -283,11 +287,20 @@ protected Skin(VersionPage control) {
schematicsListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.schematicsTab));
schematicsListItem.setOnAction(e -> control.tab.select(control.schematicsTab));

AdvancedListItem resourcePackListItem = new AdvancedListItem();
resourcePackListItem.getStyleClass().add("navigation-drawer-item");
resourcePackListItem.setTitle(i18n("resourcepack.manage"));
resourcePackListItem.setLeftGraphic(wrap(SVG.TEXTURE));
resourcePackListItem.setActionButtonVisible(false);
resourcePackListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.resourcePackTab));
resourcePackListItem.setOnAction(e -> control.tab.select(control.resourcePackTab));

AdvancedListBox sideBar = new AdvancedListBox()
.add(versionSettingsItem)
.add(installerListItem)
.add(modListItem)
.add(worldListItem)
.add(resourcePackListItem)
.add(schematicsListItem);
VBox.setVgrow(sideBar, Priority.ALWAYS);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 @@ -1197,6 +1197,11 @@ repositories.chooser=HMCL requires JavaFX to work.\n\
repositories.chooser.title=Choose download source for JavaFX

resourcepack=Resource Packs
resourcepack.add=Add Resource Pack
resourcepack.manage=Resource Packs
resourcepack.download=Download Resource Packs
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 @@ -990,6 +990,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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -542,4 +542,8 @@ public String toString() {
.append("baseDirectory", baseDirectory)
.toString();
}

public Path getResourcepacksDirectory(String id) {
return getRunDirectory(id).toPath().resolve("resourcepacks");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.jackhuang.hmcl.resourcepack;

import com.google.gson.JsonParser;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public interface ResourcepackFile {
String getDescription();

String getName();

File getFile();

Path getIcon();

default String parseDescriptionFromJson(String json) {
try {
return JsonParser.parseString(json).getAsJsonObject().getAsJsonObject("pack").get("description").getAsString();
} catch (Exception ignored) {
return "";
}

}

static ResourcepackFile parse(Path path) throws IOException {
if (Files.isRegularFile(path) && path.toString().toLowerCase().endsWith(".zip")) {
return new ResourcepackZipFile(path.toFile());
} else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) {
return new ResourcepackFolder(path.toFile());
}
return null;
}

}
Loading
Loading