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 58a3cc5fc3a..cc855a52604 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 @@ -68,12 +68,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage { - public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_MOD = - (downloadProvider, repository, version, mod, file) -> download(downloadProvider, repository, version, file, "mods"); - public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_RESOURCE_PACK = - (downloadProvider, repository, version, pack, file) -> download(downloadProvider, repository, version, file, "resourcepacks"); - public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_SHADER = - (downloadProvider, repository, version, shader, file) -> download(downloadProvider, repository, version, file, "shaderpacks"); private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1)); private final TabHeader tab; @@ -92,12 +86,12 @@ public DownloadPage() { this(null); } - public DownloadPage(String uploadVersion) { + public DownloadPage(String updateVersion) { newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(), "game", versionPageNavigator::onGameSelected))); modpackTab.setNodeSupplier(loadVersionFor(() -> { DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, repository, __, modpack, file) -> { - Versions.downloadModpackImpl(downloadProvider, repository, uploadVersion, modpack, file); + Versions.downloadModpackImpl(downloadProvider, repository, updateVersion, modpack, file); }, false); JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack")); @@ -106,9 +100,9 @@ public DownloadPage(String uploadVersion) { page.getActions().add(installLocalModpackButton); return page; })); - modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(FOR_MOD, true))); - resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(FOR_RESOURCE_PACK, true))); - shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(FOR_SHADER, true))); + modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(true))); + resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(true))); + shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(true))); worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteAddonRepository.WORLDS))); tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab); @@ -123,7 +117,7 @@ public DownloadPage(String uploadVersion) { .startCategory(i18n("download.content").toUpperCase(Locale.ROOT)) .addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(tab, resourcePackTab, i18n("resourcepack"), SVG.TEXTURE) - .addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL) + .addNavigationDrawerTab(tab, shaderTab, i18n("shaderpack"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL) .addNavigationDrawerTab(tab, worldTab, i18n("world"), SVG.PUBLIC); FXUtils.setLimitWidth(sideBar, 200); setLeft(sideBar); @@ -220,14 +214,19 @@ public void showModpackDownloads() { tab.select(modpackTab, false); } + public DownloadListPage showModDownloads() { + tab.select(modTab, false); + return modTab.getNode(); + } + public DownloadListPage showResourcePackDownloads() { tab.select(resourcePackTab, false); return resourcePackTab.getNode(); } - public DownloadListPage showModDownloads() { - tab.select(modTab, false); - return modTab.getNode(); + public DownloadListPage showShaderDownloads() { + tab.select(shaderTab, false); + return shaderTab.getNode(); } public void showWorldDownloads() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java index 47597d34a63..df72a44a957 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java @@ -30,11 +30,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class AddonCheckUpdatesTask extends Task> { +public class AddonCheckUpdatesTask extends Task> { private final DownloadProvider downloadProvider; private final List> dependents; - public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection addons) { + public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection addons) { this.downloadProvider = downloadProvider; dependents = addons.stream().map(addon -> Task.supplyAsync(Schedulers.io(), () -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java index b9c251715f6..320a26e7f69 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java @@ -60,15 +60,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class AddonUpdatesPage extends BorderPane implements DecoratorPage { +public class AddonUpdatesPage extends BorderPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("addon.check_update"))); - private final LocalAddonManager localAddonManager; + private final Path addonsDirectory; private final ObservableList objects; @SuppressWarnings("unchecked") - public AddonUpdatesPage(LocalAddonManager localAddonManager, List updates) { - this.localAddonManager = localAddonManager; + public AddonUpdatesPage(Path addonsDirectory, List updates) { + this.addonsDirectory = addonsDirectory; getStyleClass().add("gray-background"); @@ -132,7 +132,7 @@ private void setupCellValueFactory(TableColumn column, private void updateFiles() { AddonUpdateTask task = new AddonUpdateTask( - localAddonManager.getDirectory(), + addonsDirectory, objects.stream() .filter(AddonUpdateObject::isEnabled) .map(AddonUpdateObject::getData) @@ -294,19 +294,18 @@ public static class AddonUpdateTask extends Task { RemoteAddon.Version remote = addon.targetVersion(); boolean isDisabled = local.isDisabled(); String originalFileName = local.getFile().getFileName().toString(); + String fileName = addon.useRemoteFileName() ? remote.file().filename() : originalFileName; + if (isDisabled) + fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION); + String newFileName = fileName; dependents.add(Task .runAsync(Schedulers.javafx(), () -> local.setOld(true)) .thenComposeAsync(() -> { - String fileName = addon.useRemoteFileName() ? remote.file().filename() : originalFileName; - if (isDisabled) - fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION); - var task = new FileDownloadTask( remote.file().url(), - addonDirectory.resolve(fileName) + addonDirectory.resolve(newFileName) ); - task.setName(remote.name()); return task; }) @@ -317,11 +316,14 @@ public static class AddonUpdateTask extends Task { if (isDisabled) local.markDisabled(); failedAddons.add(local); - } else if (!local.keepOldFiles()) { - try { - local.delete(); - } catch (IOException e) { - LOG.warning("Failed to delete outdated addon: " + local.getFile(), e); + } else { + local.onUpdated(newFileName); + if (!local.keepOldFiles()) { + try { + local.delete(); + } catch (IOException e) { + LOG.warning("Failed to delete outdated addon: " + local.getFile(), e); + } } } }) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java index 76d130b17a0..6ae93e36a82 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java @@ -22,7 +22,7 @@ import javafx.scene.control.Skin; import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.addon.datapack.DataPack; +import org.jackhuang.hmcl.addon.pack.datapack.DataPack; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java index 40fc80daf59..0ca88000fb5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java @@ -43,7 +43,7 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.util.Duration; -import org.jackhuang.hmcl.addon.datapack.DataPack; +import org.jackhuang.hmcl.addon.pack.datapack.DataPack; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index bd1f09f6973..2129f9cd75a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -78,7 +78,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); private final ObservableList versions = FXCollections.observableArrayList(); private final StringProperty selectedVersion = new SimpleStringProperty(); - private final DownloadPage.DownloadCallback callback; + private final DownloadPage.DownloadCallback callback; // ONLY FOR MODPACKS private boolean searchInitialized = false; protected final BooleanProperty supportChinese = new SimpleBooleanProperty(); private final ObservableList actions = FXCollections.observableArrayList(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index fb06c3ca0fe..a419bd63ae7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -59,6 +59,25 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { + public static final DownloadCallback FOR_MOD = + (downloadProvider, repository, version, mod, file) -> + org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, repository, version, file, "mods"); + public static final DownloadCallback FOR_RESOURCE_PACK = + (downloadProvider, repository, version, pack, file) -> + org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, repository, version, file, "resourcepacks"); + public static final DownloadCallback FOR_SHADER = + (downloadProvider, repository, version, shader, file) -> + org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, repository, version, file, "shaderpacks"); + + public static @Nullable DownloadCallback getDownloadCallbackFor(RemoteAddonRepository.Type repoType) { + return switch (repoType) { + case MOD -> FOR_MOD; + case RESOURCE_PACK -> FOR_RESOURCE_PACK; + case SHADER_PACK -> FOR_SHADER; + default -> null; + }; + } + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loaded = new SimpleBooleanProperty(false); private final BooleanProperty loading = new SimpleBooleanProperty(false); @@ -68,7 +87,7 @@ public class DownloadPage extends Control implements DecoratorPage { private final RemoteAddon addon; private final ModTranslations.Mod mod; private final HMCLGameRepository.InstanceReference instanceReference; - private final DownloadCallback callback; + private final DownloadCallback callback; // ONLY FOR MODPACKS private final DownloadListPage page; private final RemoteAddonRepository.Type type; @@ -156,11 +175,17 @@ public void setFailed(boolean failed) { this.failed.set(failed); } + public @Nullable DownloadCallback getCallbackFor(RemoteAddon addon) { + if (addon.repoType() == RemoteAddonRepository.Type.MODPACK) return this.callback; + return getDownloadCallbackFor(addon.repoType()); + } + public void download(RemoteAddon addon, RemoteAddon.Version file) { - if (this.callback == null) { + var callback = getCallbackFor(addon); + if (callback == null) { saveAs(file); } else { - this.callback.download(page.getDownloadProvider(), instanceReference.repository(), instanceReference.instanceId(), addon, file); + callback.download(page.getDownloadProvider(), instanceReference.repository(), instanceReference.instanceId(), addon, file); } } @@ -386,21 +411,14 @@ private static final class DependencyAddonItem extends LineButton { pane.getChildren().setAll(imageView, content); FXUtils.setLimitHeight(this, 60); - RemoteAddonRepository.Type type = addon.repoType(); - DownloadCallback callback = switch (type) { - case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD; - case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK; - case SHADER_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; - default -> null; // Dependencies should not be modpacks, worlds or customized stuff - }; setOnAction((e) -> { fireEvent(new DialogCloseEvent()); - Controllers.navigate(new DownloadPage(page, addon, instanceReference, callback)); + Controllers.navigate(new DownloadPage(page, addon, instanceReference, null)); }); setNode(IDX_LEADING, pane); if (addon != RemoteAddon.BROKEN) { - ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(type).getModByCurseForgeId(addon.slug()); + ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(addon.repoType()).getModByCurseForgeId(addon.slug()); content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title()); content.setSubtitle(addon.description()); for (String category : addon.categories()) { @@ -491,7 +509,7 @@ private static final class AddonItem extends StackPane { } private static final class AddonVersion extends JFXDialogLayout { - public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage selfPage) { + public AddonVersion(RemoteAddon addon, RemoteAddon.Version version, DownloadPage selfPage) { RemoteAddonRepository.Type type = selfPage.type; String title = switch (type) { @@ -505,7 +523,7 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s VBox box = new VBox(8); box.setPadding(new Insets(8)); - AddonItem addonItem = new AddonItem(mod, version, selfPage); + AddonItem addonItem = new AddonItem(addon, version, selfPage); addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again box.getChildren().setAll(addonItem); SpinnerPane spinnerPane = new SpinnerPane(); @@ -527,14 +545,14 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s this.setBody(box); JFXButton downloadButton = null; - if (selfPage.callback != null) { - downloadButton = new JFXButton(type == RemoteAddonRepository.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install")); + if (selfPage.getCallbackFor(addon) != null) { + downloadButton = new JFXButton(type == RemoteAddonRepository.Type.MODPACK ? i18n("install.modpack") : i18n("addon.install")); downloadButton.getStyleClass().add("dialog-accept"); downloadButton.setOnAction(e -> { if (type == RemoteAddonRepository.Type.MODPACK || !spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { fireEvent(new DialogCloseEvent()); } - selfPage.download(mod, version); + selfPage.download(addon, version); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java index 2c1aa9b61f8..e6ccf917345 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java @@ -30,40 +30,48 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class HMCLLocalizedDownloadListPage extends DownloadListPage { - public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS); + public static DownloadListPage ofMod(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS); } - public static DownloadListPage ofCurseForgeMod(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, null); + public static DownloadListPage ofCurseForgeMod(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, null); } - public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, null, ModrinthRemoteAddonRepository.MODS); + public static DownloadListPage ofModrinthMod(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.MOD, null, ModrinthRemoteAddonRepository.MODS); } public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean versionSelection) { return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS); } - public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS); + public static DownloadListPage ofResourcePack(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS); } - public static DownloadListPage ofCurseForgeResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, null); + public static DownloadListPage ofCurseForgeResourcePack(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, null); } - public static DownloadListPage ofModrinthResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, null, ModrinthRemoteAddonRepository.RESOURCE_PACKS); + public static DownloadListPage ofModrinthResourcePack(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, null, ModrinthRemoteAddonRepository.RESOURCE_PACKS); } - public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS); + public static DownloadListPage ofShaderPack(boolean versionSelection) { + var page = new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS); page.supportChinese.set(false); return page; } + public static DownloadListPage ofCurseForgeShaderPack(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, null); + } + + public static DownloadListPage ofModrinthShaderPack(boolean versionSelection) { + return new HMCLLocalizedDownloadListPage(null, versionSelection, RemoteAddonRepository.Type.SHADER_PACK, null, ModrinthRemoteAddonRepository.SHADER_PACKS); + } + private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteAddonRepository.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) { super(null, callback, versionSelection); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 5207aab98e7..f8f79fabe1c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -239,7 +239,7 @@ public void checkUpdates(Collection mods) { Runnable action = () -> Controllers.taskDialog(Task .composeAsync(() -> { Optional gameVersion = repository.getGameVersion(instanceId); - return gameVersion.map(g -> new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), g, mods)).orElse(null); + return gameVersion.map(g -> new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), g, mods)).orElse(null); }) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception instanceof CancellationException) return; @@ -248,7 +248,7 @@ public void checkUpdates(Collection mods) { } else if (result.isEmpty()) { Controllers.dialog(i18n("addon.check_update.empty")); } else { - Controllers.navigateForward(new AddonUpdatesPage<>(modManager, result)); + Controllers.navigateForward(new AddonUpdatesPage(modManager.getDirectory(), result)); } }) .withStagesHints("update.checking"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java index 10ac770a365..7a54d4f309a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java @@ -460,10 +460,10 @@ final class ModInfoDialog extends JFXDialogLayout { button.setOnAction(e -> { fireEvent(new DialogCloseEvent()); Controllers.navigate(new DownloadPage( - repository instanceof CurseForgeRemoteAddonRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false), + repository instanceof CurseForgeRemoteAddonRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(false) : HMCLLocalizedDownloadListPage.ofModrinthMod(false), remoteAddon, new HMCLGameRepository.InstanceReference(ModListPageSkin.this.getSkinnable().getRepository(), ModListPageSkin.this.getSkinnable().getInstanceId()), - org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD + null )); }); button.setDisable(false); 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 504cc0bfce5..0c362cfa8bb 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 @@ -40,8 +40,8 @@ import org.jackhuang.hmcl.addon.*; import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager; +import org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackFile; +import org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.setting.DownloadProviders; @@ -54,6 +54,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.Lazy; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; @@ -76,6 +77,9 @@ public final class ResourcePackListPage extends ListPageBase implements VersionPage.GameInstanceLoadable { + public static final Lazy UNKNOWN_PACK_IMAGE = new Lazy<>(() -> + FXUtils.newBuiltinImage("/assets/img/unknown_pack.png", 64, 64, false, false)); + private static final String TIP_KEY = "resourcePackWarning"; private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) { return switch (compatibility) { @@ -155,7 +159,7 @@ public void addFiles(List files) { try { resourcePackManager.importResourcePack(file); } catch (Exception e) { - LOG.warning("Failed to add resource pack", e); + LOG.warning("Failed to add resource pack " + file, e); failures.add(file); } } @@ -172,10 +176,10 @@ public void addFiles(List files) { } - public void onAddFiles() { + private void onAddFiles() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("resourcepack.add")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.resourcepack"), "*.zip")); List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); if (files != null && !files.isEmpty()) { addFiles(files); @@ -233,7 +237,7 @@ public void checkUpdates(Collection resourcePacks) { Runnable action = () -> Controllers.taskDialog(Task .composeAsync(() -> { Optional gameVersion = repository.getGameVersion(instanceId); - return gameVersion.map(g -> new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), g, resourcePacks)).orElse(null); + return gameVersion.map(g -> new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), g, resourcePacks)).orElse(null); }) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null || result == null) { @@ -241,7 +245,7 @@ public void checkUpdates(Collection resourcePacks) { } else if (result.isEmpty()) { Controllers.dialog(i18n("addon.check_update.empty")); } else { - Controllers.navigateForward(new AddonUpdatesPage<>(resourcePackManager, result)); + Controllers.navigateForward(new AddonUpdatesPage(resourcePackDirectory, result)); } }) .withStagesHints("update.checking"), @@ -459,7 +463,7 @@ Image getIcon() { Image image = file.getIcon(); 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"); + image = UNKNOWN_PACK_IMAGE.get(); } return image; } @@ -608,7 +612,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { RemoteAddonRepository repository = item.getValue(); JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile()); + Optional versionOptional = repository.getRemoteVersionByLocalFile(pack.getFile()); if (versionOptional.isPresent()) { RemoteAddon remoteAddon = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().modid()); FXUtils.runInFX(() -> { @@ -616,11 +620,11 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { fireEvent(new DialogCloseEvent()); Controllers.navigate(new DownloadPage( repository instanceof CurseForgeRemoteAddonRepository - ? HMCLLocalizedDownloadListPage.ofCurseForgeResourcePack(null, false) - : HMCLLocalizedDownloadListPage.ofModrinthResourcePack(null, false), + ? HMCLLocalizedDownloadListPage.ofCurseForgeResourcePack(false) + : HMCLLocalizedDownloadListPage.ofModrinthResourcePack(false), remoteAddon, new HMCLGameRepository.InstanceReference(page.repository, page.instanceId), - org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK + null )); }); button.setDisable(false); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ShaderListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ShaderListPage.java new file mode 100644 index 00000000000..6dfa00e41ed --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ShaderListPage.java @@ -0,0 +1,534 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.versions; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; +import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.JFXTextField; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.util.Duration; +import org.jackhuang.hmcl.addon.RemoteAddon; +import org.jackhuang.hmcl.addon.RemoteAddonRepository; +import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; +import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; +import org.jackhuang.hmcl.addon.shader.ShaderFile; +import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.setting.DownloadProviders; +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.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.i18n.I18n; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; + +import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; +import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class ShaderListPage extends ListPageBase implements VersionPage.GameInstanceLoadable { + + private final ReentrantLock lock = new ReentrantLock(); + + private Path shadersDir; + private HMCLGameRepository repository; + private String instanceId; + + public ShaderListPage() { + FXUtils.applyDragListener(this, ShaderFile::isFileShaderPack, this::addFiles); + } + + @Override + protected Skin createDefaultSkin() { + return new ShaderListPageSkin(this); + } + + @Override + public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) { + this.repository = repository; + this.instanceId = instanceId; + this.shadersDir = repository.getShadersDirectory(instanceId); + refresh(); + } + + public void refresh() { + if (shadersDir == null) return; + setLoading(true); + Task.supplyAsync(Schedulers.io(), () -> { + lock.lock(); + try (var stream = Files.list(shadersDir)) { + return stream.map(file -> { + try { + return ShaderFile.fromFile(file); + } catch (IOException e) { + LOG.warning("Failed to load shader pack " + file, e); + return null; + } + }).filter(Objects::nonNull).toList(); + } finally { + lock.unlock(); + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + getItems().setAll(result); + } else { + LOG.warning("Failed to load shader packs", exception); + getItems().clear(); + } + setLoading(false); + }).start(); + } + + public void addFiles(List files) { + if (shadersDir == null) return; + List failures = new ArrayList<>(); + Task.runAsync(() -> { + lock.lock(); + try { + for (Path file : files) { + if (ShaderFile.isFileShaderPack(file)) { + try { + FileUtils.copyTo(file, shadersDir); + } catch (Exception e) { + LOG.warning("Failed to add shader pack " + file, e); + failures.add(file); + } + } else { + LOG.warning("Failed to add shader pack", new IllegalArgumentException("File '" + file + "' is not a shader pack")); + failures.add(file); + } + } + } finally { + lock.unlock(); + } + }).withRunAsync(Schedulers.javafx(), () -> { + if (!failures.isEmpty()) { + StringBuilder failure = new StringBuilder(i18n("shaderpack.add.failed")); + for (Path file : failures) { + failure.append("\n").append(file.toString()); + } + Controllers.dialog(failure.toString(), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + refresh(); + }).start(); + } + + public void removeFiles(List selectedItems) { + try { + for (var shader : selectedItems) { + shader.delete(); + } + } catch (IOException e) { + Controllers.dialog(i18n("shaderpack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete shader packs", e); + } + refresh(); + } + + public void checkUpdates(Collection shaderPacks) { + if (shadersDir == null) return; + + Controllers.taskDialog( + Task.composeAsync(() -> { + Optional gameVersion = repository.getGameVersion(instanceId); + return gameVersion.map(g -> new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), g, shaderPacks)).orElse(null); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null || result == null) { + Controllers.dialog(I18n.i18n("addon.check_update.failed_check"), I18n.i18n("message.failed"), MessageDialogPane.MessageType.ERROR); + } else if (result.isEmpty()) { + Controllers.dialog(I18n.i18n("addon.check_update.empty")); + } else { + Controllers.navigateForward(new AddonUpdatesPage(shadersDir, result)); + } + }).withStagesHints("update.checking"), + I18n.i18n("addon.check_update"), TaskCancellationAction.NORMAL); + } + + private void onAddFiles() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("shaderpack.add")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.shaderpack"), "*.zip")); + List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); + if (files != null && !files.isEmpty()) { + addFiles(files); + } + } + + private void onDownload() { + Controllers.getDownloadPage().showShaderDownloads().selectVersion(instanceId); + Controllers.navigate(Controllers.getDownloadPage()); + } + + private void onOpenFolder() { + if (shadersDir != null) { + FXUtils.openFolder(shadersDir); + } + } + + private static Image getOrCreateIcon(ShaderFile shaderFile) { + Image image = shaderFile.getIcon(); + if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 || + (Math.abs(image.getWidth() - image.getHeight()) >= 1)) { + image = switch (shaderFile.getLoaderType()) { + case OPTIFINE_IRIS -> FXUtils.newBuiltinImage("/assets/img/opti-iris.png"); + default -> ResourcePackListPage.UNKNOWN_PACK_IMAGE.get(); + }; + } + return image; + } + + private static final class ShaderListPageSkin extends SkinBase { + private final JFXListView listView; + private final JFXTextField searchField = new JFXTextField(); + + private final TransitionPane toolbarPane = new TransitionPane(); + private final HBox searchBar = new HBox(); + private final HBox toolbarNormal = new HBox(); + private final HBox toolbarSelecting = new HBox(); + + private boolean isSearching; + + public ShaderListPageSkin(ShaderListPage 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<>(); + + { + + // Toolbar Selecting + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> + control.removeFiles(listView.getSelectionModel().getSelectedItems()), + null); + }), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + control.checkUpdates(listView.getSelectionModel().getSelectedItems().stream().toList()) + ), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + + // Search Bar + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(new Insets(0, 5, 0, 5)); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> search()); + FXUtils.onChange(searchField.textProperty(), newValue -> { + pause.setRate(1); + pause.playFromStart(); + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); + + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + // Toolbar Normal + toolbarNormal.setAlignment(Pos.CENTER_LEFT); + toolbarNormal.setPickOnBounds(false); + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), + createToolbarButton2(i18n("shaderpack.add"), SVG.ADD, control::onAddFiles), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + control.checkUpdates(listView.getItems().stream().toList()) + ), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), + selectedItem -> { + if (selectedItem == null) + changeToolbar(isSearching ? searchBar : toolbarNormal); + else + changeToolbar(toolbarSelecting); + }); + FXUtils.setOverflowHidden(toolbarPane, 8); + root.getContent().add(toolbarPane); + + // Clear selection when pressing ESC + root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.loadingProperty().bind(control.loadingProperty()); + + listView.setCellFactory(x -> new ShaderListCell(listView, control)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), control.getItems()); + + listView.setOnContextMenuRequested(event -> { + ShaderFile selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ShaderInfoDialog(control, selectedItem)); + } + }); + + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + listView.getStyleClass().add("no-horizontal-scrollbar"); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().setAll(root); + getChildren().setAll(pane); + } + + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } + } + } + + private void search() { + isSearching = true; + + Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); + + String queryString = searchField.getText(); + if (StringUtils.isBlank(queryString)) { + listView.getItems().setAll(getSkinnable().getItems()); + } else { + listView.getItems().clear(); + + Predicate<@Nullable String> predicate; + try { + predicate = StringUtils.compileQuery(queryString); + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return; + } + + // Do we need to search in the background thread? + for (ShaderFile item : getSkinnable().getItems()) { + var meta = item.getMeta(); + if (predicate.test(item.getFile().getFileName().toString()) + || predicate.test(item.getName()) + || (meta != null && (predicate.test(meta.version()) || predicate.test(meta.description())))) { + listView.getItems().add(item); + } + } + } + } + + } + + private static final class ShaderListCell extends MDListCell { + + private final ShaderListPage page; + + private final ImageContainer imageContainer = new ImageContainer(24); + private final TwoLineListItem content = new TwoLineListItem(); + private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER); + private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO); + + public ShaderListCell(JFXListView listView, ShaderListPage page) { + super(listView); + this.page = page; + + HBox root = new HBox(8); + root.setPickOnBounds(false); + root.setAlignment(Pos.CENTER_LEFT); + + HBox.setHgrow(content, Priority.ALWAYS); + content.setMouseTransparent(true); + + root.getChildren().setAll(imageContainer, content, btnReveal, btnInfo); + + setSelectable(); + + StackPane.setMargin(root, new Insets(8)); + getContainer().getChildren().add(root); + } + + @Override + protected void updateControl(ShaderFile item, boolean empty) { + if (empty || item == null) return; + + imageContainer.setImage(getOrCreateIcon(item)); + + content.getTags().clear(); + content.setTitle(item.getFileName()); + content.setSubtitle(item.getMeta() == null || item.getMeta().name() == null ? "" : item.getMeta().name()); + content.addTag(switch (item.getLoaderType()) { + case OPTIFINE_IRIS -> i18n("shaderpack.loader.optifine_iris"); + case APERTURE -> i18n("shaderpack.loader.aperture"); + }); + if (item.getMeta() != null && item.getMeta().version() != null) + content.addTag(item.getMeta().version()); + + FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); + btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(item.getFile())); + + btnInfo.setOnAction(e -> Controllers.dialog(new ShaderInfoDialog(this.page, item))); + } + } + + private static final class ShaderInfoDialog extends JFXDialogLayout { + + public ShaderInfoDialog(ShaderListPage page, ShaderFile shaderFile) { + + HBox titleContainer = new HBox(); + titleContainer.setSpacing(8); + + Stage stage = Controllers.getStage(); + maxWidthProperty().bind(stage.widthProperty().multiply(0.7)); + + ImageContainer imageContainer = new ImageContainer(40); + imageContainer.setImage(getOrCreateIcon(shaderFile)); + + TwoLineListItem title = new TwoLineListItem(); + title.setTitle(shaderFile.getFileName()); + title.setSubtitle(shaderFile.getFile().getFileName().toString()); + title.addTag(switch (shaderFile.getLoaderType()) { + case OPTIFINE_IRIS -> i18n("shaderpack.loader.optifine_iris"); + case APERTURE -> i18n("shaderpack.loader.aperture"); + }); + + titleContainer.getChildren().setAll(imageContainer, title); + setHeading(titleContainer); + + Label description = new Label(); + if (shaderFile.getMeta() != null && shaderFile.getMeta().description() != null) + description.setText(shaderFile.getMeta().description()); + description.setWrapText(true); + FXUtils.copyOnDoubleClick(description); + + ScrollPane descriptionPane = new ScrollPane(description); + FXUtils.smoothScrolling(descriptionPane); + descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + descriptionPane.setFitToWidth(true); + FXUtils.onChange(description.heightProperty(), newVal -> { + double maxHeight = stage.getHeight() * 0.5; + double targetHeight = Math.min(newVal.doubleValue(), maxHeight); + descriptionPane.setPrefViewportHeight(targetHeight); + }); + + setBody(descriptionPane); + + for (Pair item : Arrays.asList( + pair("addon.curseforge", CurseForgeRemoteAddonRepository.SHADERS), + pair("addon.modrinth", ModrinthRemoteAddonRepository.SHADER_PACKS) + )) { + RemoteAddonRepository repository = item.getValue(); + JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); + Task.runAsync(() -> { + Optional versionOptional = repository.getRemoteVersionByLocalFile(shaderFile.getFile()); + if (versionOptional.isPresent()) { + RemoteAddon remoteAddon = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().modid()); + FXUtils.runInFX(() -> { + button.setOnAction(e -> { + fireEvent(new DialogCloseEvent()); + Controllers.navigate(new DownloadPage( + repository instanceof CurseForgeRemoteAddonRepository + ? HMCLLocalizedDownloadListPage.ofCurseForgeShaderPack(false) + : HMCLLocalizedDownloadListPage.ofModrinthShaderPack(false), + remoteAddon, + new HMCLGameRepository.InstanceReference(page.repository, page.instanceId), + null + )); + }); + button.setDisable(false); + }); + } + }).start(); + button.setDisable(true); + getActions().add(button); + } + + JFXButton okButton = new JFXButton(); + okButton.getStyleClass().add("dialog-accept"); + okButton.setText(i18n("button.ok")); + okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + getActions().add(okButton); + + onEscPressed(this, okButton::fire); + } + + } +} 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 860c7bf6c78..d49fe45bcfa 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 @@ -62,6 +62,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage 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 TabHeader.Tab shaderListTab = new TabHeader.Tab<>("shaderListTab"); private final TransitionPane transitionPane = new TransitionPane(); private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); private final ObjectProperty instanceReference = new SimpleObjectProperty<>(); @@ -82,10 +83,11 @@ public VersionPage() { installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new)); modListTab.setNodeSupplier(loadVersionFor(ModListPage::new)); resourcePackTab.setNodeSupplier(loadVersionFor(ResourcePackListPage::new)); + shaderListTab.setNodeSupplier(loadVersionFor(ShaderListPage::new)); worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new)); schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new)); - tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab); + tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, shaderListTab, worldListTab, schematicsTab); tab.select(versionSettingsTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); @@ -98,6 +100,8 @@ public VersionPage() { modListTab.getNode().loadInstance(getRepository(), getVersion()); if (resourcePackTab.isInitialized()) resourcePackTab.getNode().loadInstance(getRepository(), getVersion()); + if (shaderListTab.isInitialized()) + shaderListTab.getNode().loadInstance(getRepository(), getVersion()); if (worldListTab.isInitialized()) worldListTab.getNode().loadInstance(getRepository(), getVersion()); if (schematicsTab.isInitialized()) @@ -162,6 +166,8 @@ public void loadVersion(String version, HMCLGameRepository repository) { modListTab.getNode().loadInstance(repository, version); if (resourcePackTab.isInitialized()) resourcePackTab.getNode().loadInstance(repository, version); + if (shaderListTab.isInitialized()) + shaderListTab.getNode().loadInstance(repository, version); if (worldListTab.isInitialized()) worldListTab.getNode().loadInstance(repository, version); if (schematicsTab.isInitialized()) @@ -290,6 +296,7 @@ protected Skin(VersionPage control) { .addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL) .addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) + .addNavigationDrawerTab(control.tab, control.shaderListTab, i18n("shaderpack.manage"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL); VBox.setVgrow(sideBar, Priority.ALWAYS); diff --git a/HMCL/src/main/resources/assets/img/opti-iris.png b/HMCL/src/main/resources/assets/img/opti-iris.png new file mode 100644 index 00000000000..9df5d2349b3 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/opti-iris.png differ diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 199d231bf9c..0182147b547 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -198,6 +198,7 @@ addon.dependency.incompatible=Incompatible Addons (Installing these addons at th addon.dependency.broken=Broken Dependencies (This addon existed before, but it does not exist anymore. Try using another download source.) addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (Snapshots) +addon.install=Install addon.modrinth=Modrinth archive.author=Author(s) @@ -392,7 +393,6 @@ datetime.format=MMM d, yyyy, h\:mm\:ss a download=Download download.code.404=File "%s" not found on the remote server. download.content=Addons -download.shader=Shaders download.curseforge.unavailable=This HMCL build does not support access to CurseForge. Please use the official build to access CurseForge. download.existing=The file cannot be saved because it already exists. You can click "Save As" to save the file elsewhere. download.external_link=Visit Download Website @@ -443,6 +443,7 @@ extension.mod=Mod File extension.modloader.installer=Mod Loader Installer extension.resourcepack=Resource Pack Archive extension.schematic=Schematic File +extension.shaderpack=Shader Pack Archive extension.world=World Archive fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it. @@ -1163,7 +1164,6 @@ mods.restore=Restore mods.url=Official Page mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update? mods.warning.loader_mismatch=Mod loader mismatch -mods.install=Install mods.save_as=Save As mods.unknown=Unknown Mod @@ -1638,7 +1638,14 @@ settings.type.global.preset.remove=Delete Preset settings.type.global.preset.remove.confirm=Are you sure you want to delete preset "%s"? Instances using this preset will fall back to the default preset. settings.type.global.preset.auto_name=Preset %d +shaderpack=Shaders +shaderpack.add=Add +shaderpack.add.failed=Failed to add shader packs: +shaderpack.delete.failed=Failed to delete shader pack shaderpack.download.title=Download Shader - %1s +shaderpack.loader.aperture=Aperture +shaderpack.loader.optifine_iris=Optifine/Iris +shaderpack.manage=Shaders sponsor=Donors sponsor.bmclapi=Downloads for the Chinese Mainland are provided by BMCLAPI. Click here for more information. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 58144270e0b..7f082dfdce5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -183,6 +183,7 @@ addon.check_update.file=الملف addon.check_update.source=المصدر addon.check_update.target_version=الإصدار المستهدف addon.curseforge=CurseForge +addon.install=تثبيت addon.modrinth=Modrinth archive.author=المؤلف/المؤلفون @@ -366,7 +367,6 @@ datetime.format=MMM d, yyyy, h\:mm\:ss a download=تنزيل download.code.404=الملف "%s" غير موجود على الخادم البعيد. download.content=الإضافات -download.shader=الظلال download.curseforge.unavailable=هذا الإصدار من HMCL لا يدعم الوصول إلى CurseForge. يُرجى استخدام الإصدار الرسمي للوصول إلى CurseForge. download.existing=لا يمكن حفظ الملف لأنه موجود مسبقاً. يمكنك النقر على "حفظ باسم" لحفظ الملف في مكان آخر. download.external_link=زيارة موقع التنزيل @@ -1108,7 +1108,6 @@ mods.restore=استعادة mods.url=الصفحة الرسمية mods.update_modpack_mod.warning=تحديث المودات في حزمة مودات قد يؤدي إلى نتائج لا يمكن إصلاحها، وقد يتسبب في تلف الحزمة بحيث لا تعمل. هل أنت متأكد من التحديث؟ mods.warning.loader_mismatch=عدم تطابق محمّل المودات -mods.install=تثبيت mods.save_as=حفظ باسم mods.unknown=مود غير معروف @@ -1444,6 +1443,7 @@ settings.take_effect_after_restart=يُطبَّق بعد إعادة التشغي settings.type=نوع إعدادات النسخة settings.type.global.manage=الإعدادات العامة +shaderpack=الظلال shaderpack.download.title=تنزيل ظلال - %1s sponsor=المتبرعون diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 6e93385e37d..fc66bbb4364 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -164,6 +164,7 @@ addon.check_update.file=Archivo addon.check_update.source=Fuente addon.check_update.target_version=Versión de destino addon.curseforge=CurseForge +addon.install=Instalar addon.modrinth=Modrinth archive.author=Autor(es) @@ -346,7 +347,6 @@ datetime.format=d MMM yyyy, H\:mm\:ss download=Descargar download.code.404=Archivo no encontrado en el servidor remoto: %s download.content=Complementos -download.shader=Sombreadores download.curseforge.unavailable=Esta versión de HMCL no permite acceder a CurseForge. Utilice la versión oficial para acceder a CurseForge. download.existing=El archivo no se puede guardar porque ya existe. Puedes hacer clic en «Guardar como» para guardar el archivo en otro lugar. download.external_link=Abrir sitio web @@ -1065,7 +1065,6 @@ mods.not_modded=¡Debes instalar primero un cargador de mods (Forge, NeoForge, F mods.restore=Restaurar mods.url=Página oficial mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar? -mods.install=Instalar mods.save_as=Guardar como nbt.entries=%s entradas @@ -1344,6 +1343,8 @@ settings.take_effect_after_restart=Se aplica después de reiniciar settings.type=Tipo de configuración de la instancia settings.type.global.manage=Config. Global +shaderpack=Sombreadores + sponsor=Donantes sponsor.bmclapi=Las descargas de China continental son proporcionadas por BMCLAPI. Haga clic aquí para obtener más información. sponsor.hmcl=Hello Minecraft! Launcher es un launcher FOSS de Minecraft que permite a los usuarios gestionar múltiples instancias de Minecraft fácilmente. Haga clic aquí para obtener más información. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index e8c7856aa8e..6dc4c0a5b5e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -299,7 +299,6 @@ datetime.format=yyyy/MM/dd H:mm:ss download=ダウンロード download.code.404=リモートサーバーにファイルが見つかりません:%s download.content=ゲームコンテンツ -download.shader=シェーダー download.existing=ファイルは既に存在するため、保存できません。「名前を付けて保存」を選択して、ファイルを別の場所に保存できます。 download.external_link=ダウンロードサイトを開く download.failed=%1$s のダウンロードに失敗しました、応答コード:%2$d @@ -871,6 +870,8 @@ settings.take_effect_after_restart=再起動後に有効になります settings.type=バージョン設定タイプ settings.type.global.manage=グローバルゲーム設定 +shaderpack=シェーダー + sponsor=寄付 sponsor.bmclapi=中国本土向けのダウンロードサービスはBMCLAPIによって提供されています。詳細については、ここをクリックしてください。 sponsor.hmcl=Hello Minecraft! Launcherは、無料のオープンソースのMinecraftランチャーであり、ユーザーは複数の個別のMinecraftインストールを簡単に管理できます。詳細については、ここをクリックしてください。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 7c8c5eb4f3a..354e74bede4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -342,7 +342,6 @@ datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss download=引 download.code.404=伺服器無案曰:%s\n君可求助於右上之鈕。 download.content=戲案 -download.shader=光影 download.curseforge.unavailable=HMCL 是版不能訪 CurseForge。宜行官版以取之。 download.existing=是案既存,無可貯之。宜移案他處。 download.external_link=啟引之網葉 @@ -877,7 +876,7 @@ mods.not_modded=君須先於「自裝」頁裝鍛、新鍛、緞、褥或輕載 mods.restore=復 mods.url=改囊官網 mods.update_modpack_mod.warning=更迭改囊集之改囊或損之,以不能啟。行而不復,誠更迭乎? -mods.install=裝至是例 +addon.install=裝至是例 mods.save_as=引至私案夾 nbt.entries=%s 元 @@ -1167,6 +1166,7 @@ settings.take_effect_after_restart=復啟而效 settings.type=戲例置設之類 settings.type.global.manage=全例戲設 +shaderpack=光影 shaderpack.download.title=引光影 - %1s sponsor=資勉 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index e9093c84542..7dca9be3e7a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -163,6 +163,7 @@ addon.check_update.file=Файл addon.check_update.source=Источник addon.check_update.target_version=Целевая версия addon.curseforge=CurseForge +addon.install=Установить addon.modrinth=Modrinth archive.author=Автор(ы) @@ -343,7 +344,6 @@ datetime.format=d MMM yyyy г., HH\:mm\:ss download=Скачать download.code.404=Файл «%s» не найден на удаленном сервере. download.content=Аддоны -download.shader=Шейдеры download.curseforge.unavailable=Эта сборка HMCL не поддерживает доступ к CurseForge. Пожалуйста, используйте официальную сборку для доступа к CurseForge. download.existing=Файл существует и по этому не может быть сохранён. Можно использовать «Сохранить как», чтобы сохранить файл в другом месте. download.external_link=Открыть сайт @@ -1058,7 +1058,6 @@ mods.not_modded=Для управления модами необходимо с mods.restore=Восстановить mods.url=Официальная страница mods.update_modpack_mod.warning=Обновление модов в пакете модов может привести к непоправимым результатам, возможно, повредить пакет модов так, что он не сможет запуститься. Вы уверены, что хотите обновить? -mods.install=Установить mods.save_as=Сохранить как nbt.entries=%s записи @@ -1333,6 +1332,8 @@ settings.take_effect_after_restart=Применится после переза settings.type=Тип настроек экземпляра settings.type.global.manage=Глобальные настройки +shaderpack=Шейдеры + sponsor=Спонсоры sponsor.bmclapi=Загрузки для материкового Китая предоставляются BMCLAPI. Нажмите здесь для получения подробностей. sponsor.hmcl=Hello Minecraft! Launcher — лаунчер Minecraft с открытым исходным кодом, который позволяет пользователям легко управлять несколькими экземплярами Minecraft. Для получения подробностей нажмите здесь. diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 3607e586ebe..c0a1fb40cc6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -163,6 +163,7 @@ addon.check_update.file=Файл addon.check_update.source=Джерело addon.check_update.target_version=Цільова версія addon.curseforge=CurseForge +addon.install=Встановити addon.modrinth=Modrinth archive.author=Автор(и) @@ -342,7 +343,6 @@ datetime.format=dd.MM.yyyy, HH:mm:ss download=Завантажити download.code.404=Файл "%s" не знайдено на віддаленому сервері. download.content=Додатки -download.shader=Шейдери download.curseforge.unavailable=Ця збірка HMCL не підтримує доступ до CurseForge. Використовуйте офіційну збірку для доступу до CurseForge. download.existing=Файл не може бути збережений, оскільки він вже існує. Ви можете натиснути "Зберегти як", щоб зберегти файл в іншому місці. download.external_link=Відвідати веб-сайт завантаження @@ -1006,7 +1006,6 @@ mods.not_modded=Ви повинні спочатку встановити зав mods.restore=Відновити mods.url=Офіційна сторінка mods.update_modpack_mod.warning=Оновлення модів у модпаку може призвести до непоправних наслідків, можливо, пошкодивши модпак так, що він не зможе запуститися. Ви впевнені, що хочете оновити? -mods.install=Встановити mods.save_as=Зберегти як nbt.entries=%s записів @@ -1282,6 +1281,8 @@ settings.take_effect_after_restart=Застосовується після пе settings.type=Тип налаштувань екземпляра settings.type.global.manage=Глобальні налаштування +shaderpack=Шейдери + sponsor=Спонсори sponsor.bmclapi=Завантаження для материкового Китаю надається BMCLAPI. Натисніть тут для отримання додаткової інформації. sponsor.hmcl=Hello Minecraft! Лаунчер - це FOSS лаунчер Minecraft, який дозволяє користувачам легко керувати кількома екземплярами Minecraft. Натисніть тут для отримання додаткової інформації. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 9e571a5b40c..c3aada374f5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -196,6 +196,7 @@ addon.dependency.incompatible=不相容附加內容 (與正在下載的檔案同 addon.dependency.broken=損壞的相依內容 (該相依內容曾經存在於附加內容下載源中,但現在已被刪除,請嘗試其他下載源) addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (快照) +addon.install=安裝到目前實例 addon.modrinth=Modrinth archive.author=作者 @@ -390,7 +391,6 @@ datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss download=下載 download.code.404=遠端伺服器沒有需要下載的檔案:%s download.content=遊戲內容 -download.shader=光影 download.curseforge.unavailable=這個 HMCL 版本不支援訪問 CurseForge。請使用官方版本進行下載。 download.existing=檔案已存在,無法儲存。你可以將檔案儲存至其他地方。 download.external_link=開啟下載網站 @@ -437,6 +437,7 @@ extension.mod=模組檔案 extension.modloader.installer=模組載入器安裝檔 extension.resourcepack=資源包壓縮檔 extension.schematic=原理圖檔案 +extension.shaderpack=光影包壓縮包 extension.world=世界壓縮檔 fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。 @@ -960,7 +961,6 @@ mods.restore=回退 mods.url=官方頁面 mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎? mods.warning.loader_mismatch=模組載入器不匹配 -mods.install=安裝到目前實例 mods.save_as=下載到本機目錄 mods.unknown=未知模組 @@ -1429,7 +1429,14 @@ settings.type.global.preset.remove=刪除預設 settings.type.global.preset.remove.confirm=確定要刪除預設「%s」嗎?使用該預設的實例將改用預設項目。 settings.type.global.preset.auto_name=預設 %d +shaderpack=光影 +shaderpack.add=新增光影包 +shaderpack.add.failed=新增光影包失敗: +shaderpack.delete.failed=刪除光影包失敗 shaderpack.download.title=光影下載 - %1s +shaderpack.loader.aperture=Aperture +shaderpack.loader.optifine_iris=Optifine/Iris +shaderpack.manage=光影包管理 sponsor=贊助 sponsor.bmclapi=中國大陸下載源由 BMCLAPI 提供高速下載服務。點選此處查閱詳細訊息。 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 3f339510c78..b8137104010 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -198,6 +198,7 @@ addon.dependency.incompatible=不兼容的附加内容 (同时与正在下载的 addon.dependency.broken=损坏的前置内容 (该前置内容曾经在该附加内容下载源上存在过,但现在被删除了,换个下载源试试吧) addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (快照) +addon.install=安装到当前实例 addon.modrinth=Modrinth archive.author=作者 @@ -392,7 +393,6 @@ datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss download=下载 download.code.404=远程服务器不包含需要下载的文件: %s\n你可以点击右上角帮助按钮进行求助。 download.content=游戏内容 -download.shader=光影 download.curseforge.unavailable=此 HMCL 版本不支持访问 CurseForge。请使用官方版本进行下载。 download.existing=文件已存在,无法保存。你可以将文件保存至其他地方。 download.external_link=打开下载网站 @@ -440,6 +440,7 @@ extension.mod=模组文件 extension.modloader.installer=模组加载器安装包 extension.resourcepack=资源包压缩包 extension.schematic=原理图文件 +extension.shaderpack=光影包压缩包 extension.world=世界压缩包 fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。 @@ -965,7 +966,6 @@ mods.restore=回退 mods.url=官方页面 mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗? mods.warning.loader_mismatch=模组加载器不匹配 -mods.install=安装到当前实例 mods.save_as=下载到本地文件夹 mods.unknown=未知模组 @@ -1434,7 +1434,14 @@ settings.type.global.preset.remove=删除预设 settings.type.global.preset.remove.confirm=确定要删除预设“%s”吗?使用该预设的实例将回退到默认预设。 settings.type.global.preset.auto_name=预设 %d +shaderpack=光影 +shaderpack.add=添加光影包 +shaderpack.add.failed=添加光影包失败: +shaderpack.delete.failed=删除光影包失败 shaderpack.download.title=光影下载 - %1s +shaderpack.loader.aperture=Aperture +shaderpack.loader.optifine_iris=Optifine/Iris +shaderpack.manage=光影包管理 sponsor=赞助 sponsor.bmclapi=国内下载源由 BMCLAPI 提供高速下载服务。BMCLAPI 为公益服务,赞助 BMCLAPI 可以帮助作者更好地提供稳定高速的下载服务。[点击此处查阅详细信息] diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LoaderType.java new file mode 100644 index 00000000000..eaf5fa6bc34 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LoaderType.java @@ -0,0 +1,27 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon; + +import java.util.Set; + +/// For mods and shaders +public interface LoaderType { + + Set names(); + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java index ab6c9d4aeca..be53fa38460 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java @@ -50,10 +50,17 @@ public boolean isDisabled() { public abstract boolean keepOldFiles(); - public abstract void delete() throws IOException; + public void delete() throws IOException { + FileUtils.forceDeleteIfExists(getFile()); + } @Nullable - public abstract AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException; + public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException { + return null; + } + + public void onUpdated(String newFileNameWithExtension) { + } public record AddonUpdate( LocalAddonFile localAddonFile, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java index 0520b67a8fc..f636317b128 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java @@ -94,7 +94,7 @@ public Path setOld(T modFile, boolean old) throws IOException { } } - private Path backupFile(Path file) throws IOException { + public static Path backupFile(Path file) throws IOException { Path newPath = file.resolveSibling( StringUtils.addSuffix( StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION), @@ -107,7 +107,7 @@ private Path backupFile(Path file) throws IOException { return newPath; } - private Path restoreFile(Path file) throws IOException { + public static Path restoreFile(Path file) throws IOException { Path newPath = file.resolveSibling( StringUtils.removeSuffix(FileUtils.getName(file), OLD_EXTENSION) ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java index f4bfafab115..b7c10d28264 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java @@ -27,7 +27,6 @@ import org.jackhuang.hmcl.util.io.FileUtils; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -189,11 +188,6 @@ public void markDisabled() throws IOException { file = modManager.disableMod(file); } - @Override - public void delete() throws IOException { - Files.deleteIfExists(file); - } - @Override public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException { RemoteAddonRepository repository = source.getRepoForType(RemoteAddonRepository.Type.MOD); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModLoaderType.java index 61ba46cea84..bc9a5f91f0b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModLoaderType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModLoaderType.java @@ -17,13 +17,28 @@ */ package org.jackhuang.hmcl.addon.mod; -public enum ModLoaderType { +import org.jackhuang.hmcl.addon.LoaderType; + +import java.util.Set; + +public enum ModLoaderType implements LoaderType { UNKNOWN, - FORGE, - CLEANROOM, - NEO_FORGE, - FABRIC, - QUILT, - LITE_LOADER, - LEGACY_FABRIC + FORGE("forge"), + CLEANROOM("cleanroom"), + NEO_FORGE("neoforge"), + FABRIC("fabric"), + QUILT("quilt"), + LITE_LOADER("liteloader"), + LEGACY_FABRIC("legacy-fabric"); + + private final Set names; + + ModLoaderType(String... names) { + this.names = Set.of(names); + } + + @Override + public Set names() { + return names; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java index 8acc3151117..9b9540587a1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java @@ -20,7 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.addon.LocalAddonFile; import org.jackhuang.hmcl.addon.LocalAddonManager; -import org.jackhuang.hmcl.addon.meta.*; +import org.jackhuang.hmcl.addon.mod.meta.*; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.util.Pair; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/FabricModMetadata.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/FabricModMetadata.java index 4601ed1ca5d..e7098789f91 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/FabricModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeNewModMetadata.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeNewModMetadata.java index 6b7ff0085b4..e86c1ca0df7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeNewModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadata.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadata.java index 81f358c94e6..a49e7926351 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadataLst.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadataLst.java similarity index 95% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadataLst.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadataLst.java index 25909ac0ccd..8d1a1308344 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadataLst.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadataLst.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import org.jackhuang.hmcl.util.gson.JsonSerializable; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/LiteModMetadata.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/LiteModMetadata.java index aea8c4fab2e..dea7f306a1a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/LiteModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonParseException; import kala.compress.archivers.zip.ZipArchiveEntry; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/QuiltModMetadata.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/QuiltModMetadata.java index 02e31ed7269..775acbf0f4b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/QuiltModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/PackMcMeta.java similarity index 97% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/PackMcMeta.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/PackMcMeta.java index 3cc4d34e43c..76507488c9f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/PackMcMeta.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.pack; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; @@ -40,10 +40,8 @@ @JsonSerializable public record PackMcMeta(@SerializedName("pack") PackInfo pack) implements Validation { - private static final Gson LENIENT_GSON = JsonUtils.defaultGsonBuilder().setStrictness(Strictness.LENIENT).create(); - public static PackMcMeta fromNonNullJson(String jsonString) throws JsonParseException { - PackMcMeta parsed = LENIENT_GSON.fromJson(jsonString, PackMcMeta.class); + PackMcMeta parsed = JsonUtils.LENIENT_GSON.fromJson(jsonString, PackMcMeta.class); if (parsed == null) throw new JsonParseException("Json object cannot be null."); return parsed; @@ -51,7 +49,7 @@ public static PackMcMeta fromNonNullJson(String jsonString) throws JsonParseExce public static PackMcMeta fromNonNullJsonFile(Path jsonFile) throws JsonParseException, IOException { try (var reader = Files.newBufferedReader(jsonFile)) { - PackMcMeta parsed = LENIENT_GSON.fromJson(reader, PackMcMeta.class); + PackMcMeta parsed = JsonUtils.LENIENT_GSON.fromJson(reader, PackMcMeta.class); if (parsed == null) throw new JsonParseException("Json object cannot be null."); return parsed; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/datapack/DataPack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/datapack/DataPack.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/datapack/DataPack.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/datapack/DataPack.java index 6c15efbe74d..593cd0d4f16 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/datapack/DataPack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/datapack/DataPack.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.datapack; +package org.jackhuang.hmcl.addon.pack.datapack; import com.google.gson.JsonParseException; import javafx.application.Platform; @@ -24,7 +24,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.jackhuang.hmcl.addon.LocalAddonFile; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFile.java similarity index 74% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFile.java index ffa35e4c227..e9c15697aa5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFile.java @@ -15,14 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import javafx.scene.image.Image; import org.jackhuang.hmcl.addon.LocalAddonFile; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,32 +31,44 @@ import java.nio.file.Path; import java.util.List; import java.util.Locale; +import java.util.Objects; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public sealed abstract class ResourcePackFile extends LocalAddonFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { - static ResourcePackFile fromFile(ResourcePackManager manager, Path path) throws IOException { - if (isFileResourcePack(path)) { - return Files.isRegularFile(path) ? new ResourcePackZipFile(manager, path) : new ResourcePackFolder(manager, path); - } - return null; + static @Nullable ResourcePackFile fromFile(ResourcePackManager manager, Path path) throws IOException { + return Files.isRegularFile(path) ? ResourcePackZipFile.load(manager, path) : ResourcePackFolder.load(manager, path); } public static boolean isFileResourcePack(Path file) { if (Files.isDirectory(file)) return Files.isRegularFile(file.resolve("pack.mcmeta")); - if (Files.isRegularFile(file)) return file.toString().toLowerCase(Locale.ROOT).endsWith(".zip"); + if (Files.isRegularFile(file) && file.toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + try (var zipFileTree = CompressingUtils.openZipFile(file)) { + return zipFileTree.getEntry("pack.mcmeta") != null; + } catch (IOException e) { + LOG.warning("Failed to check if file is resource pack", e); + } + } return false; } protected final ResourcePackManager manager; protected Path file; + protected final PackMcMeta meta; + protected final Image icon; + protected final String fileName; protected final String fileNameWithExtension; private Compatibility compatibility = null; - protected ResourcePackFile(ResourcePackManager manager, Path file) { + protected ResourcePackFile(ResourcePackManager manager, Path file, @NotNull PackMcMeta meta, @Nullable Image icon) { super(); this.manager = manager; this.file = file; + this.meta = Objects.requireNonNull(meta); + this.icon = icon; + this.fileName = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(file)); this.fileNameWithExtension = file.getFileName().toString(); } @@ -112,19 +124,21 @@ public final boolean keepOldFiles() { public void markDisabled() { } - @Nullable - @Contract(pure = true) - public abstract PackMcMeta getMeta(); + public @NotNull PackMcMeta getMeta() { + return meta; + } + + // 64 * 64 + public @Nullable Image getIcon() { + return icon; + } @Nullable public LocalAddonFile.Description getDescription() { - if (getMeta() == null || getMeta().pack() == null) return null; + if (getMeta().pack() == null) return null; return getMeta().pack().description(); } - // 64*64 - public abstract @Nullable Image getIcon(); - @Override public int compareTo(@NotNull ResourcePackFile other) { return this.fileNameWithExtension.compareTo(other.fileNameWithExtension); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFolder.java similarity index 64% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFolder.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFolder.java index 7267d04923d..607f2775c41 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFolder.java @@ -15,13 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import javafx.scene.image.Image; -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.addon.RemoteAddon; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; -import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; @@ -32,22 +29,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; final class ResourcePackFolder extends ResourcePackFile { - private final PackMcMeta meta; - private final @Nullable Image icon; - - public ResourcePackFolder(ResourcePackManager manager, Path path) { - super(manager, path); + public static @Nullable ResourcePackFolder load(ResourcePackManager manager, Path path) { PackMcMeta meta = null; try { meta = PackMcMeta.fromNonNullJsonFile(path.resolve("pack.mcmeta")); } catch (Exception e) { LOG.warning("Failed to parse resource pack meta", e); } - this.meta = meta; + if (meta == null) return null; byte[] iconData = null; - Image iconTemp = null; + Image icon = null; try { iconData = Files.readAllBytes(path.resolve("pack.png")); } catch (IOException e) { @@ -55,31 +48,15 @@ public ResourcePackFolder(ResourcePackManager manager, Path path) { } if (iconData != null) { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { - iconTemp = new Image(inputStream, 64, 64, true, true); + icon = new Image(inputStream, 64, 64, true, true); } catch (Exception e) { LOG.warning("Failed to load resource pack icon", e); } } - this.icon = iconTemp; - } - - @Override - public PackMcMeta getMeta() { - return meta; - } - - @Override - public @Nullable Image getIcon() { - return icon; - } - - @Override - public void delete() throws IOException { - FileUtils.deleteDirectory(file); + return new ResourcePackFolder(manager, path, meta, icon); } - @Override - public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) { - return null; + private ResourcePackFolder(ResourcePackManager manager, Path path, PackMcMeta meta, @Nullable Image icon) { + super(manager, path, meta, icon); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManager.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManager.java index d571f6422cf..ca2fcffe32d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManager.java @@ -15,12 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.addon.LocalAddonManager; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonSerializable; @@ -344,11 +344,7 @@ public void importResourcePack(Path file) throws IOException, IllegalArgumentExc Files.createDirectories(resourcePackDirectory); Path newFile = resourcePackDirectory.resolve(file.getFileName()); - if (Files.isDirectory(file)) { - FileUtils.copyDirectory(file, newFile); - } else { - FileUtils.copyFile(file, newFile); - } + FileUtils.copyTo(file, resourcePackDirectory); addResourcePackInfo(newFile); } else { @@ -502,7 +498,7 @@ public Stream> arePacksEnabled(Stream. */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import javafx.scene.image.Image; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; import java.util.List; @@ -36,21 +35,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; final class ResourcePackZipFile extends ResourcePackFile { - private final PackMcMeta meta; - private final @Nullable Image icon; - public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException { - super(manager, path); - - PackMcMeta metaTemp = null; + public static @Nullable ResourcePackZipFile load(ResourcePackManager manager, Path path) throws IOException { + PackMcMeta meta = null; byte[] iconData = null; try (var zipFileTree = CompressingUtils.openZipTree(path)) { try { - metaTemp = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta")); + meta = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta")); } catch (Exception e) { LOG.warning("Failed to parse resource pack meta", e); } + if (meta == null) return null; var iconEntry = zipFileTree.getEntry("/pack.png"); if (iconEntry != null) { @@ -61,32 +57,20 @@ public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOExce } } } - this.meta = metaTemp; - Image iconTemp = null; + Image icon = null; if (iconData != null) { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { - iconTemp = new Image(inputStream, 64, 64, true, true); + icon = new Image(inputStream, 64, 64, true, true); } catch (Exception e) { LOG.warning("Failed to load resource pack icon", e); } } - this.icon = iconTemp; - } - - @Override - public PackMcMeta getMeta() { - return meta; + return new ResourcePackZipFile(manager, path, meta, icon); } - @Override - public @Nullable Image getIcon() { - return icon; - } - - @Override - public void delete() throws IOException { - Files.deleteIfExists(file); + private ResourcePackZipFile(ResourcePackManager manager, Path path, PackMcMeta meta, @Nullable Image icon) { + super(manager, path, meta, icon); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index 3d12959d30f..e5422b1b701 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -138,7 +138,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion response = withApiKey(HttpRequest.GET(candidate.toString())) .getJson(Response.typeOf(listTypeOf(CurseAddon.class))); if (searchFilter.isEmpty()) { - return new SearchResult(response.data().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); + return new SearchResult(response.data().stream().map(addon -> addon.toAddon(type)), calculateTotalPages(response, pageSize)); } break; } catch (IOException e) { @@ -167,7 +167,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); - return new SearchResult(response.data().stream().map(addon -> addon.toMod(type)).map(remoteMod -> { + return new SearchResult(response.data().stream().map(addon -> addon.toAddon(type)).map(remoteMod -> { String lowerCaseResult = remoteMod.title().toLowerCase(Locale.ROOT); int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult); @@ -178,7 +178,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } return pair(remoteMod, diff); - }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.data().stream().map(addon -> addon.toMod(type)), calculateTotalPages(response, pageSize)); + }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.data().stream().map(addon -> addon.toAddon(type)), calculateTotalPages(response, pageSize)); } finally { SEMAPHORE.release(); } @@ -227,7 +227,7 @@ public RemoteAddon getModById(DownloadProvider downloadProvider, String id) thro try { Response response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id)) .getJson(Response.typeOf(CurseAddon.class)); - return response.data.toMod(type); + return response.data.toAddon(type); } finally { SEMAPHORE.release(); } @@ -379,7 +379,7 @@ public Stream loadVersions(RemoteAddonRepository modReposit return modRepository.getRemoteVersionsById(downloadProvider, Integer.toString(id)); } - public RemoteAddon toMod(Type type) { + public RemoteAddon toAddon(Type type) { String iconUrl = ""; if (logo != null) { if (StringUtils.isNotBlank(logo.thumbnailUrl())) @@ -393,7 +393,7 @@ else if (StringUtils.isNotBlank(logo.url())) "", name, summary, - categories.stream().map(category -> Integer.toString(category.getId())).collect(Collectors.toList()), + categories.stream().map(category -> Integer.toString(category.getId())).toList(), links.websiteUrl, iconUrl, this, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java index c4deb57d091..196de7c78e5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/ModrinthRemoteAddonRepository.java @@ -153,7 +153,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion LOG.info("Fetching " + candidate); Response response = HttpRequest.GET(candidate.toString()) .getJson(Response.typeOf(ProjectSearchResult.class)); - return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize)); + return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toAddon), (int) Math.ceil((double) response.totalHits / pageSize)); } catch (IOException e) { LOG.warning("Failed to search addons: " + candidate, e); @@ -463,7 +463,7 @@ public Stream loadVersions(RemoteAddonRepository modReposit return modRepository.getRemoteVersionsById(downloadProvider, projectId()); } - public RemoteAddon toMod() { + public RemoteAddon toAddon() { Type type = switch (projectType) { case "modpack" -> Type.MODPACK; case "resourcepack" -> Type.RESOURCE_PACK; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderFile.java new file mode 100644 index 00000000000..b0a2a0ddaa0 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderFile.java @@ -0,0 +1,121 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon.shader; + +import javafx.scene.image.Image; +import org.jackhuang.hmcl.addon.LocalAddonFile; +import org.jackhuang.hmcl.addon.LocalAddonManager; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public sealed abstract class ShaderFile extends LocalAddonFile implements Comparable permits ShaderZipFile, ShaderFolder { + public static @Nullable ShaderFile fromFile(Path file) throws IOException { + return Files.isRegularFile(file) ? ShaderZipFile.load(file) : ShaderFolder.load(file); + } + + public static boolean isFileShaderPack(Path file) { + if (Files.isDirectory(file)) return Files.isDirectory(file.resolve("shaders")); + if (Files.isRegularFile(file)) { + try (var zipSystem = CompressingUtils.createReadOnlyZipFileSystem(file)) { + Path root = zipSystem.getRootDirectories().iterator().next(); + try (Stream stream = Files.walk(root)) { + return stream.filter(path -> path.endsWith("shaders")).anyMatch(Files::isDirectory); + } + } catch (IOException e) { + LOG.warning("Failed to check if file is shader pack", e); + } + } + return false; + } + + protected Path file; + protected final String fileNameWithoutExtension; + protected final ShaderLoaderType loaderType; + protected final @Nullable ShaderPackMeta shaderPackMeta; + protected final @Nullable Image icon; + + protected ShaderFile(Path file, ShaderLoaderType loaderType, @Nullable ShaderPackMeta shaderPackMeta, @Nullable Image icon) { + this.file = file; + this.fileNameWithoutExtension = FileUtils.getNameWithoutExtension(file); + this.loaderType = loaderType; + this.shaderPackMeta = shaderPackMeta; + this.icon = icon; + } + + @Override + public Path getFile() { + return file; + } + + @Override + public String getFileName() { + return fileNameWithoutExtension; + } + + public ShaderLoaderType getLoaderType() { + return loaderType; + } + + public @Nullable ShaderPackMeta getMeta() { + return shaderPackMeta; + } + + public @Nullable Image getIcon() { + return icon; + } + + public @NotNull String getName() { + return Optional.ofNullable(getMeta()).map(ShaderPackMeta::name).orElse(getFile().getFileName().toString()); + } + + @Override + public boolean isDisabled() { + return false; + } + + @Override + public void markDisabled() { + // NO-OP + } + + @Override + public boolean keepOldFiles() { + return false; + } + + @Override + public void setOld(boolean old) throws IOException { + if (old) file = LocalAddonManager.backupFile(file); + else file = LocalAddonManager.restoreFile(file); + } + + @Override + public int compareTo(@NotNull ShaderFile other) { + return fileNameWithoutExtension.compareTo(other.fileNameWithoutExtension); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderFolder.java new file mode 100644 index 00000000000..d2a6ff62d6d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderFolder.java @@ -0,0 +1,66 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon.shader; + +import javafx.scene.image.Image; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +final class ShaderFolder extends ShaderFile { + + public static @Nullable ShaderFolder load(Path file) { + Path shadersPath = file.resolve("shaders"); + if (!Files.isDirectory(shadersPath)) return null; + + ShaderPackMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(JsonUtils.LENIENT_GSON, shadersPath.resolve("pack.json"), ShaderPackMeta.class); + } catch (IOException e) { + LOG.warning("Failed to load shader metadata", e); + } + byte[] iconData = null; + Image icon = null; + try { + iconData = Files.readAllBytes(shadersPath.resolve("pack.png")); + } catch (IOException e) { + LOG.warning("Failed to read aperture shader icon", e); + } + if (iconData != null) { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { + icon = new Image(inputStream, 64, 64, true, true); + } catch (Exception e) { + LOG.warning("Failed to load aperture shader icon", e); + } + } + if (Files.isRegularFile(shadersPath.resolve("pack.ts"))) { // Aperture + return new ShaderFolder(file, ShaderLoaderType.APERTURE, meta, icon); + } + return new ShaderFolder(file, ShaderLoaderType.OPTIFINE_IRIS, meta, icon); + } + + private ShaderFolder(Path file, ShaderLoaderType loaderType, @Nullable ShaderPackMeta shaderPackMeta, @Nullable Image icon) { + super(file, loaderType, shaderPackMeta, icon); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderLoaderType.java new file mode 100644 index 00000000000..a42c757db35 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderLoaderType.java @@ -0,0 +1,38 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon.shader; + +import org.jackhuang.hmcl.addon.LoaderType; + +import java.util.Set; + +public enum ShaderLoaderType implements LoaderType { + OPTIFINE_IRIS("optifine", "iris"), + APERTURE("aperture"); + + private final Set names; + + ShaderLoaderType(String... names) { + this.names = Set.of(names); + } + + @Override + public Set names() { + return names; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackMeta.java new file mode 100644 index 00000000000..d2eed08de1c --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackMeta.java @@ -0,0 +1,27 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon.shader; + +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.util.gson.JsonSerializable; +import org.jetbrains.annotations.Nullable; + +@JsonSerializable +public record ShaderPackMeta(@Nullable String name, @Nullable String version, + @SerializedName(value = "shaderDescription") @Nullable String description) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderZipFile.java new file mode 100644 index 00000000000..a9597bb7449 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderZipFile.java @@ -0,0 +1,116 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.addon.shader; + +import javafx.scene.image.Image; +import org.jackhuang.hmcl.addon.RemoteAddon; +import org.jackhuang.hmcl.addon.RemoteAddonRepository; +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +final class ShaderZipFile extends ShaderFile { + + public static @Nullable ShaderZipFile load(Path file) throws IOException { + if (!Files.isRegularFile(file) || !file.toString().toLowerCase(Locale.ROOT).endsWith(".zip")) return null; + + try (var zipSystem = CompressingUtils.createReadOnlyZipFileSystem(file)) { + Path root = zipSystem.getRootDirectories().iterator().next(); + try (Stream stream = Files.walk(root)) { + Path shadersPath = stream + .filter(path -> path.endsWith("shaders")) + .filter(Files::isDirectory) + .findFirst().orElse(null); + if (shadersPath == null) return null; + + ShaderPackMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(JsonUtils.LENIENT_GSON, shadersPath.resolve("pack.json"), ShaderPackMeta.class); + } catch (IOException e) { + LOG.warning("Failed to load aperture shader metadata", e); + } + byte[] iconData = null; + Image icon = null; + try { + iconData = Files.readAllBytes(shadersPath.resolve("pack.png")); + } catch (IOException e) { + LOG.warning("Failed to read aperture shader icon", e); + } + if (iconData != null) { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) { + icon = new Image(inputStream, 64, 64, true, true); + } catch (Exception e) { + LOG.warning("Failed to load aperture shader icon", e); + } + } + if (Files.isRegularFile(shadersPath.resolve("pack.ts"))) { // Aperture + return new ShaderZipFile(file, ShaderLoaderType.APERTURE, meta, icon); + } + return new ShaderZipFile(file, ShaderLoaderType.OPTIFINE_IRIS, meta, icon); + } + } + } + + private ShaderZipFile(Path file, ShaderLoaderType loaderType, @Nullable ShaderPackMeta shaderPackMeta, @Nullable Image icon) { + super(file, loaderType, shaderPackMeta, icon); + } + + @Override + public @Nullable AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException { + RemoteAddonRepository repository = source.getRepoForType(RemoteAddonRepository.Type.SHADER_PACK); + if (repository == null) return null; + Optional currentVersion = repository.getRemoteVersionByLocalFile(file); + if (currentVersion.isEmpty()) return null; + List remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().modid()) + .filter(version -> version.gameVersions().contains(gameVersion)) //TODO loader + .filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0) + .sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()) + .toList(); + if (remoteVersions.isEmpty()) return null; + return new AddonUpdate(this, currentVersion.get(), remoteVersions.get(0), true); + } + + @Override + public void onUpdated(String newFileNameWithExtension) { + super.onUpdated(newFileNameWithExtension); + var configPath = getFile().resolveSibling(getFileName() + ".zip.txt"); + var newConfigPath = getFile().resolveSibling(newFileNameWithExtension + ".txt"); + if (Files.isRegularFile(configPath)) { + try { + Files.move(configPath, newConfigPath); + } catch (IOException e) { + LOG.warning("Failed to rename shader config file " + configPath, e); + } + } else { + LOG.warning("Failed to rename shader config file " + configPath + " because the file doesn't exist"); + } + } +} 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 fb3f606081e..0d8669b8458 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -151,6 +151,11 @@ public Path getResourcePackDirectory(String id) { return getRunDirectory(id).resolve("resourcepacks"); } + @Override + public Path getShadersDirectory(String id) { + return getRunDirectory(id).resolve("shaderpacks"); + } + @Override public Path getVersionRoot(String id) { return getBaseDirectory().resolve("versions/" + id); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java index 1b7cc27dcd6..5e8377202f1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -143,6 +143,12 @@ default Task refreshVersionsAsync() { /// @return the resource pack directory Path getResourcePackDirectory(String id); + /// Get the directory for placing shader packs. + /// + /// @param id instance id + /// @return the shader pack directory + Path getShadersDirectory(String id); + /** * Get minecraft jar * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 0451c92ea90..08a0670df0e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -79,6 +79,16 @@ public final class JsonUtils { .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE) .create(); + /// The lenient [Gson] instance. + /// + /// Configured with: + /// - Pretty printing enabled. + /// - Complex map key serialization enabled. + /// - Type adapters for [java.time.Instant], [java.util.UUID], and [java.nio.file.Path]. + /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapterFactory], and + /// [JsonTypeAdapterFactory]. + public static final Gson LENIENT_GSON = defaultGsonBuilder().setStrictness(Strictness.LENIENT).create(); + /// Not instantiable. private JsonUtils() { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index c583860bcc8..262e9290b12 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -468,6 +468,14 @@ public static void forceDelete(Path file) Files.delete(file); } + public static void forceDeleteIfExists(Path file) + throws IOException { + if (Files.isDirectory(file)) + deleteDirectory(file); + else + Files.deleteIfExists(file); + } + public static void copyFile(Path srcFile, Path destFile) throws IOException { Objects.requireNonNull(srcFile, "Source must not be null"); @@ -483,6 +491,16 @@ public static void copyFile(Path srcFile, Path destFile) Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); } + public static void copyTo(Path src, Path destDir) throws IOException { + Objects.requireNonNull(src, "Source must not be null"); + Objects.requireNonNull(destDir, "Destination directory must not be null"); + Path dest = destDir.resolve(src.getFileName()); + if (Files.isDirectory(src)) + copyDirectory(src, dest); + else + copyFile(src, dest); + } + public static List listFilesByExtension(Path file, String extension) { try (Stream list = Files.list(file)) { return list.filter(it -> Files.isRegularFile(it) && extension.equals(getExtension(it))) diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/resourcepack/ResourcePackManagerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManagerTest.java similarity index 88% rename from HMCLCore/src/test/java/org/jackhuang/hmcl/resourcepack/ResourcePackManagerTest.java rename to HMCLCore/src/test/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManagerTest.java index 26ab05a748b..eb990917c78 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/resourcepack/ResourcePackManagerTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManagerTest.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,13 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.junit.jupiter.api.Test; -import static org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager.isMcVersionSupported; -import static org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager.isMcVersionSupportsNewOptionsFormat; +import static org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager.isMcVersionSupported; +import static org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager.isMcVersionSupportsNewOptionsFormat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue;