From ef3df925926fc838f38a90b3904c334466d93f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:17:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B5=84=E6=BA=90=E5=8C=85=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20(#4475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> Co-authored-by: Glavo --- .../hmcl/ui/download/DownloadPage.java | 4 + .../ui/versions/ResourcepackListPage.java | 282 ++++++++++++++++++ .../hmcl/ui/versions/VersionPage.java | 7 +- .../resources/assets/lang/I18N.properties | 5 + .../resources/assets/lang/I18N_zh.properties | 5 + .../assets/lang/I18N_zh_CN.properties | 5 + .../hmcl/game/DefaultGameRepository.java | 4 + .../java/org/jackhuang/hmcl/mod/Datapack.java | 2 +- .../hmcl/mod/modinfo/PackMcMeta.java | 55 +--- .../hmcl/resourcepack/ResourcepackFile.java | 30 ++ .../hmcl/resourcepack/ResourcepackFolder.java | 60 ++++ .../resourcepack/ResourcepackZipFile.java | 72 +++++ 12 files changed, 482 insertions(+), 49 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index fc66f761d..5d071d453 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -200,6 +200,10 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage tab.select(modpackTab, false); } + public void showResourcepackDownloads() { + tab.select(resourcePackTab, false); + } + public DownloadListPage showModDownloads() { tab.select(modTab, false); return modTab.getNode(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java new file mode 100644 index 000000000..acd07b1d5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java @@ -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 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 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 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 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 { + private final JFXListView 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 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 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 { + 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 listView, Holder 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); + } + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 331f334e3..34ded2d75 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -56,6 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage private final TabHeader.Tab modListTab = new TabHeader.Tab<>("modListTab"); private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); + private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); private final TransitionPane transitionPane = new TransitionPane(); private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); private final ObjectProperty version = new SimpleObjectProperty<>(); @@ -67,10 +68,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage 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); @@ -130,6 +132,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage 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()) @@ -238,6 +242,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage .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); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 1c7964d5c..5fabd687e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index b35d64089..6ccc3dd27 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -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=在檔案管理員中查看 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 cb79828ca..0e2e57588 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -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=在文件管理器中查看 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 df8f45326..42d5fcafe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -564,4 +564,8 @@ public class DefaultGameRepository implements GameRepository { .append("baseDirectory", baseDirectory) .toString(); } + + public Path getResourcepacksDirectory(String id) { + return getRunDirectory(id).resolve("resourcepacks"); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java index 09093ac9b..48081fe48 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java @@ -219,7 +219,7 @@ public class Datapack { private Optional 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) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index 3e9c6091c..80b89d259 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -23,9 +23,9 @@ import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModManager; -import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonSerializable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.FileUtils; @@ -36,28 +36,12 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -@Immutable -public class PackMcMeta implements Validation { - @SerializedName("pack") - private final PackInfo pack; - - public PackMcMeta() { - this(new PackInfo()); - } - - public PackMcMeta(PackInfo packInfo) { - this.pack = packInfo; - } - - public PackInfo getPackInfo() { - return pack; - } - +@JsonSerializable +public record PackMcMeta(@SerializedName("pack") PackInfo pack) implements Validation { @Override public void validate() throws JsonParseException { if (pack == null) @@ -65,29 +49,10 @@ public class PackMcMeta implements Validation { } @JsonAdapter(PackInfoDeserializer.class) - public static class PackInfo { - @SerializedName("pack_format") - private final int packFormat; - - @SerializedName("min_format") - private final PackVersion minPackVersion; - @SerializedName("max_format") - private final PackVersion maxPackVersion; - - @SerializedName("description") - private final LocalModFile.Description description; - - public PackInfo() { - this(0, PackVersion.UNSPECIFIED, PackVersion.UNSPECIFIED, new LocalModFile.Description(Collections.emptyList())); - } - - public PackInfo(int packFormat, PackVersion minPackVersion, PackVersion maxPackVersion, LocalModFile.Description description) { - this.packFormat = packFormat; - this.minPackVersion = minPackVersion; - this.maxPackVersion = maxPackVersion; - this.description = description; - } - + public record PackInfo(@SerializedName("pack_format") int packFormat, + @SerializedName("min_format") PackVersion minPackVersion, + @SerializedName("max_format") PackVersion maxPackVersion, + @SerializedName("description") LocalModFile.Description description) { public PackVersion getEffectiveMinVersion() { return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0); } @@ -95,10 +60,6 @@ public class PackMcMeta implements Validation { public PackVersion getEffectiveMaxVersion() { return !maxPackVersion.isUnspecified() ? maxPackVersion : new PackVersion(packFormat, 0); } - - public LocalModFile.Description getDescription() { - return description; - } } public record PackVersion(int majorVersion, int minorVersion) implements Comparable { @@ -148,7 +109,7 @@ public class PackMcMeta implements Validation { } } - public static class PackInfoDeserializer implements JsonDeserializer { + public static final class PackInfoDeserializer implements JsonDeserializer { private List pairToPart(List> lists, String color) { List parts = new ArrayList<>(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java new file mode 100644 index 000000000..6bfea9392 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java @@ -0,0 +1,30 @@ +package org.jackhuang.hmcl.resourcepack; + +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; + +public interface ResourcepackFile { + @Nullable + LocalModFile.Description getDescription(); + + String getName(); + + Path getPath(); + + byte @Nullable [] getIcon(); + + static ResourcepackFile parse(Path path) throws IOException { + String fileName = path.getFileName().toString(); + if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) { + return new ResourcepackZipFile(path); + } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { + return new ResourcepackFolder(path); + } + return null; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java new file mode 100644 index 000000000..14b5d3c63 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java @@ -0,0 +1,60 @@ +package org.jackhuang.hmcl.resourcepack; + +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class ResourcepackFolder implements ResourcepackFile { + private final Path path; + private final LocalModFile.Description description; + private final byte @Nullable [] icon; + + public ResourcepackFolder(Path path) { + this.path = path; + + LocalModFile.Description description = null; + try { + description = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class).pack().description(); + } catch (Exception e) { + LOG.warning("Failed to parse resourcepack meta", e); + } + + byte[] icon; + try { + icon = Files.readAllBytes(path.resolve("pack.png")); + } catch (IOException e) { + icon = null; + LOG.warning("Failed to read resourcepack icon", e); + } + this.icon = icon; + + this.description = description; + } + + @Override + public String getName() { + return path.getFileName().toString(); + } + + @Override + public Path getPath() { + return path; + } + + @Override + public LocalModFile.Description getDescription() { + return description; + } + + @Override + public byte @Nullable [] getIcon() { + return icon; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java new file mode 100644 index 000000000..8f35f933b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java @@ -0,0 +1,72 @@ +package org.jackhuang.hmcl.resourcepack; + +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.tree.ZipFileTree; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class ResourcepackZipFile implements ResourcepackFile { + private final Path path; + private final byte @Nullable [] icon; + private final String name; + private final LocalModFile.Description description; + + public ResourcepackZipFile(Path path) throws IOException { + this.path = path; + LocalModFile.Description description = null; + + byte[] icon = null; + + try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(path))) { + try { + description = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class).pack().description(); + } catch (Exception e) { + LOG.warning("Failed to parse resourcepack meta", e); + } + + var iconEntry = zipFileTree.getEntry("/pack.png"); + if (iconEntry != null) { + try (InputStream is = zipFileTree.getInputStream(iconEntry)) { + icon = is.readAllBytes(); + } catch (Exception e) { + LOG.warning("Failed to load resourcepack icon", e); + } + } + } + + this.icon = icon; + this.description = description; + + name = FileUtils.getNameWithoutExtension(path); + } + + @Override + public String getName() { + return name; + } + + @Override + public Path getPath() { + return path; + } + + @Override + public LocalModFile.Description getDescription() { + return description; + } + + @Override + public byte @Nullable [] getIcon() { + return icon; + } +} +