Skip to content

Commit ef3df92

Browse files
CiiLu3gf8jv4dvGlavo
authored
feat: 资源包管理 (#4475)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> Co-authored-by: Glavo <zjx001202@gmail.com>
1 parent 04b300f commit ef3df92

File tree

12 files changed

+482
-49
lines changed

12 files changed

+482
-49
lines changed

HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ public void showModpackDownloads() {
200200
tab.select(modpackTab, false);
201201
}
202202

203+
public void showResourcepackDownloads() {
204+
tab.select(resourcePackTab, false);
205+
}
206+
203207
public DownloadListPage showModDownloads() {
204208
tab.select(modTab, false);
205209
return modTab.getNode();
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package org.jackhuang.hmcl.ui.versions;
2+
3+
import com.jfoenix.controls.JFXButton;
4+
import com.jfoenix.controls.JFXListView;
5+
import javafx.beans.binding.Bindings;
6+
import javafx.geometry.Insets;
7+
import javafx.geometry.Pos;
8+
import javafx.scene.control.Skin;
9+
import javafx.scene.control.SkinBase;
10+
import javafx.scene.image.Image;
11+
import javafx.scene.image.ImageView;
12+
import javafx.scene.layout.BorderPane;
13+
import javafx.scene.layout.HBox;
14+
import javafx.scene.layout.Priority;
15+
import javafx.scene.layout.StackPane;
16+
import javafx.stage.FileChooser;
17+
import org.jackhuang.hmcl.mod.LocalModFile;
18+
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
19+
import org.jackhuang.hmcl.setting.Profile;
20+
import org.jackhuang.hmcl.task.Schedulers;
21+
import org.jackhuang.hmcl.task.Task;
22+
import org.jackhuang.hmcl.ui.Controllers;
23+
import org.jackhuang.hmcl.ui.FXUtils;
24+
import org.jackhuang.hmcl.ui.ListPageBase;
25+
import org.jackhuang.hmcl.ui.SVG;
26+
import org.jackhuang.hmcl.ui.construct.*;
27+
import org.jackhuang.hmcl.util.Holder;
28+
import org.jackhuang.hmcl.util.io.FileUtils;
29+
30+
import java.io.ByteArrayInputStream;
31+
import java.io.IOException;
32+
import java.lang.ref.WeakReference;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
35+
import java.util.Comparator;
36+
import java.util.List;
37+
import java.util.Objects;
38+
import java.util.stream.Stream;
39+
40+
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
41+
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
42+
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
43+
44+
public final class ResourcepackListPage extends ListPageBase<ResourcepackListPage.ResourcepackInfoObject> implements VersionPage.VersionLoadable {
45+
private Path resourcepackDirectory;
46+
47+
public ResourcepackListPage() {
48+
FXUtils.applyDragListener(this, file -> file.getFileName().toString().endsWith(".zip"), this::addFiles);
49+
}
50+
51+
@Override
52+
protected Skin<?> createDefaultSkin() {
53+
return new ResourcepackListPageSkin(this);
54+
}
55+
56+
@Override
57+
public void loadVersion(Profile profile, String version) {
58+
this.resourcepackDirectory = profile.getRepository().getResourcepacksDirectory(version);
59+
60+
try {
61+
if (!Files.exists(resourcepackDirectory)) {
62+
Files.createDirectories(resourcepackDirectory);
63+
}
64+
} catch (IOException e) {
65+
LOG.warning("Failed to create resourcepack directory" + resourcepackDirectory, e);
66+
}
67+
refresh();
68+
}
69+
70+
public void refresh() {
71+
if (resourcepackDirectory == null || !Files.isDirectory(resourcepackDirectory)) return;
72+
setLoading(true);
73+
Task.supplyAsync(Schedulers.io(), () -> {
74+
try (Stream<Path> stream = Files.list(resourcepackDirectory)) {
75+
return stream.sorted(Comparator.comparing(FileUtils::getName))
76+
.flatMap(item -> {
77+
try {
78+
return Stream.of(ResourcepackFile.parse(item)).filter(Objects::nonNull).map(ResourcepackInfoObject::new);
79+
} catch (IOException e) {
80+
LOG.warning("Failed to load resourcepack " + item, e);
81+
return Stream.empty();
82+
}
83+
})
84+
.toList();
85+
}
86+
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
87+
if (exception == null) {
88+
getItems().setAll(result);
89+
} else {
90+
LOG.warning("Failed to load resourcepacks", exception);
91+
getItems().clear();
92+
}
93+
setLoading(false);
94+
})).start();
95+
}
96+
97+
public void addFiles(List<Path> files) {
98+
if (resourcepackDirectory == null) return;
99+
100+
try {
101+
for (Path file : files) {
102+
Path target = resourcepackDirectory.resolve(file.getFileName());
103+
if (!Files.exists(target)) {
104+
Files.copy(file, target);
105+
}
106+
}
107+
} catch (IOException e) {
108+
LOG.warning("Failed to add resourcepacks", e);
109+
Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
110+
}
111+
112+
refresh();
113+
}
114+
115+
public void onAddFiles() {
116+
FileChooser fileChooser = new FileChooser();
117+
fileChooser.setTitle(i18n("resourcepack.add"));
118+
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
119+
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
120+
if (files != null && !files.isEmpty()) {
121+
addFiles(files);
122+
}
123+
}
124+
125+
private void onDownload() {
126+
Controllers.getDownloadPage().showResourcepackDownloads();
127+
Controllers.navigate(Controllers.getDownloadPage());
128+
}
129+
130+
private static final class ResourcepackListPageSkin extends SkinBase<ResourcepackListPage> {
131+
private final JFXListView<ResourcepackInfoObject> listView;
132+
133+
private ResourcepackListPageSkin(ResourcepackListPage control) {
134+
super(control);
135+
136+
StackPane pane = new StackPane();
137+
pane.setPadding(new Insets(10));
138+
pane.getStyleClass().addAll("notice-pane");
139+
140+
ComponentList root = new ComponentList();
141+
root.getStyleClass().add("no-padding");
142+
listView = new JFXListView<>();
143+
144+
HBox toolbar = new HBox();
145+
toolbar.setAlignment(Pos.CENTER_LEFT);
146+
toolbar.setPickOnBounds(false);
147+
toolbar.getChildren().setAll(
148+
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh),
149+
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles),
150+
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload)
151+
);
152+
root.getContent().add(toolbar);
153+
154+
SpinnerPane center = new SpinnerPane();
155+
ComponentList.setVgrow(center, Priority.ALWAYS);
156+
center.getStyleClass().add("large-spinner-pane");
157+
center.loadingProperty().bind(control.loadingProperty());
158+
159+
Holder<Object> lastCell = new Holder<>();
160+
listView.setCellFactory(x -> new ResourcepackListCell(listView, lastCell, control));
161+
Bindings.bindContent(listView.getItems(), control.getItems());
162+
163+
center.setContent(listView);
164+
root.getContent().add(center);
165+
166+
pane.getChildren().setAll(root);
167+
getChildren().setAll(pane);
168+
}
169+
}
170+
171+
public static class ResourcepackInfoObject {
172+
private final ResourcepackFile file;
173+
private WeakReference<Image> iconCache;
174+
175+
public ResourcepackInfoObject(ResourcepackFile file) {
176+
this.file = file;
177+
}
178+
179+
public ResourcepackFile getFile() {
180+
return file;
181+
}
182+
183+
Image getIcon() {
184+
Image image = null;
185+
if (iconCache != null && (image = iconCache.get()) != null) {
186+
return image;
187+
}
188+
byte[] iconData = file.getIcon();
189+
if (iconData != null) {
190+
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) {
191+
image = new Image(inputStream, 64, 64, true, true);
192+
} catch (Exception e) {
193+
LOG.warning("Failed to load resourcepack icon " + file.getPath(), e);
194+
}
195+
}
196+
197+
if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 ||
198+
(Math.abs(image.getWidth() - image.getHeight()) >= 1)) {
199+
image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
200+
}
201+
iconCache = new WeakReference<>(image);
202+
return image;
203+
}
204+
}
205+
206+
private static final class ResourcepackListCell extends MDListCell<ResourcepackInfoObject> {
207+
private final ImageView imageView = new ImageView();
208+
private final TwoLineListItem content = new TwoLineListItem();
209+
private final JFXButton btnReveal = new JFXButton();
210+
private final JFXButton btnDelete = new JFXButton();
211+
private final ResourcepackListPage page;
212+
213+
public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, Holder<Object> lastCell, ResourcepackListPage page) {
214+
super(listView, lastCell);
215+
216+
this.page = page;
217+
218+
BorderPane root = new BorderPane();
219+
root.getStyleClass().add("md-list-cell");
220+
root.setPadding(new Insets(8));
221+
222+
HBox left = new HBox(8);
223+
left.setAlignment(Pos.CENTER);
224+
FXUtils.limitSize(imageView, 32, 32);
225+
left.getChildren().add(imageView);
226+
left.setPadding(new Insets(0, 8, 0, 0));
227+
FXUtils.setLimitWidth(left, 48);
228+
root.setLeft(left);
229+
230+
HBox.setHgrow(content, Priority.ALWAYS);
231+
root.setCenter(content);
232+
233+
btnReveal.getStyleClass().add("toggle-icon4");
234+
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
235+
236+
btnDelete.getStyleClass().add("toggle-icon4");
237+
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
238+
239+
HBox right = new HBox(8);
240+
right.setAlignment(Pos.CENTER_RIGHT);
241+
right.getChildren().setAll(btnReveal, btnDelete);
242+
root.setRight(right);
243+
244+
getContainer().getChildren().add(new RipplerContainer(root));
245+
}
246+
247+
@Override
248+
protected void updateControl(ResourcepackListPage.ResourcepackInfoObject item, boolean empty) {
249+
if (empty || item == null) {
250+
return;
251+
}
252+
253+
ResourcepackFile file = item.getFile();
254+
imageView.setImage(item.getIcon());
255+
256+
content.setTitle(file.getName());
257+
LocalModFile.Description description = file.getDescription();
258+
content.setSubtitle(description != null ? description.toString() : "");
259+
260+
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
261+
btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getPath()));
262+
263+
btnDelete.setOnAction(event ->
264+
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
265+
() -> onDelete(file), null));
266+
}
267+
268+
private void onDelete(ResourcepackFile file) {
269+
try {
270+
if (Files.isDirectory(file.getPath())) {
271+
FileUtils.deleteDirectory(file.getPath());
272+
} else {
273+
Files.delete(file.getPath());
274+
}
275+
page.refresh();
276+
} catch (IOException e) {
277+
Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
278+
LOG.warning("Failed to delete resourcepack", e);
279+
}
280+
}
281+
}
282+
}

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
5656
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
5757
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
5858
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
59+
private final TabHeader.Tab<ResourcepackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
5960
private final TransitionPane transitionPane = new TransitionPane();
6061
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
6162
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
@@ -67,10 +68,11 @@ public VersionPage() {
6768
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
6869
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
6970
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
71+
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new));
7072
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
7173
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));
7274

