From 92d175b52914321c3c85ae0b5a335a6d329035f5 Mon Sep 17 00:00:00 2001 From: Ciilu <109708109+Ciilu@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:03:16 +0800 Subject: [PATCH 1/5] update --- .../hmcl/ui/download/DownloadPage.java | 4 + .../ui/versions/ResourcepackListPage.java | 230 ++++++++++++++++++ .../hmcl/ui/versions/VersionPage.java | 15 +- .../resources/assets/img/unknown_pack.png | Bin 0 -> 11275 bytes .../assets/lang/I18N_zh_CN.properties | 5 + .../hmcl/game/DefaultGameRepository.java | 4 + .../hmcl/resourcepack/ResourcepackFile.java | 38 +++ .../hmcl/resourcepack/ResourcepackFolder.java | 39 +++ .../resourcepack/ResourcepackZipFile.java | 44 ++++ 9 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java create mode 100644 HMCL/src/main/resources/assets/img/unknown_pack.png 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 aee357f7b..bf86306ea 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 @@ -213,6 +213,10 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage tab.select(modpackTab); } + public void showResourcepackDownloads() { + tab.select(resourcePackTab); + } + public DownloadListPage showModDownloads() { tab.select(modTab); return modTab.getNode(); 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..559cfce19 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java @@ -0,0 +1,230 @@ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXButton; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.resourcepack.ResourcepackFile; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class ResourcepackListPage extends ListPageBase implements VersionPage.VersionLoadable { + private Path resourcepackDirectory; + + public ResourcepackListPage() { + FXUtils.applyDragListener(this, file -> file.isFile() && file.getName().endsWith(".zip"), files -> addFiles(files.stream().map(File::toPath).collect(Collectors.toList()))); + } + + private static Node createIcon(Path img) { + ImageView imageView = new ImageView(); + imageView.setFitWidth(32); + imageView.setFitHeight(32); + + if (Files.exists(img)) { + try (InputStream is = Files.newInputStream(img)) { + Image image = new Image(is); + imageView.setImage(image); + } catch (IOException ignored) { + } + } + + if (imageView.getImage() == null) { + imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png")); + } + + return imageView; + } + + @Override + protected Skin createDefaultSkin() { + return new ResourcepackListPageSkin(this); + } + + @Override + public void loadVersion(Profile profile, String version) { + this.resourcepackDirectory = profile.getRepository().getResourcepacksDirectory(version); + + try { + if (!Files.exists(resourcepackDirectory)) { + Files.createDirectories(resourcepackDirectory); + } + } catch (IOException e) { + LOG.error("Failed to create resourcepack directory", e); + } + refresh(); + } + + public void refresh() { + Task.runAsync(Schedulers.javafx(), this::load).whenComplete(Schedulers.javafx(), (result, exception) -> setLoading(false)).start(); + setLoading(true); + } + + public void addFiles(List files) { + if (resourcepackDirectory == null) return; + + try { + for (Path file : files) { + Path target = resourcepackDirectory.resolve(file.getFileName()); + if (!Files.exists(target)) { + Files.copy(file, target); + } + } + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to add resourcepacks", e); + } + } + + public void onAddFiles() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("resourcepack.add")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); + List files = fileChooser.showOpenMultipleDialog(Controllers.getStage()); + if (files != null && !files.isEmpty()) { + addFiles(files.stream().map(File::toPath).collect(Collectors.toList())); + } + } + + private void load() { + itemsProperty().clear(); + if (resourcepackDirectory == null || !Files.exists(resourcepackDirectory)) return; + + try (Stream stream = Files.list(resourcepackDirectory)) { + stream.forEach(path -> { + try { + itemsProperty().add(new ResourcepackItem(ResourcepackFile.parse(path))); + } catch (Exception e) { + LOG.warning("Failed to load resourcepacks " + path.getFileName(), e); + } + }); + } catch (IOException e) { + LOG.warning("Failed to list resourcepacks directory", e); + } + + itemsProperty().sort(Comparator.comparing(item -> item.getFile().getName())); + } + + private void onDownload() { + runInFX(() -> { + Controllers.getDownloadPage().showResourcepackDownloads(); + Controllers.navigate(Controllers.getDownloadPage()); + }); + } + + private static class ResourcepackListPageSkin extends ToolbarListPageSkin { + protected ResourcepackListPageSkin(ResourcepackListPage control) { + super(control); + } + + @Override + protected List initializeToolbar(ResourcepackListPage skinnable) { + return Arrays.asList(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, skinnable::onAddFiles), createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, skinnable::onDownload)); + } + } + + public class ResourcepackItem extends Control { + private final ResourcepackFile file; +// final JFXCheckBox checkBox = new JFXCheckBox(); + + public ResourcepackItem(ResourcepackFile file) { + this.file = file; + } + + @Override + protected Skin createDefaultSkin() { + return new ResourcepackItemSkin(this); + } + + public void onDelete() { + try { + if (file.getFile().isDirectory()) { + FileUtils.deleteDirectory(file.getFile()); + } else { + Files.delete(file.getFile().toPath()); + } + ResourcepackListPage.this.refresh(); + } catch (IOException e) { + Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete resourcepack", e); + } + } + + public void onReveal() { + FXUtils.showFileInExplorer(file.getFile().toPath()); + } + + public ResourcepackFile getFile() { + return file; + } + } + + private class ResourcepackItemSkin extends SkinBase { + public ResourcepackItemSkin(ResourcepackItem item) { + super(item); + BorderPane root = new BorderPane(); + root.getStyleClass().add("md-list-cell"); + root.setPadding(new Insets(8)); + + HBox left = new HBox(8); + left.setAlignment(Pos.CENTER); + left.getChildren().addAll(createIcon(item.getFile().getIcon())); +// left.getChildren().addAll(item.checkBox, createIcon(item.getFile().getIcon())); + left.setPadding(new Insets(0, 8, 0, 0)); + FXUtils.setLimitWidth(left, 64); + root.setLeft(left); + + TwoLineListItem center = new TwoLineListItem(); + center.setPadding(new Insets(0, 0, 0, 8)); + center.setTitle(item.getFile().getName()); + center.setSubtitle(item.getFile().getDescription()); + root.setCenter(center); + + HBox right = new HBox(8); + right.setAlignment(Pos.CENTER_RIGHT); + JFXButton btnReveal = new JFXButton(); + FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); + btnReveal.getStyleClass().add("toggle-icon4"); + btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), -1)); + btnReveal.setOnAction(event -> item.onReveal()); + + JFXButton btnDelete = new JFXButton(); + btnDelete.getStyleClass().add("toggle-icon4"); + btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon(Theme.blackFill(), -1)); + btnDelete.setOnAction(event -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), item::onDelete, null)); + right.getChildren().setAll(btnReveal, btnDelete); + root.setRight(right); + + this.getChildren().add(new RipplerContainer(root)); + } + } +} \ No newline at end of file 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 cc1dbe114..48eecef1d 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 @@ -60,6 +60,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<>(); @@ -73,8 +74,9 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage modListTab.setNodeSupplier(loadVersionFor(ModListPage::new)); worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new)); schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new)); + resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new)); - tab = new TabHeader(versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab); + tab = new TabHeader(versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab, resourcePackTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); @@ -139,6 +141,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage worldListTab.getNode().loadVersion(profile, version); if (schematicsTab.isInitialized()) schematicsTab.getNode().loadVersion(profile, version); + if (resourcePackTab.isInitialized()) + resourcePackTab.getNode().loadVersion(profile, version); currentVersionUpgradable.set(profile.getRepository().isModpack(version)); } @@ -283,11 +287,20 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage schematicsListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.schematicsTab)); schematicsListItem.setOnAction(e -> control.tab.select(control.schematicsTab)); + AdvancedListItem resourcePackListItem = new AdvancedListItem(); + resourcePackListItem.getStyleClass().add("navigation-drawer-item"); + resourcePackListItem.setTitle(i18n("resourcepack.manage")); + resourcePackListItem.setLeftGraphic(wrap(SVG.TEXTURE)); + resourcePackListItem.setActionButtonVisible(false); + resourcePackListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.resourcePackTab)); + resourcePackListItem.setOnAction(e -> control.tab.select(control.resourcePackTab)); + AdvancedListBox sideBar = new AdvancedListBox() .add(versionSettingsItem) .add(installerListItem) .add(modListItem) .add(worldListItem) + .add(resourcePackListItem) .add(schematicsListItem); VBox.setVgrow(sideBar, Priority.ALWAYS); diff --git a/HMCL/src/main/resources/assets/img/unknown_pack.png b/HMCL/src/main/resources/assets/img/unknown_pack.png new file mode 100644 index 0000000000000000000000000000000000000000..b741909b1472e65130ae0d164d0a4478c30d6c26 GIT binary patch literal 11275 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 BeqsOs literal 0 HcmV?d00001 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 59c813c05..3c3828d71 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.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/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 57608468f..05457f2f2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -542,4 +542,8 @@ public class DefaultGameRepository implements GameRepository { .append("baseDirectory", baseDirectory) .toString(); } + + public Path getResourcepacksDirectory(String id) { + return getRunDirectory(id).toPath().resolve("resourcepacks"); + } } 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..951232e73 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFile.java @@ -0,0 +1,38 @@ +package org.jackhuang.hmcl.resourcepack; + +import com.google.gson.JsonParser; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public interface ResourcepackFile { + String getDescription(); + + String getName(); + + File getFile(); + + Path getIcon(); + + + default String parseDescriptionFromJson(String json) { + try { + return JsonParser.parseString(json).getAsJsonObject().getAsJsonObject("pack").get("description").getAsString(); + } catch (Exception ignored) { + return ""; + } + + } + + static ResourcepackFile parse(Path path) throws IOException { + if (Files.isRegularFile(path) && path.toString().toLowerCase().endsWith(".zip")) { + return new ResourcepackZipFile(path.toFile()); + } else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) { + return new ResourcepackFolder(path.toFile()); + } + return null; + } + +} 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..396b42cf7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java @@ -0,0 +1,39 @@ +package org.jackhuang.hmcl.resourcepack; + +import com.google.gson.JsonParser; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResourcepackFolder implements ResourcepackFile { + private final File folder; + + public ResourcepackFolder(File folder) { + this.folder = folder; + } + + @Override + public String getName() { + return folder.getName(); + } + + @Override + public File getFile() { + return folder; + } + + @Override + public String getDescription() { + try { + return parseDescriptionFromJson(Files.readString(folder.toPath().resolve("pack.mcmeta"))); + } catch (Exception ignored) { + return ""; + } + } + + @Override + public Path getIcon() { + return folder.toPath().resolve("pack.png"); + } +} \ No newline at end of file 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..458ce290c --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java @@ -0,0 +1,44 @@ +package org.jackhuang.hmcl.resourcepack; + +import org.jackhuang.hmcl.util.io.CompressingUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResourcepackZipFile implements ResourcepackFile { + private final FileSystem zipfs; + private final File resourcepackfile; + + public ResourcepackZipFile(File resourcepackfile) throws IOException { + this.resourcepackfile = resourcepackfile; + this.zipfs = CompressingUtils.createReadOnlyZipFileSystem(resourcepackfile.toPath()); + } + + @Override + public String getName() { + return resourcepackfile.getName().replace(".zip", ""); + } + + @Override + public File getFile() { + return resourcepackfile; + } + + @Override + public String getDescription() { + + try { + return parseDescriptionFromJson(Files.readString(zipfs.getPath("pack.mcmeta"))); + } catch (Exception ignored) { + return ""; + } + } + + @Override + public Path getIcon() { + return zipfs.getPath("pack.png"); + } +} From b99520d3982b559350df667212ec2062f6898b5b Mon Sep 17 00:00:00 2001 From: Ciilu <109708109+Ciilu@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:04:27 +0800 Subject: [PATCH 2/5] update --- .../org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 559cfce19..612f0bd0e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java @@ -200,11 +200,12 @@ public class ResourcepackListPage extends ListPageBase Date: Fri, 19 Sep 2025 21:49:02 +0800 Subject: [PATCH 3/5] i18n --- HMCL/src/main/resources/assets/lang/I18N.properties | 5 +++++ HMCL/src/main/resources/assets/lang/I18N_zh.properties | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 2bcb9b5b3..a0d3296cd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1197,6 +1197,11 @@ repositories.chooser=HMCL requires JavaFX to work.\n\ repositories.chooser.title=Choose download source for JavaFX resourcepack=Resource Packs +resourcepack.add=Add Resource Pack +resourcepack.manage=Resource Packs +resourcepack.download=Download Resource Packs +resourcepack.add.failed=Failed to add resource pack +resourcepack.delete.failed=Failed to delete resource pack resourcepack.download.title=Download Resource Pack - %1s reveal.in_file_manager=Reveal in File Manager diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index ebb749f8e..e12b4340b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -990,6 +990,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=在檔案管理員中查看 From 69510ac8f3491d557dcffc6869d60919ffda4aa3 Mon Sep 17 00:00:00 2001 From: Ciilu <109708109+Ciilu@users.noreply.github.com> Date: Fri, 19 Sep 2025 21:55:09 +0800 Subject: [PATCH 4/5] checkstyle --- .../org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java | 2 +- .../org/jackhuang/hmcl/resourcepack/ResourcepackFile.java | 1 - .../org/jackhuang/hmcl/resourcepack/ResourcepackFolder.java | 4 +--- .../org/jackhuang/hmcl/resourcepack/ResourcepackZipFile.java | 1 + 4 files changed, 3 insertions(+), 5 deletions(-) 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 index 612f0bd0e..3bff0edb4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcepackListPage.java @@ -228,4 +228,4 @@ public class ResourcepackListPage extends ListPageBase Date: Fri, 19 Sep 2025 22:28:05 +0800 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e12b4340b..439201b73 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -990,11 +990,11 @@ repositories.chooser=缺少 JavaFX 執行環境。HMCL 需要 JavaFX 才能正 repositories.chooser.title=選取 JavaFX 下載源 resourcepack=資源包 -resourcepack.add=添加資源包 +resourcepack.add=新增資源包 resourcepack.manage=資源包管理 resourcepack.download=下載資源包 -resourcepack.add.failed=添加資源包失敗 -resourcepack.delete.failed=删除資源包失敗 +resourcepack.add.failed=新增資源包失敗 +resourcepack.delete.failed=刪除資源包失敗 resourcepack.download.title=資源包下載 - %1s reveal.in_file_manager=在檔案管理員中查看