Merge 2f71a1692eb01204695dee78b24e113dad62c378 into bd9ae189f83e33a6977bbe056774c851e96fe0a7

This commit is contained in:
辞庐 2025-09-21 15:25:58 +08:00 committed by GitHub
commit ba7a3d53c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 387 additions and 1 deletions

View File

@ -212,6 +212,10 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
tab.select(modpackTab);
}
public void showResourcepackDownloads() {
tab.select(resourcePackTab);
}
public DownloadListPage showModDownloads() {
tab.select(modTab);
return modTab.getNode();

View File

@ -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)));
} 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);
FXUtils.setLimitWidth(left, 48);
root.setLeft(left);
TwoLineListItem center = new TwoLineListItem();
// center.setPadding(new Insets(0, 0, 0, 8));
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));
}
}
}

View File

@ -59,6 +59,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<>();
@ -72,8 +73,9 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
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);
@ -138,6 +140,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
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));
}
@ -282,11 +286,20 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1200,6 +1200,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

View File

@ -993,6 +993,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=在檔案管理員中查看

View File

@ -1003,6 +1003,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=在文件管理器中查看

View File

@ -559,4 +559,8 @@ public class DefaultGameRepository implements GameRepository {
.append("baseDirectory", baseDirectory)
.toString();
}
public Path getResourcepacksDirectory(String id) {
return getRunDirectory(id).toPath().resolve("resourcepacks");
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,37 @@
package org.jackhuang.hmcl.resourcepack;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
public class ResourcepackFolder implements ResourcepackFile {
private final File folder;
public ResourcepackFolder(File folder) {
this.folder = folder;
}
@Override
public String getName() {
return folder.getName();
}
@Override
public File getFile() {
return folder;
}
@Override
public String getDescription() {
try {
return parseDescriptionFromJson(Files.readString(folder.toPath().resolve("pack.mcmeta")));
} catch (Exception ignored) {
return "";
}
}
@Override
public Path getIcon() {
return folder.toPath().resolve("pack.png");
}
}

View File

@ -0,0 +1,45 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
public class ResourcepackZipFile implements ResourcepackFile {
private final FileSystem zipfs;
private final File resourcepackfile;
public ResourcepackZipFile(File resourcepackfile) throws IOException {
this.resourcepackfile = resourcepackfile;
this.zipfs = CompressingUtils.createReadOnlyZipFileSystem(resourcepackfile.toPath());
}
@Override
public String getName() {
return resourcepackfile.getName().replace(".zip", "");
}
@Override
public File getFile() {
return resourcepackfile;
}
@Override
public String getDescription() {
try {
return parseDescriptionFromJson(Files.readString(zipfs.getPath("pack.mcmeta")));
} catch (Exception ignored) {
return "";
}
}
@Override
public Path getIcon() {
return zipfs.getPath("pack.png");
}
}