73-
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab);
75+
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
7476
tab.select(versionSettingsTab);
7577

7678
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
@@ -130,6 +132,8 @@ public void loadVersion(String version, Profile profile) {
130132
installerListTab.getNode().loadVersion(profile, version);
131133
if (modListTab.isInitialized())
132134
modListTab.getNode().loadVersion(profile, version);
135+
if (resourcePackTab.isInitialized())
136+
resourcePackTab.getNode().loadVersion(profile, version);
133137
if (worldListTab.isInitialized())
134138
worldListTab.getNode().loadVersion(profile, version);
135139
if (schematicsTab.isInitialized())
@@ -238,6 +242,7 @@ protected Skin(VersionPage control) {
238242
.addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL)
239243
.addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL)
240244
.addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL)
245+
.addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE)
241246
.addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC)
242247
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL);
243248
VBox.setVgrow(sideBar, Priority.ALWAYS);

HMCL/src/main/resources/assets/lang/I18N.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,11 @@ repositories.chooser=HMCL requires JavaFX to work.\n\
12071207
repositories.chooser.title=Choose download source for JavaFX
12081208

12091209
resourcepack=Resource Packs
1210+
resourcepack.add=Add
1211+
resourcepack.manage=Resource Packs
1212+
resourcepack.download=Download
1213+
resourcepack.add.failed=Failed to add resource pack
1214+
resourcepack.delete.failed=Failed to delete resource pack
12101215
resourcepack.download.title=Download Resource Pack - %1s
12111216

