feat: 资源包管理 (#4475)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=在檔案管理員中查看
|
||||||
|
|||||||
@@ -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=在文件管理器中查看
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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<>();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user