feat: 资源包管理 (#4475)

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
辞庐
2025-12-06 22:17:04 +08:00
committed by GitHub
parent 04b300f453
commit ef3df92592
12 changed files with 482 additions and 49 deletions

View File

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

View File

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

View File

@@ -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<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab"); 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 TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>(); private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
@@ -67,10 +68,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false))); versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new)); installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new)); modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new)); worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::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); tab.select(versionSettingsTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
@@ -130,6 +132,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
installerListTab.getNode().loadVersion(profile, version); installerListTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized()) if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version); modListTab.getNode().loadVersion(profile, version);
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(profile, version);
if (worldListTab.isInitialized()) if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(profile, version); worldListTab.getNode().loadVersion(profile, version);
if (schematicsTab.isInitialized()) 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.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.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.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.worldListTab, i18n("world.manage"), SVG.PUBLIC)
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL); .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL);
VBox.setVgrow(sideBar, Priority.ALWAYS); VBox.setVgrow(sideBar, Priority.ALWAYS);

View File

@@ -1207,6 +1207,11 @@ repositories.chooser=HMCL requires JavaFX to work.\n\
repositories.chooser.title=Choose download source for JavaFX repositories.chooser.title=Choose download source for JavaFX
resourcepack=Resource Packs 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 resourcepack.download.title=Download Resource Pack - %1s
reveal.in_file_manager=Reveal in File Manager reveal.in_file_manager=Reveal in File Manager

View File

@@ -1000,6 +1000,11 @@ repositories.chooser=缺少 JavaFX 執行環境。HMCL 需要 JavaFX 才能正
repositories.chooser.title=選取 JavaFX 下載源 repositories.chooser.title=選取 JavaFX 下載源
resourcepack=資源包 resourcepack=資源包
resourcepack.add=新增資源包
resourcepack.manage=資源包管理
resourcepack.download=下載資源包
resourcepack.add.failed=新增資源包失敗
resourcepack.delete.failed=刪除資源包失敗
resourcepack.download.title=資源包下載 - %1s resourcepack.download.title=資源包下載 - %1s
reveal.in_file_manager=在檔案管理員中查看 reveal.in_file_manager=在檔案管理員中查看

View File

@@ -1010,6 +1010,11 @@ repositories.chooser=缺少 JavaFX 运行环境。HMCL 需要 JavaFX 才能正
repositories.chooser.title=选择 JavaFX 下载源 repositories.chooser.title=选择 JavaFX 下载源
resourcepack=资源包 resourcepack=资源包
resourcepack.add=添加资源包
resourcepack.manage=资源包管理
resourcepack.download=下载资源包
resourcepack.add.failed=添加资源包失败
resourcepack.delete.failed=删除资源包失败
resourcepack.download.title=资源包下载 - %1s resourcepack.download.title=资源包下载 - %1s
reveal.in_file_manager=在文件管理器中查看 reveal.in_file_manager=在文件管理器中查看

View File

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

View File

@@ -219,7 +219,7 @@ public class Datapack {
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) { private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
try { try {
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class); 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) { } catch (JsonParseException e) {
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e); LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
} catch (IOException e) { } catch (IOException e) {

View File

@@ -23,9 +23,9 @@ import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils; 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.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils; 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.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@Immutable @JsonSerializable
public class PackMcMeta implements Validation { public record PackMcMeta(@SerializedName("pack") PackInfo pack) 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;
}
@Override @Override
public void validate() throws JsonParseException { public void validate() throws JsonParseException {
if (pack == null) if (pack == null)
@@ -65,29 +49,10 @@ public class PackMcMeta implements Validation {
} }
@JsonAdapter(PackInfoDeserializer.class) @JsonAdapter(PackInfoDeserializer.class)
public static class PackInfo { public record PackInfo(@SerializedName("pack_format") int packFormat,
@SerializedName("pack_format") @SerializedName("min_format") PackVersion minPackVersion,
private final int packFormat; @SerializedName("max_format") PackVersion maxPackVersion,
@SerializedName("description") LocalModFile.Description description) {
@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 PackVersion getEffectiveMinVersion() { public PackVersion getEffectiveMinVersion() {
return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0); return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0);
} }
@@ -95,10 +60,6 @@ public class PackMcMeta implements Validation {
public PackVersion getEffectiveMaxVersion() { public PackVersion getEffectiveMaxVersion() {
return !maxPackVersion.isUnspecified() ? maxPackVersion : new PackVersion(packFormat, 0); return !maxPackVersion.isUnspecified() ? maxPackVersion : new PackVersion(packFormat, 0);
} }
public LocalModFile.Description getDescription() {
return description;
}
} }
public record PackVersion(int majorVersion, int minorVersion) implements Comparable<PackVersion> { public record PackVersion(int majorVersion, int minorVersion) implements Comparable<PackVersion> {
@@ -148,7 +109,7 @@ public class PackMcMeta implements Validation {
} }
} }
public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> { public static final class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
private List<LocalModFile.Description.Part> pairToPart(List<Pair<String, String>> lists, String color) { private List<LocalModFile.Description.Part> pairToPart(List<Pair<String, String>> lists, String color) {
List<LocalModFile.Description.Part> parts = new ArrayList<>(); List<LocalModFile.Description.Part> parts = new ArrayList<>();

View File

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

View File

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

View File

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