12121217
reveal.in_file_manager=Reveal in File Manager

HMCL/src/main/resources/assets/lang/I18N_zh.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,11 @@ repositories.chooser=缺少 JavaFX 執行環境。HMCL 需要 JavaFX 才能正
10001000
repositories.chooser.title=選取 JavaFX 下載源
10011001

10021002
resourcepack=資源包
1003+
resourcepack.add=新增資源包
1004+
resourcepack.manage=資源包管理
1005+
resourcepack.download=下載資源包
1006+
resourcepack.add.failed=新增資源包失敗
1007+
resourcepack.delete.failed=刪除資源包失敗
10031008
resourcepack.download.title=資源包下載 - %1s
10041009

10051010
reveal.in_file_manager=在檔案管理員中查看

HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,11 @@ repositories.chooser=缺少 JavaFX 运行环境。HMCL 需要 JavaFX 才能正
10101010
repositories.chooser.title=选择 JavaFX 下载源
10111011

10121012
resourcepack=资源包
1013+
resourcepack.add=添加资源包
1014+
resourcepack.manage=资源包管理
1015+
resourcepack.download=下载资源包
1016+
resourcepack.add.failed=添加资源包失败
1017+
resourcepack.delete.failed=删除资源包失败
10131018
resourcepack.download.title=资源包下载 - %1s
10141019

10151020
reveal.in_file_manager=在文件管理器中查看

HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,8 @@ public String toString() {
564564
.append("baseDirectory", baseDirectory)
565565
.toString();
566566
}
567+
568+
public Path getResourcepacksDirectory(String id) {
569+
return getRunDirectory(id).resolve("resourcepacks");
570+
}
567571
}

HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ private Optional<Pack> loadSinglePackFromZipFile(Path path) {
219219
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
220220
try {
221221
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class);
222-
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.getPackInfo().getDescription(), this));
222+
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.pack().description(), this));
223223
} catch (JsonParseException e) {
224224
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
225225
} catch (IOException e) {

0 commit comments

Comments
 (0)