From 242df8a81a2aa16f2e1ed0737764bd71692da2ba Mon Sep 17 00:00:00 2001 From: Burning_TNT <88144530+burningtnt@users.noreply.github.com> Date: Sun, 31 Dec 2023 23:15:54 +0800 Subject: [PATCH] Enhance mod download (#2411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support #2376 * Add necessary @Nullable annotations * Display different types of dependencies in different sections. * Fix checkstyle * Add I18N for different types of dependencies. * Enhance UI * Code cleanup * Enhance UI * Manually sort the result from curseforge when searching mods by name. * Render the search results from remote mod repositories in several pages. * Fix merge * Fix * Add a button which navigates to the modpack download page in the modpack installl page * Fix I18N * Render the mod loaders supported by the version in mod info page. * Fix #2104 * Enhance TwoLineListItem * Render the mod loader supported by this mod file on the ModListPage * Fix chinese searching and curseforge searching * Update I18N * Fix * Fix * Select the specific game version when clicking the 'download' button on ModListPage * Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json * Enhance :HMCL:build.gradle.kts * Revert parse_mcmod_data.py * Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError. FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden. * Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage. * Fix checkstyle * Fix * Fix * Fix * Add license for RemoteResourceManager * Remove TODO * Enhance Chinese searching * Support to decode metadata for local quilt mod. * Enhance ModManager * Fix checkstyle * Refactor * Fix * Fix * Refactor DownloadPage * Fix * Revert "Refactor DownloadPage" This reverts commit 953558da77af5a0fe3153e77cdcb9b6affa30ffa. * Refactor DownloadPage * Refactor * Fix * Fix checkstyle * Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class. * Fix * Fix * Fix * Enhance SimpleMultimap * Revert TwoLineListItem * Fix * Code cleanup * Code cleanup * Fix * Code cleanup * Add license for IModMetadataReader * Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage * Fix #2498 * Update README_cn.md * Opti ModMananger * Log a warning message when 'hmcl.update_source.override' is used. * Fix chinese searching * Enhance chinese searching. * Enhance memory usage * Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty. * Cache builtin images. * Enhance FXUtils (Make tooltip installer faster). * Fix * Fix * Fix #2560 * Fix typo * Fix remote image cache. * Fix javadoc * Fix checkstyle * Optimize FXUtils::shutdown * Fix merge * I have no idea on why the sha1 was matched. * Revert "Enhance FXUtils (Make tooltip installer faster)." This reverts commit 0a49eb2c1204e4be7dc0df3084faa59fdf9b0394. * Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland. * Modify dynamic remote resource urls. * Optimize codes with StringUtils.DynamicCommonSubsequence. * Prevent unofficial HMCL to access HMCL Resource Update URL. * Zip the dynamic-remote-resources json by Gradle automatically. * Remove unnecessary getters. --------- Co-authored-by: Burning_TNT --- HMCL/build.gradle.kts | 72 +++- .../mickey/minecraft/skin/fx/SkinCanvas.java | 5 +- .../java/org/jackhuang/hmcl/Launcher.java | 90 ++--- .../java/org/jackhuang/hmcl/Metadata.java | 4 +- .../hmcl/game/HMCLGameRepository.java | 20 +- .../game/LocalizedRemoteModRepository.java | 92 ++++-- .../org/jackhuang/hmcl/ui/Controllers.java | 11 +- .../org/jackhuang/hmcl/ui/CrashWindow.java | 6 +- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 150 ++++++++- .../jackhuang/hmcl/ui/GameCrashWindow.java | 4 +- .../org/jackhuang/hmcl/ui/InstallerItem.java | 3 +- .../java/org/jackhuang/hmcl/ui/LogWindow.java | 4 +- .../org/jackhuang/hmcl/ui/UpgradeDialog.java | 2 +- .../java/org/jackhuang/hmcl/ui/WebStage.java | 4 +- .../hmcl/ui/account/CreateAccountPane.java | 2 +- .../hmcl/ui/construct/TwoLineListItem.java | 11 +- .../ui/decorator/DecoratorController.java | 6 +- .../hmcl/ui/download/DownloadPage.java | 7 +- .../ui/download/ModpackSelectionPage.java | 9 +- .../hmcl/ui/download/VersionsPage.java | 3 +- .../org/jackhuang/hmcl/ui/main/AboutPage.java | 23 +- .../jackhuang/hmcl/ui/main/FeedbackPage.java | 9 +- .../org/jackhuang/hmcl/ui/main/MainPage.java | 8 +- .../org/jackhuang/hmcl/ui/main/RootPage.java | 2 +- .../jackhuang/hmcl/ui/main/SettingsPage.java | 8 +- .../hmcl/ui/versions/DownloadListPage.java | 117 +++++-- .../hmcl/ui/versions/DownloadPage.java | 309 +++++++++++------- .../ui/versions/GameAdvancedListItem.java | 4 +- .../hmcl/ui/versions/ModDownloadListPage.java | 9 + .../hmcl/ui/versions/ModListPage.java | 2 +- .../hmcl/ui/versions/ModListPageSkin.java | 49 +-- .../hmcl/ui/versions/ModTranslations.java | 41 ++- .../ui/versions/ModpackDownloadListPage.java | 9 + .../ResourcePackDownloadListPage.java | 9 + .../hmcl/ui/versions/VersionIconDialog.java | 3 +- .../hmcl/ui/versions/VersionSettingsPage.java | 3 +- .../{ => hmcl}/ExecutableHeaderHelper.java | 2 +- .../upgrade/{ => hmcl}/HMCLDownloadTask.java | 2 +- .../upgrade/{ => hmcl}/IntegrityChecker.java | 2 +- .../upgrade/{ => hmcl}/RemoteVersion.java | 2 +- .../upgrade/{ => hmcl}/UpdateChannel.java | 2 +- .../upgrade/{ => hmcl}/UpdateChecker.java | 4 +- .../upgrade/{ => hmcl}/UpdateHandler.java | 2 +- .../resource/RemoteResourceManager.java | 204 ++++++++++++ .../jackhuang/hmcl/util/CrashReporter.java | 4 +- .../resources/assets/lang/I18N.properties | 16 +- .../resources/assets/lang/I18N_es.properties | 1 - .../resources/assets/lang/I18N_ja.properties | 1 - .../resources/assets/lang/I18N_ru.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 20 +- .../assets/lang/I18N_zh_CN.properties | 18 +- .../hmcl/download/LibraryAnalyzer.java | 36 +- .../jackhuang/hmcl/download/MaintainTask.java | 2 +- .../jackhuang/hmcl/download/VersionList.java | 2 +- .../jackhuang/hmcl/event/EventManager.java | 2 +- .../java/org/jackhuang/hmcl/mod/Datapack.java | 1 + .../org/jackhuang/hmcl/mod/ModLoaderType.java | 22 +- .../org/jackhuang/hmcl/mod/ModManager.java | 86 ++--- .../org/jackhuang/hmcl/mod/RemoteMod.java | 72 +++- .../hmcl/mod/RemoteModRepository.java | 34 +- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 18 +- .../curse/CurseForgeRemoteModRepository.java | 49 ++- .../mod/{ => modinfo}/FabricModMetadata.java | 5 +- .../{ => modinfo}/ForgeNewModMetadata.java | 8 +- .../{ => modinfo}/ForgeOldModMetadata.java | 6 +- .../mod/{ => modinfo}/LiteModMetadata.java | 8 +- .../hmcl/mod/{ => modinfo}/PackMcMeta.java | 6 +- .../hmcl/mod/modinfo/QuiltModMetadata.java | 81 +++++ .../modrinth/ModrinthRemoteModRepository.java | 51 +-- .../jackhuang/hmcl/util/SimpleMultimap.java | 20 +- .../org/jackhuang/hmcl/util/StringUtils.java | 26 ++ .../jackhuang/hmcl/util/io/HttpRequest.java | 2 +- .../org/jackhuang/hmcl/util/io/IOUtils.java | 5 + .../jackhuang/hmcl/util/io/NetworkUtils.java | 8 +- README.md | 28 +- README_cn.md | 28 +- data-json/dynamic-remote-resources-raw.json | 24 ++ data-json/dynamic-remote-resources.json | 1 + 78 files changed, 1538 insertions(+), 484 deletions(-) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/ExecutableHeaderHelper.java (99%) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/HMCLDownloadTask.java (98%) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/IntegrityChecker.java (99%) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/RemoteVersion.java (98%) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/UpdateChannel.java (96%) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/UpdateChecker.java (97%) rename HMCL/src/main/java/org/jackhuang/hmcl/upgrade/{ => hmcl}/UpdateHandler.java (99%) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/resource/RemoteResourceManager.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/mod/{ => modinfo}/FabricModMetadata.java (95%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/mod/{ => modinfo}/ForgeNewModMetadata.java (93%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/mod/{ => modinfo}/ForgeOldModMetadata.java (96%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/mod/{ => modinfo}/LiteModMetadata.java (93%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/mod/{ => modinfo}/PackMcMeta.java (97%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java create mode 100644 data-json/dynamic-remote-resources-raw.json create mode 100644 data-json/dynamic-remote-resources.json diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 508425eb8..19aae7277 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -1,3 +1,6 @@ +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonObject import java.net.URI import java.nio.file.FileSystems import java.nio.file.Files @@ -7,12 +10,23 @@ import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec import java.util.zip.ZipFile +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath("com.google.code.gson:gson:2.10.1") + } +} + plugins { id("com.github.johnrengelman.shadow") version "7.1.2" } val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null - || (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun" && System.getenv("GITHUB_BASE_REF").isNullOrEmpty()) + || (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun" && System.getenv("GITHUB_BASE_REF") + .isNullOrEmpty()) val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number -> val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0 @@ -80,6 +94,62 @@ fun attachSignature(jar: File) { } } +tasks.getByName("compileJava") { + dependsOn(tasks.create("computeDynamicResources") { + this@create.inputs.file(rootProject.rootDir.toPath().resolve("data-json/dynamic-remote-resources-raw.json")) + this@create.outputs.file(rootProject.rootDir.toPath().resolve("data-json/dynamic-remote-resources.json")) + + doLast { + Gson().also { gsonInstance -> + Files.newBufferedReader( + rootProject.rootDir.toPath().resolve("data-json/dynamic-remote-resources-raw.json"), + Charsets.UTF_8 + ).use { br -> + (gsonInstance.fromJson(br, JsonElement::class.java) as JsonObject) + }.also { data -> + data.asMap().forEach { (namespace, namespaceData) -> + (namespaceData as JsonObject).asMap().forEach { (name, nameData) -> + (nameData as JsonObject).asMap().forEach { (version, versionData) -> + require(versionData is JsonObject) + val localPath = + (versionData.get("local_path") as com.google.gson.JsonPrimitive).asString + val sha1 = (versionData.get("sha1") as com.google.gson.JsonPrimitive).asString + + val currentSha1 = digest( + "SHA-1", + Files.readAllBytes(rootProject.rootDir.toPath().resolve(localPath)) + ).joinToString(separator = "") { "%02x".format(it) } + + if (!sha1.equals(currentSha1, ignoreCase = true)) { + throw IllegalStateException("Mismatched SHA-1 in $.${namespace}.${name}.${version} of dynamic remote resources detected. Require ${currentSha1}, but found $sha1") + } + } + } + } + + rootProject.rootDir.toPath().resolve("data-json/dynamic-remote-resources.json").also { zippedPath -> + gsonInstance.toJson(data).also { expectedData -> + if (Files.exists(zippedPath)) { + Files.readString(zippedPath, Charsets.UTF_8).also { rawData -> + if (!rawData.equals(expectedData)) { + if (System.getenv("GITHUB_SHA") == null) { + Files.writeString(zippedPath, expectedData, Charsets.UTF_8) + } else { + throw IllegalStateException("Mismatched zipped dynamic-remote-resources json file!") + } + } + } + } else { + Files.writeString(zippedPath, expectedData, Charsets.UTF_8) + } + } + } + } + } + } + }) +} + val java11 = sourceSets.create("java11") { java { srcDir("src/main/java11") diff --git a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java index 889732c20..a52763570 100644 --- a/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java +++ b/HMCL/src/main/java/moe/mickey/minecraft/skin/fx/SkinCanvas.java @@ -12,12 +12,13 @@ import javafx.scene.transform.Rotate; import javafx.scene.transform.Scale; import javafx.scene.transform.Translate; +import org.jackhuang.hmcl.ui.FXUtils; import org.jetbrains.annotations.Nullable; public class SkinCanvas extends Group { - public static final Image ALEX = new Image("/assets/img/skin/alex.png"); - public static final Image STEVE = new Image("/assets/img/skin/steve.png"); + public static final Image ALEX = FXUtils.newBuiltinImage("/assets/img/skin/alex.png"); + public static final Image STEVE = FXUtils.newBuiltinImage("/assets/img/skin/steve.png"); public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true); public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 717cfa548..14e8edb8d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -33,10 +33,12 @@ import org.jackhuang.hmcl.setting.SambaException; import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.upgrade.UpdateChecker; -import org.jackhuang.hmcl.upgrade.UpdateHandler; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateHandler; +import org.jackhuang.hmcl.upgrade.resource.RemoteResourceManager; import org.jackhuang.hmcl.util.CrashReporter; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.platform.Architecture; @@ -71,42 +73,7 @@ public final class Launcher extends Application { CookieHandler.setDefault(COOKIE_MANAGER); - Skin.registerDefaultSkinLoader((type) -> { - switch (type) { - case ALEX: - return Skin.class.getResourceAsStream("/assets/img/skin/alex.png"); - case ARI: - return Skin.class.getResourceAsStream("/assets/img/skin/ari.png"); - case EFE: - return Skin.class.getResourceAsStream("/assets/img/skin/efe.png"); - case KAI: - return Skin.class.getResourceAsStream("/assets/img/skin/kai.png"); - case MAKENA: - return Skin.class.getResourceAsStream("/assets/img/skin/makena.png"); - case NOOR: - return Skin.class.getResourceAsStream("/assets/img/skin/noor.png"); - case STEVE: - return Skin.class.getResourceAsStream("/assets/img/skin/steve.png"); - case SUNNY: - return Skin.class.getResourceAsStream("/assets/img/skin/sunny.png"); - case ZURI: - return Skin.class.getResourceAsStream("/assets/img/skin/zuri.png"); - default: - return null; - } - }); - - RemoteMod.registerEmptyRemoteMod(new RemoteMod("", "", i18n("mods.broken_dependency.title"), i18n("mods.broken_dependency.desc"), new ArrayList<>(), "", "/assets/img/icon.png", new RemoteMod.IMod() { - @Override - public List loadDependencies(RemoteModRepository modRepository) throws IOException { - throw new IOException(); - } - - @Override - public Stream loadVersions(RemoteModRepository modRepository) throws IOException { - throw new IOException(); - } - })); + register(); LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version")); try { @@ -156,6 +123,10 @@ public final class Launcher extends Application { UpdateChecker.init(); + RemoteResourceManager.init(); + + RemoteResourceManager.register(); + primaryStage.show(); }); } catch (Throwable e) { @@ -163,6 +134,45 @@ public final class Launcher extends Application { } } + private static void register() { + Skin.registerDefaultSkinLoader((type) -> { + switch (type) { + case ALEX: + return Skin.class.getResourceAsStream("/assets/img/skin/alex.png"); + case ARI: + return Skin.class.getResourceAsStream("/assets/img/skin/ari.png"); + case EFE: + return Skin.class.getResourceAsStream("/assets/img/skin/efe.png"); + case KAI: + return Skin.class.getResourceAsStream("/assets/img/skin/kai.png"); + case MAKENA: + return Skin.class.getResourceAsStream("/assets/img/skin/makena.png"); + case NOOR: + return Skin.class.getResourceAsStream("/assets/img/skin/noor.png"); + case STEVE: + return Skin.class.getResourceAsStream("/assets/img/skin/steve.png"); + case SUNNY: + return Skin.class.getResourceAsStream("/assets/img/skin/sunny.png"); + case ZURI: + return Skin.class.getResourceAsStream("/assets/img/skin/zuri.png"); + default: + return null; + } + }); + + RemoteMod.registerEmptyRemoteMod(new RemoteMod("", "", i18n("mods.broken_dependency.title"), i18n("mods.broken_dependency.desc"), new ArrayList<>(), "", "/assets/img/icon@8x.png", new RemoteMod.IMod() { + @Override + public List loadDependencies(RemoteModRepository modRepository) throws IOException { + throw new IOException(); + } + + @Override + public Stream loadVersions(RemoteModRepository modRepository) throws IOException { + throw new IOException(); + } + })); + } + private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) { return new Alert(alertType, contentText, buttons).showAndWait().orElse(null); } @@ -283,6 +293,10 @@ public final class Launcher extends Application { if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE")); + if (System.getProperty("hmcl.update_source.override") != null) { + Logging.LOG.log(Level.WARNING, "'hmcl.update_source.override' is deprecated! Please use 'hmcl.hmcl_update_source.override' instead"); + } + launch(Launcher.class, args); } catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter. CRASH_REPORTER.uncaughtException(Thread.currentThread(), e); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index 8abd681ac..b84ef0b36 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -37,7 +37,9 @@ public final class Metadata { public static final String TITLE = NAME + " " + VERSION; public static final String FULL_TITLE = FULL_NAME + " v" + VERSION; - public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link"); + // hmcl.update_source.override is deprecated. If it is used, a warning message will be printed in org.jackhuang.hmcl.Launcher.main . + public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.hmcl_update_source.override", System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link")); + public static final String RESOURCE_UPDATE_URL = System.getProperty("hmcl.resource_update_source.override", "https://hmcl.huangyuhui.net/api/dynamic_remote_resource/update_link"); public static final String CONTACT_URL = "https://docs.hmcl.net/help.html"; public static final String HELP_URL = "https://docs.hmcl.net"; public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/"; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 7b5aea0b7..2609468b9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -52,7 +52,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; @@ -263,7 +263,7 @@ public class HMCLGameRepository extends DefaultGameRepository { public Image getVersionIconImage(String id) { if (id == null || !isLoaded()) - return newImage("/assets/img/grass.png"); + return newBuiltinImage("/assets/img/grass.png"); VersionSetting vs = getLocalVersionSettingOrCreate(id); VersionIconType iconType = Optional.ofNullable(vs).map(VersionSetting::getVersionIcon).orElse(VersionIconType.DEFAULT); @@ -276,21 +276,21 @@ public class HMCLGameRepository extends DefaultGameRepository { else if (LibraryAnalyzer.isModded(this, version)) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC)) - return newImage("/assets/img/fabric.png"); + return newBuiltinImage("/assets/img/fabric.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE)) - return newImage("/assets/img/forge.png"); + return newBuiltinImage("/assets/img/forge.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT)) - return newImage("/assets/img/quilt.png"); + return newBuiltinImage("/assets/img/quilt.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) - return newImage("/assets/img/command.png"); + return newBuiltinImage("/assets/img/command.png"); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER)) - return newImage("/assets/img/chicken.png"); + return newBuiltinImage("/assets/img/chicken.png"); else - return newImage("/assets/img/furnace.png"); + return newBuiltinImage("/assets/img/furnace.png"); } else - return newImage("/assets/img/grass.png"); + return newBuiltinImage("/assets/img/grass.png"); } else { - return newImage(iconType.getResourceUrl()); + return newBuiltinImage(iconType.getResourceUrl()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java index d90d2a1de..73ccd0e13 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -21,44 +21,86 @@ import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.ui.versions.ModTranslations; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; public abstract class LocalizedRemoteModRepository implements RemoteModRepository { + // Yes, I'm not kidding you. The similarity check is based on these two magic number. :) + private static final int CONTAIN_CHINESE_WEIGHT = 10; + + private static final int INITIAL_CAPACITY = 16; protected abstract RemoteModRepository getBackedRemoteModRepository(); + protected abstract SortType getBackedRemoteModRepositorySortOrder(); + @Override - public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { - String newSearchFilter; - if (StringUtils.containsChinese(searchFilter)) { - ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType()); - List mods = modTranslations.searchMod(searchFilter); - List searchFilters = new ArrayList<>(); - int count = 0; - for (ModTranslations.Mod mod : mods) { - String englishName = mod.getName(); - if (StringUtils.isNotBlank(mod.getSubname())) { - englishName = mod.getSubname(); - } - - searchFilters.add(englishName); - - count++; - if (count >= 3) break; - } - newSearchFilter = String.join(" ", searchFilters); - } else { - newSearchFilter = searchFilter; + public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { + if (!StringUtils.containsChinese(searchFilter)) { + return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder); } - return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder); + Set englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY); + + int count = 0; + for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) { + for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) { + if (englishSearchFiltersSet.contains(englishWord)) { + continue; + } + + englishSearchFiltersSet.add(englishWord); + } + + count++; + if (count >= 3) break; + } + + SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder); + + RemoteMod[] searchResultArray = new RemoteMod[pageSize]; + int chineseIndex = 0, englishIndex = searchResultArray.length - 1; + for (RemoteMod remoteMod : Lang.toIterable(searchResult.getUnsortedResults())) { + if (chineseIndex > englishIndex) { + throw new IOException("There are too many search results!"); + } + + ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug()); + if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) { + searchResultArray[chineseIndex++] = remoteMod; + } else { + searchResultArray[englishIndex--] = remoteMod; + } + } + int totalPages = searchResult.getTotalPages(); + searchResult = null; // Release memory + + StringUtils.DynamicCommonSubsequence calc = new StringUtils.DynamicCommonSubsequence(16, 16); + return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> { + ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug()); + if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) { + return Pair.pair(remoteMod, Integer.MAX_VALUE); + } + + String chineseRemoteModName = chineseRemoteMod.getName(); + if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) { + return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length())); + } + + int weight = calc.calc(searchFilter, chineseRemoteModName); + for (int i = 0;i < searchFilter.length(); i ++) { + if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) { + return Pair.pair(remoteMod, weight + CONTAIN_CHINESE_WEIGHT); + } + } + return Pair.pair(remoteMod, weight); + }).sorted(Comparator.>comparingInt(Pair::getValue).reversed()).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index adfddb7ac..352da31ed 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -51,10 +51,7 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; import org.jackhuang.hmcl.ui.versions.GameListPage; import org.jackhuang.hmcl.ui.versions.VersionPage; -import org.jackhuang.hmcl.util.FutureCallback; -import org.jackhuang.hmcl.util.Lazy; -import org.jackhuang.hmcl.util.Logging; -import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.JavaVersion; @@ -65,7 +62,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import static org.jackhuang.hmcl.setting.ConfigHolder.*; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class Controllers { @@ -204,7 +201,7 @@ public final class Controllers { decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty()); scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily())); - stage.getIcons().add(newImage("/assets/img/icon.png")); + stage.getIcons().add(newBuiltinImage("/assets/img/icon.png")); stage.setTitle(Metadata.FULL_TITLE); stage.initStyle(StageStyle.TRANSPARENT); stage.setScene(scene); @@ -357,5 +354,7 @@ public final class Controllers { stage = null; scene = null; onApplicationStop(); + + FXUtils.shutdown(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java index 0d08bb5be..6761ad986 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java @@ -28,9 +28,9 @@ import javafx.scene.layout.StackPane; import javafx.stage.Stage; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.countly.CrashReport; -import org.jackhuang.hmcl.upgrade.UpdateChecker; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; /** @@ -70,7 +70,7 @@ public class CrashWindow extends Stage { Scene scene = new Scene(pane, 800, 480); setScene(scene); - getIcons().add(newImage("/assets/img/icon.png")); + getIcons().add(newBuiltinImage("/assets/img/icon.png")); setTitle(i18n("message.error")); setOnCloseRequest(e -> System.exit(1)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index d7e03a00a..db475d464 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -31,10 +31,10 @@ import javafx.beans.value.WritableValue; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.*; import javafx.scene.control.ScrollPane; -import javafx.scene.image.*; +import javafx.scene.control.*; import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.input.*; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.Priority; @@ -46,7 +46,11 @@ import javafx.scene.text.TextFlow; import javafx.util.Callback; import javafx.util.Duration; import javafx.util.StringConverter; +import org.glavo.png.PNGType; +import org.glavo.png.PNGWriter; +import org.glavo.png.javafx.PNGJavaFXUtils; import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.construct.JFXHyperlink; import org.jackhuang.hmcl.util.Holder; @@ -74,11 +78,11 @@ import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; @@ -94,7 +98,24 @@ public final class FXUtils { private FXUtils() { } - public static String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace"; + public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace"; + + private static final Map builtinImageCache = new ConcurrentHashMap<>(); + + private static final Map remoteImageCache = new ConcurrentHashMap<>(); + + public static void shutdown() { + for (Map.Entry entry: remoteImageCache.entrySet()) { + try { + Files.deleteIfExists(entry.getValue()); + } catch (IOException e) { + LOG.log(Level.WARNING, String.format("Failed to delete cache file %s.", entry.getValue()), e); + } + remoteImageCache.remove(entry.getKey()); + } + + builtinImageCache.clear(); + } public static void runInFX(Runnable runnable) { if (Platform.isFxApplicationThread()) { @@ -449,7 +470,8 @@ public final class FXUtils { } catch (Throwable e) { LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e); } - } if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) { + } + if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) { for (String browser : linuxBrowsers) { try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) { if (is.read() != -1) { @@ -663,12 +685,109 @@ public final class FXUtils { * @see org.jackhuang.hmcl.util.CrashReporter * @see ResourceNotFoundError */ - public static Image newImage(String url) { - try { - return new Image(url); - } catch (IllegalArgumentException e) { - throw new ResourceNotFoundError("Cannot access image: " + url, e); + public static Image newBuiltinImage(String url) { + return newBuiltinImage(url, 0, 0, false, false); + } + + /** + * Suppress IllegalArgumentException since the url is supposed to be correct definitely. + * + * @param url the url of image. The image resource should be a file within the jar. + * @param requestedWidth the image's bounding box width + * @param requestedHeight the image's bounding box height + * @param preserveRatio indicates whether to preserve the aspect ratio of + * the original image when scaling to fit the image within the + * specified bounding box + * @param smooth indicates whether to use a better quality filtering + * algorithm or a faster one when scaling this image to fit within + * the specified bounding box + * @return the image resource within the jar. + * @see org.jackhuang.hmcl.util.CrashReporter + * @see ResourceNotFoundError + */ + public static Image newBuiltinImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { + return builtinImageCache.computeIfAbsent(url, s -> { + try { + return new Image(s, requestedWidth, requestedHeight, preserveRatio, smooth); + } catch (IllegalArgumentException e) { + throw new ResourceNotFoundError("Cannot access image: " + s, e); + } + }); + } + + /** + * Load image from the internet. It will cache the data of images for the further usage. + * The cached data will be deleted when HMCL is closed or hidden. + * + * @param url the url of image. The image resource should be a file on the internet. + * @return the image resource within the jar. + */ + public static Image newRemoteImage(String url) { + return newRemoteImage(url, 0, 0, false, false, false); + } + + /** + * Load image from the internet. It will cache the data of images for the further usage. + * The cached data will be deleted when HMCL is closed or hidden. + * + * @param url the url of image. The image resource should be a file on the internet. + * @param requestedWidth the image's bounding box width + * @param requestedHeight the image's bounding box height + * @param preserveRatio indicates whether to preserve the aspect ratio of + * the original image when scaling to fit the image within the + * specified bounding box + * @param smooth indicates whether to use a better quality filtering + * algorithm or a faster one when scaling this image to fit within + * the specified bounding box + * @return the image resource within the jar. + */ + public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) { + Path currentPath = remoteImageCache.get(url); + if (currentPath != null) { + if (Files.isReadable(currentPath)) { + try (InputStream inputStream = Files.newInputStream(currentPath)) { + return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth); + } catch (IOException e) { + LOG.log(Level.WARNING, "An exception encountered while reading data from cached image file.", e); + } + } + + // The file is unavailable or unreadable. + remoteImageCache.remove(url); + + try { + Files.deleteIfExists(currentPath); + } catch (IOException e) { + LOG.log(Level.WARNING, "An exception encountered while deleting broken cached image file.", e); + } } + + Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading); + image.progressProperty().addListener((observable, oldValue, newValue) -> { + if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) { + Task.runAsync(() -> { + Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache"); + try ( // Make sure the file is released from JVM before we put the path into remoteImageCache. + OutputStream outputStream = Files.newOutputStream(newPath); + PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL) + ) { + writer.write(PNGJavaFXUtils.asArgbImage(image)); + } catch (IOException e) { + try { + Files.delete(newPath); + } catch (IOException e2) { + e2.addSuppressed(e); + throw e2; + } + throw e; + } + if (remoteImageCache.putIfAbsent(url, newPath) != null) { + Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder. + } + }).start(); + } + }); + return image; } public static JFXButton newRaisedButton(String text) { @@ -678,6 +797,13 @@ public final class FXUtils { return button; } + public static JFXButton newBorderButton(String text) { + JFXButton button = new JFXButton(text); + button.getStyleClass().add("jfx-button-border"); + button.setButtonType(JFXButton.ButtonType.RAISED); + return button; + } + public static void applyDragListener(Node node, FileFilter filter, Consumer> callback) { applyDragListener(node, filter, callback, null); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index ac53e69b4..e100eef2c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -66,7 +66,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; @@ -116,7 +116,7 @@ public class GameCrashWindow extends Stage { setScene(new Scene(view, 800, 480)); getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily())); setTitle(i18n("game.crash.title")); - getIcons().add(newImage("/assets/img/icon.png")); + getIcons().add(newBuiltinImage("/assets/img/icon.png")); analyzeCrashReport(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java index a8ff44826..c94b5c085 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java @@ -30,7 +30,6 @@ import javafx.scene.control.Control; import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.*; @@ -207,7 +206,7 @@ public class InstallerItem extends Control { pane.pseudoClassStateChanged(CARD, control.style == Style.CARD); if (control.imageUrl != null) { - ImageView view = new ImageView(new Image(control.imageUrl)); + ImageView view = new ImageView(FXUtils.newRemoteImage(control.imageUrl)); Node node = FXUtils.limitingSize(view, 32, 32); node.setMouseTransparent(true); node.getStyleClass().add("installer-item-image"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index 523b46dd4..4af43cac8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -57,7 +57,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.util.Lang.thread; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence; @@ -94,7 +94,7 @@ public final class LogWindow extends Stage { setScene(new Scene(impl, 800, 480)); getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily())); setTitle(i18n("logwindow.title")); - getIcons().add(newImage("/assets/img/icon.png")); + getIcons().add(newBuiltinImage("/assets/img/icon.png")); levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs())); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java index b43133a1c..733e393da 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java @@ -25,7 +25,7 @@ import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.upgrade.RemoteVersion; +import org.jackhuang.hmcl.upgrade.hmcl.RemoteVersion; import java.util.logging.Level; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebStage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebStage.java index 056266385..32a413407 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebStage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/WebStage.java @@ -29,7 +29,7 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.setting.Theme; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; public class WebStage extends Stage { protected final StackPane pane = new StackPane(); @@ -44,7 +44,7 @@ public class WebStage extends Stage { public WebStage(int width, int height) { setScene(new Scene(pane, width, height)); getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily())); - getIcons().add(newImage("/assets/img/icon.png")); + getIcons().add(newBuiltinImage("/assets/img/icon.png")); webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile()); webView.setContextMenuEnabled(false); progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java index 06c6763a0..dfa578cc2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java @@ -58,7 +58,7 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.upgrade.IntegrityChecker; +import org.jackhuang.hmcl.upgrade.hmcl.IntegrityChecker; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.javafx.BindingMapping; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java index 51c98e159..c0f6929cd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java @@ -26,6 +26,7 @@ import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.AggregatedObservableList; @@ -38,9 +39,6 @@ public class TwoLineListItem extends VBox { private final ObservableList tags = FXCollections.observableArrayList(); private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle"); - private final ObservableList tagLabels; - private final AggregatedObservableList firstLineChildren; - public TwoLineListItem(String titleString, String subtitleString) { this(); @@ -58,14 +56,14 @@ public class TwoLineListItem extends VBox { lblTitle.getStyleClass().add("title"); lblTitle.textProperty().bind(title); - tagLabels = MappedObservableList.create(tags, tag -> { + ObservableList tagLabels = MappedObservableList.create(tags, tag -> { Label tagLabel = new Label(); tagLabel.getStyleClass().add("tag"); tagLabel.setText(tag); HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0)); return tagLabel; }); - firstLineChildren = new AggregatedObservableList<>(); + AggregatedObservableList firstLineChildren = new AggregatedObservableList<>(); firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle)); firstLineChildren.appendList(tagLabels); Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList()); @@ -85,6 +83,9 @@ public class TwoLineListItem extends VBox { }); getStyleClass().add(DEFAULT_STYLE_CLASS); + + this.minWidthProperty().set(0); + HBox.setHgrow(this, Priority.SOMETIMES); } public String getTitle() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 14dba47ba..6c02002a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -64,7 +64,7 @@ import java.util.stream.Stream; import static java.util.logging.Level.WARNING; import static java.util.stream.Collectors.toList; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.io.FileUtils.getExtension; @@ -172,7 +172,7 @@ public class DecoratorController { image = tryLoadImage(backgroundImageUrl).orElse(null); break; case CLASSIC: - image = newImage("/assets/img/background-classic.jpg"); + image = newBuiltinImage("/assets/img/background-classic.jpg"); break; case TRANSLUCENT: return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY)); @@ -202,7 +202,7 @@ public class DecoratorController { return image.orElseGet(() -> { if (defaultBackground == null) - defaultBackground = newImage("/assets/img/background.jpg"); + defaultBackground = newBuiltinImage("/assets/img/background.jpg"); return defaultBackground; }); } 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 09e0a66d3..6ad979833 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 @@ -232,8 +232,13 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage tab.select(newGameTab); } - public void showModDownloads() { + public void showModpackDownloads() { + tab.select(modpackTab); + } + + public DownloadListPage showModDownloads() { tab.select(modTab); + return modTab.getNode(); } public void showWorldDownloads() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index 8687119a8..b03315f3f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -69,7 +69,8 @@ public final class ModpackSelectionPage extends VBox implements WizardPage { this.getChildren().setAll( title, createButton("local", this::onChooseLocalFile), - createButton("remote", this::onChooseRemoteFile) + createButton("remote", this::onChooseRemoteFile), + createButton("repository", this::onChooseRepository) ); Optional filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class); @@ -168,6 +169,12 @@ public final class ModpackSelectionPage extends VBox implements WizardPage { }); } + public void onChooseRepository() { + DownloadPage downloadPage = new DownloadPage(); + downloadPage.showModpackDownloads(); + Controllers.navigate(downloadPage); + } + @Override public void cleanup(Map settings) { } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index f4b08d39e..9bee8d246 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -41,6 +41,7 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion; import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; @@ -286,7 +287,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres } private Image getIcon(VersionIconType type) { - return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl())); + return icons.computeIfAbsent(type, iconType -> FXUtils.newBuiltinImage(iconType.getResourceUrl())); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/AboutPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/AboutPage.java index 158baa165..12d032f16 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/AboutPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/AboutPage.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main; import javafx.geometry.Insets; import javafx.scene.control.ScrollPane; -import javafx.scene.image.Image; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.Metadata; @@ -35,13 +34,13 @@ public class AboutPage extends StackPane { ComponentList about = new ComponentList(); { IconedTwoLineListItem launcher = new IconedTwoLineListItem(); - launcher.setImage(new Image("/assets/img/craft_table.png")); + launcher.setImage(FXUtils.newBuiltinImage("/assets/img/craft_table.png")); launcher.setTitle("Hello Minecraft! Launcher"); launcher.setSubtitle(Metadata.VERSION); launcher.setExternalLink("https://hmcl.huangyuhui.net"); IconedTwoLineListItem author = new IconedTwoLineListItem(); - author.setImage(new Image("/assets/img/yellow_fish.png")); + author.setImage(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png")); author.setTitle("huanghongxun"); author.setSubtitle(i18n("about.author.statement")); author.setExternalLink("https://space.bilibili.com/1445341"); @@ -52,54 +51,54 @@ public class AboutPage extends StackPane { ComponentList thanks = new ComponentList(); { IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem(); - yushijinhun.setImage(new Image("/assets/img/yushijinhun.png")); + yushijinhun.setImage(FXUtils.newBuiltinImage("/assets/img/yushijinhun.png")); yushijinhun.setTitle("yushijinhun"); yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement")); yushijinhun.setExternalLink("https://yushi.moe/"); IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem(); - bangbang93.setImage(new Image("/assets/img/bangbang93.png")); + bangbang93.setImage(FXUtils.newBuiltinImage("/assets/img/bangbang93.png")); bangbang93.setTitle("bangbang93"); bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement")); bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/"); IconedTwoLineListItem glavo = new IconedTwoLineListItem(); - glavo.setImage(new Image("/assets/img/glavo.png")); + glavo.setImage(FXUtils.newBuiltinImage("/assets/img/glavo.png")); glavo.setTitle("Glavo"); glavo.setSubtitle(i18n("about.thanks_to.glavo.statement")); glavo.setExternalLink("https://github.com/Glavo"); IconedTwoLineListItem gamerteam = new IconedTwoLineListItem(); gamerteam.setTitle("gamerteam"); - gamerteam.setImage(new Image("/assets/img/gamerteam.png")); + gamerteam.setImage(FXUtils.newBuiltinImage("/assets/img/gamerteam.png")); gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement")); gamerteam.setExternalLink("http://www.zhaisoul.com/"); IconedTwoLineListItem redLnn = new IconedTwoLineListItem(); redLnn.setTitle("Red_lnn"); - redLnn.setImage(new Image("/assets/img/red_lnn.png")); + redLnn.setImage(FXUtils.newBuiltinImage("/assets/img/red_lnn.png")); redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement")); IconedTwoLineListItem mcbbs = new IconedTwoLineListItem(); - mcbbs.setImage(new Image("/assets/img/chest.png")); + mcbbs.setImage(FXUtils.newBuiltinImage("/assets/img/chest.png")); mcbbs.setTitle(i18n("about.thanks_to.mcbbs")); mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement")); mcbbs.setExternalLink("https://www.mcbbs.net/"); IconedTwoLineListItem mcmod = new IconedTwoLineListItem(); - mcmod.setImage(new Image("/assets/img/mcmod.png")); + mcmod.setImage(FXUtils.newBuiltinImage("/assets/img/mcmod.png")); mcmod.setTitle(i18n("about.thanks_to.mcmod")); mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement")); mcmod.setExternalLink("https://www.mcmod.cn/"); IconedTwoLineListItem contributors = new IconedTwoLineListItem(); - contributors.setImage(new Image("/assets/img/github.png")); + contributors.setImage(FXUtils.newBuiltinImage("/assets/img/github.png")); contributors.setTitle(i18n("about.thanks_to.contributors")); contributors.setSubtitle(i18n("about.thanks_to.contributors.statement")); contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors"); IconedTwoLineListItem users = new IconedTwoLineListItem(); - users.setImage(new Image("/assets/img/craft_table.png")); + users.setImage(FXUtils.newBuiltinImage("/assets/img/craft_table.png")); users.setTitle(i18n("about.thanks_to.users")); users.setSubtitle(i18n("about.thanks_to.users.statement")); users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java index 58972ed3f..36353e050 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main; import javafx.geometry.Insets; import javafx.scene.control.ScrollPane; -import javafx.scene.image.Image; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.ComponentList; @@ -43,25 +42,25 @@ public class FeedbackPage extends SpinnerPane { ComponentList community = new ComponentList(); { IconedTwoLineListItem users = new IconedTwoLineListItem(); - users.setImage(new Image("/assets/img/craft_table.png")); + users.setImage(FXUtils.newBuiltinImage("/assets/img/craft_table.png")); users.setTitle(i18n("feedback.qq_group")); users.setSubtitle(i18n("feedback.qq_group.statement")); users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor"); IconedTwoLineListItem github = new IconedTwoLineListItem(); - github.setImage(new Image("/assets/img/github.png")); + github.setImage(FXUtils.newBuiltinImage("/assets/img/github.png")); github.setTitle(i18n("feedback.github")); github.setSubtitle(i18n("feedback.github.statement")); github.setExternalLink("https://github.com/huanghongxun/HMCL/issues/new/choose"); IconedTwoLineListItem discord = new IconedTwoLineListItem(); - discord.setImage(new Image("/assets/img/discord.png")); + discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png")); discord.setTitle(i18n("feedback.discord")); discord.setSubtitle(i18n("feedback.discord.statement")); discord.setExternalLink("https://discord.gg/jVvC7HfM6U"); IconedTwoLineListItem kookapp = new IconedTwoLineListItem(); - kookapp.setImage(new Image("/assets/img/kookapp.png")); + kookapp.setImage(FXUtils.newBuiltinImage("/assets/img/kookapp.png")); kookapp.setTitle(i18n("feedback.kookapp")); kookapp.setSubtitle(i18n("feedback.kookapp.statement")); kookapp.setExternalLink("https://kook.top/Kx7n3t"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index ec1baa0c9..d88550fb0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -54,9 +54,9 @@ import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.versions.GameItem; import org.jackhuang.hmcl.ui.versions.Versions; -import org.jackhuang.hmcl.upgrade.RemoteVersion; -import org.jackhuang.hmcl.upgrade.UpdateChecker; -import org.jackhuang.hmcl.upgrade.UpdateHandler; +import org.jackhuang.hmcl.upgrade.hmcl.RemoteVersion; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateHandler; import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.javafx.MappedObservableList; import org.jackhuang.hmcl.util.platform.JavaVersion; @@ -105,7 +105,7 @@ public final class MainPage extends StackPane implements DecoratorPage { } catch (IOException ignored) { } } else { - titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false)); + titleIcon.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png", 20, 20, false, false)); } Label titleLabel = new Label(Metadata.FULL_TITLE); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 44bd1b692..e4b4b5cfa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -41,7 +41,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; import org.jackhuang.hmcl.ui.versions.Versions; -import org.jackhuang.hmcl.upgrade.UpdateChecker; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.versioning.VersionNumber; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index b53470944..a76070c1c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -27,10 +27,10 @@ import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; -import org.jackhuang.hmcl.upgrade.RemoteVersion; -import org.jackhuang.hmcl.upgrade.UpdateChannel; -import org.jackhuang.hmcl.upgrade.UpdateChecker; -import org.jackhuang.hmcl.upgrade.UpdateHandler; +import org.jackhuang.hmcl.upgrade.hmcl.RemoteVersion; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChannel; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateHandler; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.io.FileUtils; 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 07d21e1ac..17ad04b11 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 @@ -35,7 +35,6 @@ import javafx.scene.control.Control; import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import org.jackhuang.hmcl.game.GameVersion; @@ -77,6 +76,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private final BooleanProperty failed = new SimpleBooleanProperty(false); private final boolean versionSelection; private final ObjectProperty version = new SimpleObjectProperty<>(); + private final IntegerProperty pageOffset = new SimpleIntegerProperty(0); + private final IntegerProperty pageCount = new SimpleIntegerProperty(-1); private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); private final ObservableList versions = FXCollections.observableArrayList(); private final StringProperty selectedVersion = new SimpleStringProperty(); @@ -154,6 +155,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP this.loading.set(loading); } + public void selectVersion(String versionID) { + FXUtils.runInFX(() -> selectedVersion.set(versionID)); + } + public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, RemoteModRepository.SortType sort) { retrySearch = null; setLoading(true); @@ -178,10 +183,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP }).whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); if (exception == null) { - items.setAll(result.collect(Collectors.toList())); + items.setAll(result.getResults().collect(Collectors.toList())); + pageCount.set(result.getTotalPages()); failed.set(false); } else { failed.set(true); + pageCount.set(-1); retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort); } }).executor(true); @@ -221,8 +228,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } private static class ModDownloadListPageSkin extends SkinBase { - private final AggregatedObservableList actions = new AggregatedObservableList<>(); - protected ModDownloadListPageSkin(DownloadListPage control) { super(control); @@ -341,25 +346,90 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP sortComboBox.getSelectionModel().select(0); searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane); - JFXButton searchButton = FXUtils.newRaisedButton(i18n("search")); - ObservableList last = FXCollections.observableArrayList(searchButton); - HBox searchBox = new HBox(8); - actions.appendList(control.actions); - actions.appendList(last); - Bindings.bindContent(searchBox.getChildren(), actions.getAggregatedList()); - GridPane.setColumnSpan(searchBox, 4); - searchBox.setAlignment(Pos.CENTER_RIGHT); - searchPane.addRow(rowIndex++, searchBox); + StringProperty previousSearchFilter = new SimpleStringProperty(this, "Previous Seach Filter", ""); + EventHandler searchAction = e -> { + if (!previousSearchFilter.get().equals(nameField.getText())) { + control.pageOffset.set(0); + } + + previousSearchFilter.set(nameField.getText()); + getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(), + Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem()) + .map(CategoryIndented::getCategory) + .orElse(null), + control.pageOffset.get(), + nameField.getText(), + sortComboBox.getSelectionModel().getSelectedItem()); + }; + + HBox actionsBox = new HBox(8); + GridPane.setColumnSpan(actionsBox, 4); + actionsBox.setAlignment(Pos.CENTER); + { + AggregatedObservableList actions = new AggregatedObservableList<>(); + + JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page")); + firstPageButton.setOnAction(event -> { + control.pageOffset.set(0); + searchAction.handle(event); + }); + firstPageButton.setDisable(true); + control.pageCount.addListener((observable, oldValue, newValue) -> firstPageButton.setDisable(control.pageCount.get() == -1)); + + JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page")); + previousPageButton.setOnAction(event -> { + if (control.pageOffset.get() > 0) { + control.pageOffset.set(control.pageOffset.get() - 1); + searchAction.handle(event); + } + }); + previousPageButton.setDisable(true); + control.pageOffset.addListener((observable, oldValue, newValue) -> previousPageButton.setDisable( + control.pageCount.get() == -1 || control.pageOffset.get() == 0 + )); + + Label pageOffset = new Label(i18n("search.page_n", 0, "-")); + control.pageOffset.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n( + "search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString() + ))); + control.pageCount.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n( + "search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString() + ))); + + JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page")); + nextPageButton.setOnAction(event -> { + control.pageOffset.set(control.pageOffset.get() + 1); + searchAction.handle(event); + }); + nextPageButton.setDisable(true); + control.pageOffset.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable( + control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1 + )); + control.pageCount.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable( + control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1 + )); + + JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page")); + lastPageButton.setOnAction(event -> { + control.pageOffset.set(control.pageCount.get() - 1); + searchAction.handle(event); + }); + lastPageButton.setDisable(true); + control.pageCount.addListener((observable, oldValue, newValue) -> lastPageButton.setDisable(control.pageCount.get() == -1)); + + Pane placeholder = new Pane(); + HBox.setHgrow(placeholder, Priority.SOMETIMES); + + JFXButton searchButton = FXUtils.newRaisedButton(i18n("search")); + searchButton.setOnAction(searchAction); + + actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageOffset, nextPageButton, lastPageButton, placeholder, searchButton)); + actions.appendList(control.actions); + Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList()); + } + + searchPane.addRow(rowIndex++, actionsBox); - EventHandler searchAction = e -> getSkinnable() - .search(gameVersionField.getSelectionModel().getSelectedItem(), - Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem()) - .map(CategoryIndented::getCategory) - .orElse(null), - 0, - nameField.getText(), - sortComboBox.getSelectionModel().getSelectedItem()); - searchButton.setOnAction(searchAction); nameField.setOnAction(searchAction); gameVersionField.setOnAction(searchAction); categoryComboBox.setOnAction(searchAction); @@ -402,6 +472,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP pane.getChildren().add(container); container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content); + HBox.setHgrow(content, Priority.ALWAYS); } @Override @@ -415,7 +486,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP .collect(Collectors.toList())); if (StringUtils.isNotBlank(dataItem.getIconUrl())) { - imageView.setImage(new Image(dataItem.getIconUrl(), 40, 40, true, true, true)); + imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true)); } } }); 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 f506fd8b2..d968920b9 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 @@ -18,8 +18,8 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; @@ -27,14 +27,12 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Control; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; -import javafx.scene.image.Image; +import javafx.scene.control.*; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.stage.FileChooser; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.RemoteMod; @@ -49,11 +47,10 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.util.SimpleMultimap; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jetbrains.annotations.Nullable; @@ -65,8 +62,8 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); @@ -81,8 +78,7 @@ public class DownloadPage extends Control implements DecoratorPage { private final DownloadCallback callback; private final DownloadListPage page; - private List dependencies; - private SimpleMultimap versions; + private SimpleMultimap> versions; public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { this.page = page; @@ -105,10 +101,8 @@ public class DownloadPage extends Control implements DecoratorPage { setLoading(true); setFailed(false); - Task.allOf( - Task.supplyAsync(() -> addon.getData().loadDependencies(repository)), - Task.supplyAsync(() -> { - Stream versions = addon.getData().loadVersions(repository); + Task.supplyAsync(() -> { + Stream versions = addon.getData().loadVersions(repository); // if (StringUtils.isNotBlank(version.getVersion())) { // Optional gameVersion = GameVersion.minecraftVersion(versionJar); // if (gameVersion.isPresent()) { @@ -116,30 +110,23 @@ public class DownloadPage extends Control implements DecoratorPage { // .filter(file -> file.getGameVersions().contains(gameVersion.get()))); // } // } - return sortVersions(versions); - })) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - @SuppressWarnings("unchecked") - List dependencies = (List) result.get(0); - @SuppressWarnings("unchecked") - SimpleMultimap versions = (SimpleMultimap) result.get(1); + return sortVersions(versions); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + this.versions = result; - this.dependencies = dependencies; - this.versions = versions; - - loaded.set(true); - setFailed(false); - } else { - setFailed(true); - } - setLoading(false); - }).start(); + loaded.set(true); + setFailed(false); + } else { + setFailed(true); + } + setLoading(false); + }).start(); } - private SimpleMultimap sortVersions(Stream versions) { - SimpleMultimap classifiedVersions - = new SimpleMultimap(HashMap::new, ArrayList::new); + private SimpleMultimap> sortVersions(Stream versions) { + SimpleMultimap> classifiedVersions + = new SimpleMultimap<>(HashMap::new, ArrayList::new); versions.forEach(version -> { for (String gameVersion : version.getGameVersions()) { classifiedVersions.put(gameVersion, version); @@ -147,7 +134,7 @@ public class DownloadPage extends Control implements DecoratorPage { }); for (String gameVersion : classifiedVersions.keys()) { - List versionList = (List) classifiedVersions.get(gameVersion); + List versionList = classifiedVersions.get(gameVersion); versionList.sort(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()); } return classifiedVersions; @@ -246,7 +233,7 @@ public class DownloadPage extends Control implements DecoratorPage { { ImageView imageView = new ImageView(); if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) { - imageView.setImage(new Image(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true)); + imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true)); } descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40)); @@ -264,12 +251,14 @@ public class DownloadPage extends Control implements DecoratorPage { JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod")); openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod)); descriptionPane.getChildren().add(openMcmodButton); + openMcmodButton.setMinWidth(Region.USE_PREF_SIZE); runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod"))); if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) { JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs")); openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs())); descriptionPane.getChildren().add(openMcbbsButton); + openMcbbsButton.setMinWidth(Region.USE_PREF_SIZE); runInFX(() -> FXUtils.installFastTooltip(openMcbbsButton, i18n("mods.mcbbs"))); } } @@ -277,32 +266,10 @@ public class DownloadPage extends Control implements DecoratorPage { JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage()); openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl()); descriptionPane.getChildren().add(openUrlButton); + openUrlButton.setMinWidth(Region.USE_PREF_SIZE); runInFX(() -> FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage())); } - { - ComponentList dependencyPane = new ComponentList(); - dependencyPane.getStyleClass().add("no-padding"); - - FXUtils.onChangeAndOperate(control.loaded, loaded -> { - if (loaded) { - dependencyPane.getContent().setAll(control.dependencies.stream() - .map(dependency -> new DependencyModItem(getSkinnable().page, dependency, control.version, control.callback)) - .collect(Collectors.toList())); - } - }); - - Node title = ComponentList.createComponentListTitle(i18n("mods.dependencies")); - - BooleanBinding show = Bindings.createBooleanBinding(() -> control.loaded.get() && !control.dependencies.isEmpty(), control.loaded); - title.managedProperty().bind(show); - title.visibleProperty().bind(show); - dependencyPane.managedProperty().bind(show); - dependencyPane.visibleProperty().bind(show); - - pane.getChildren().addAll(title, dependencyPane); - } - SpinnerPane spinnerPane = new SpinnerPane(); VBox.setVgrow(spinnerPane, Priority.ALWAYS); pane.getChildren().add(spinnerPane); @@ -324,6 +291,23 @@ public class DownloadPage extends Control implements DecoratorPage { FXUtils.onChangeAndOperate(control.loaded, loaded -> { if (control.versions == null) return; + if (control.version.getProfile() != null && control.version.getVersion() != null) { + Version game = control.version.getProfile().getRepository().getResolvedPreservingPatchesVersion(control.version.getVersion()); + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(game); + libraryAnalyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT).ifPresent(currentGameVersion -> { + Set currentGameModLoaders = libraryAnalyzer.getModLoaders(); + if (control.versions.containsKey(currentGameVersion)) { + control.versions.get(currentGameVersion).stream() + .filter(version1 -> version1.getLoaders().isEmpty() || version1.getLoaders().stream().anyMatch(currentGameModLoaders::contains)) + .findFirst() + .ifPresent(value -> list.getContent().addAll( + ComponentList.createComponentListTitle(i18n("mods.download.recommend", currentGameVersion)), + new ModItem(value, control) + )); + } + }); + } + for (String gameVersion : control.versions.keys().stream() .sorted(VersionNumber.VERSION_COMPARATOR.reversed()) .collect(Collectors.toList())) { @@ -332,7 +316,7 @@ public class DownloadPage extends Control implements DecoratorPage { .map(version -> new ModItem(version, control)) .collect(Collectors.toList())); sublist.getStyleClass().add("no-padding"); - sublist.setTitle(gameVersion); + sublist.setTitle("Minecraft " + gameVersion); list.getContent().add(sublist); } @@ -344,10 +328,19 @@ public class DownloadPage extends Control implements DecoratorPage { } private static final class DependencyModItem extends StackPane { + public static final EnumMap I18N_KEY = new EnumMap<>(Lang.mapOf( + Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"), + Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"), + Pair.pair(RemoteMod.DependencyType.REQUIRED, "mods.dependency.required"), + Pair.pair(RemoteMod.DependencyType.TOOL, "mods.dependency.tool"), + Pair.pair(RemoteMod.DependencyType.INCLUDE, "mods.dependency.include"), + Pair.pair(RemoteMod.DependencyType.INCOMPATIBLE, "mods.dependency.incompatible"), + Pair.pair(RemoteMod.DependencyType.BROKEN, "mods.dependency.broken") + )); DependencyModItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, DownloadCallback callback) { HBox pane = new HBox(8); - pane.setPadding(new Insets(8)); + pane.setPadding(new Insets(0, 8, 0, 8)); pane.setAlignment(Pos.CENTER_LEFT); TwoLineListItem content = new TwoLineListItem(); HBox.setHgrow(content, Priority.ALWAYS); @@ -366,7 +359,7 @@ public class DownloadPage extends Control implements DecoratorPage { .collect(Collectors.toList())); if (StringUtils.isNotBlank(addon.getIconUrl())) { - imageView.setImage(new Image(addon.getIconUrl(), 40, 40, true, true, true)); + imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true)); } } } @@ -374,64 +367,152 @@ public class DownloadPage extends Control implements DecoratorPage { private static final class ModItem extends StackPane { ModItem(RemoteMod.Version dataItem, DownloadPage selfPage) { - HBox pane = new HBox(8); - pane.setPadding(new Insets(8)); - pane.setAlignment(Pos.CENTER_LEFT); - TwoLineListItem content = new TwoLineListItem(); - StackPane graphicPane = new StackPane(); - JFXButton saveAsButton = new JFXButton(); + VBox pane = new VBox(8); + pane.setPadding(new Insets(8, 0, 8, 0)); + + { + HBox descPane = new HBox(8); + descPane.setPadding(new Insets(0, 8, 0, 8)); + descPane.setAlignment(Pos.CENTER_LEFT); + + { + StackPane graphicPane = new StackPane(); + graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); + + TwoLineListItem content = new TwoLineListItem(); + HBox.setHgrow(content, Priority.ALWAYS); + content.setTitle(dataItem.getName()); + content.setSubtitle(FORMATTER.format(dataItem.getDatePublished().toInstant())); + + switch (dataItem.getVersionType()) { + case Alpha: + case Beta: + content.getTags().add(i18n("version.game.snapshot")); + break; + case Release: + content.getTags().add(i18n("version.game.release")); + break; + } + + for (ModLoaderType modLoaderType : dataItem.getLoaders()) { + switch (modLoaderType) { + case FORGE: + content.getTags().add(i18n("install.installer.forge")); + break; + case FABRIC: + content.getTags().add(i18n("install.installer.fabric")); + break; + case LITE_LOADER: + content.getTags().add(i18n("install.installer.liteloader")); + break; + case QUILT: + content.getTags().add(i18n("install.installer.quilt")); + break; + } + } + + descPane.getChildren().setAll(graphicPane, content); + } + + pane.getChildren().add(descPane); + } RipplerContainer container = new RipplerContainer(pane); - container.setOnMouseClicked(e -> selfPage.download(dataItem)); + container.setOnMouseClicked(e -> Controllers.dialog(new ModVersion(dataItem, selfPage))); getChildren().setAll(container); - saveAsButton.getStyleClass().add("toggle-icon4"); - saveAsButton.setGraphic(SVG.CONTENT_SAVE_MOVE_OUTLINE.createIcon(Theme.blackFill(), -1, -1)); - - HBox.setHgrow(content, Priority.ALWAYS); - pane.getChildren().setAll(graphicPane, content, saveAsButton); - - content.setTitle(dataItem.getName()); - content.setSubtitle(FORMATTER.format(dataItem.getDatePublished().toInstant())); - saveAsButton.setOnAction(e -> selfPage.saveAs(dataItem)); - - switch (dataItem.getVersionType()) { - case Release: - graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); - content.getTags().add(i18n("version.game.release")); - break; - case Beta: - graphicPane.getChildren().setAll(SVG.BETA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); - content.getTags().add(i18n("version.game.snapshot")); - break; - case Alpha: - graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); - content.getTags().add(i18n("version.game.snapshot")); - break; - } - - for (ModLoaderType modLoaderType : dataItem.getLoaders()) { - switch (modLoaderType) { - case FORGE: - content.getTags().add(i18n("install.installer.forge")); - break; - case FABRIC: - content.getTags().add(i18n("install.installer.fabric")); - break; - case LITE_LOADER: - content.getTags().add(i18n("install.installer.liteloader")); - break; - case QUILT: - content.getTags().add(i18n("install.installer.quilt")); - break; - } - } - // Workaround for https://github.com/huanghongxun/HMCL/issues/2129 this.setMinHeight(50); } } + private static final class ModVersion extends JFXDialogLayout { + public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { + this.setHeading(new HBox(new Label(i18n("mods.download.title", version.getName())))); + + VBox box = new VBox(8); + box.setPadding(new Insets(8)); + ModItem modItem = new ModItem(version, selfPage); + modItem.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent())); + box.getChildren().setAll(modItem); + SpinnerPane spinnerPane = new SpinnerPane(); + ScrollPane scrollPane = new ScrollPane(); + ComponentList dependenciesList = new ComponentList(Lang::immutableListOf); + loadDependencies(version, selfPage, spinnerPane, dependenciesList); + spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList)); + + scrollPane.setContent(dependenciesList); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + spinnerPane.setContent(scrollPane); + box.getChildren().add(spinnerPane); + VBox.setVgrow(spinnerPane, Priority.SOMETIMES); + + this.setBody(box); + + JFXButton downloadButton = new JFXButton(i18n("download")); + downloadButton.getStyleClass().add("dialog-accept"); + downloadButton.setOnAction(e -> { + if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { + fireEvent(new DialogCloseEvent()); + } + selfPage.download(version); + }); + + JFXButton saveAsButton = new JFXButton(i18n("button.save_as")); + saveAsButton.getStyleClass().add("dialog-accept"); + saveAsButton.setOnAction(e -> { + if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { + fireEvent(new DialogCloseEvent()); + } + selfPage.saveAs(version); + }); + + JFXButton cancelButton = new JFXButton(i18n("button.cancel")); + cancelButton.getStyleClass().add("dialog-cancel"); + cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + + this.setActions(downloadButton, saveAsButton, cancelButton); + + this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7)); + this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7)); + } + + private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) { + spinnerPane.setLoading(true); + Task.supplyAsync(() -> { + EnumMap> dependencies = new EnumMap<>(RemoteMod.DependencyType.class); + for (RemoteMod.Dependency dependency : version.getDependencies()) { + if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) { + continue; + } + + if (!dependencies.containsKey(dependency.getType())) { + List list = new ArrayList<>(); + Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType()))); + title.setPadding(new Insets(0, 8, 0, 8)); + list.add(title); + dependencies.put(dependency.getType(), list); + } + DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback); + dependencyModItem.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent())); + dependencies.get(dependency.getType()).add(dependencyModItem); + } + + return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + spinnerPane.setLoading(false); + if (exception == null) { + dependenciesList.getContent().setAll(result); + spinnerPane.setFailedReason(null); + } else { + dependenciesList.getContent().setAll(); + spinnerPane.setFailedReason(i18n("download.failed.refresh")); + } + }).start(); + } + } + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault()); public interface DownloadCallback { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java index 3b07ecda6..d6a71f526 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java @@ -30,7 +30,7 @@ import org.jackhuang.hmcl.util.Pair; import java.util.function.Consumer; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; +import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameAdvancedListItem extends AdvancedListItem { @@ -72,7 +72,7 @@ public class GameAdvancedListItem extends AdvancedListItem { Tooltip.uninstall(this,tooltip); setTitle(i18n("version.empty")); setSubtitle(i18n("version.empty.add")); - imageView.setImage(newImage("/assets/img/grass.png")); + imageView.setImage(newBuiltinImage("/assets/img/grass.png")); tooltip.setText(""); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java index a86291fd8..5deea4029 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java @@ -49,6 +49,15 @@ public class ModDownloadListPage extends DownloadListPage { } } + @Override + protected SortType getBackedRemoteModRepositorySortOrder() { + if ("mods.modrinth".equals(downloadSource.get())) { + return SortType.NAME; + } else { + return SortType.POPULARITY; + } + } + @Override public Type getType() { return Type.MOD; 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 892e9238c..3a5eb77ce 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 @@ -210,7 +210,7 @@ public final class ModListPage extends ListPageBase { if (stream != null) { imageView.setImage(new Image(stream, 40, 40, true, true)); } else { - imageView.setImage(new Image("/assets/img/command.png", 40, 40, true, true)); + imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true)); } }).start(); } @@ -326,26 +321,32 @@ class ModListPageSkin extends SkinBase { setBody(description); if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) { - Lang.>>>immutableListOf( - pair("mods.curseforge", pair( - CurseForgeRemoteModRepository.MODS, - (remoteVersion) -> Integer.toString(((CurseAddon.LatestFile) remoteVersion.getSelf()).getModId()) - )), - pair("mods.modrinth", pair( - ModrinthRemoteModRepository.MODS, - (remoteVersion) -> ((ModrinthRemoteModRepository.ProjectVersion) remoteVersion.getSelf()).getProjectId() - )) + Lang.>immutableListOf( + pair("mods.curseforge", CurseForgeRemoteModRepository.MODS), + pair("mods.modrinth", ModrinthRemoteModRepository.MODS) ).forEach(item -> { String text = item.getKey(); - RemoteModRepository remoteModRepository = item.getValue().getKey(); - Function projectIDProvider = item.getValue().getValue(); + RemoteModRepository remoteModRepository = item.getValue(); JFXHyperlink button = new JFXHyperlink(i18n(text)); Task.runAsync(() -> { Optional versionOptional = remoteModRepository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile()); if (versionOptional.isPresent()) { - RemoteMod remoteMod = remoteModRepository.getModById(projectIDProvider.apply(versionOptional.get())); + RemoteMod remoteMod = remoteModRepository.getModById(versionOptional.get().getModid()); FXUtils.runInFX(() -> { + for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) { + switch (modLoaderType) { + case FABRIC: + case FORGE: + case LITE_LOADER: + case QUILT: { + if (!title.getTags().contains(modLoaderType.getLoaderName())) { + title.getTags().add(modLoaderType.getLoaderName()); + } + } + } + } + button.setOnAction(e -> { fireEvent(new DialogCloseEvent()); Controllers.navigate(new DownloadPage( @@ -455,10 +456,12 @@ class ModListPageSkin extends SkinBase { protected void updateControl(ModInfoObject dataItem, boolean empty) { if (empty) return; content.setTitle(dataItem.getTitle()); - if (dataItem.getMod() != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA) { - content.getTags().setAll(dataItem.getMod().getDisplayName()); - } else { - content.getTags().clear(); + content.getTags().clear(); + content.getTags().add(dataItem.getModInfo().getModLoaderType().getLoaderName()); + if (dataItem.getMod() != null) { + if (I18n.getCurrentLocale().getLocale() == Locale.CHINA) { + content.getTags().add(dataItem.getMod().getDisplayName()); + } } content.setSubtitle(dataItem.getSubtitle()); if (booleanProperty != null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java index 8875f2f24..7d7a22cb4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java @@ -18,11 +18,13 @@ package org.jackhuang.hmcl.ui.versions; import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.upgrade.resource.RemoteResourceManager; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.IOUtils; import org.jetbrains.annotations.Nullable; +import java.io.InputStream; import java.util.*; import java.util.logging.Level; import java.util.stream.Collectors; @@ -36,19 +38,19 @@ import static org.jackhuang.hmcl.util.Pair.pair; * @see mcmod.cn */ public enum ModTranslations { - MOD("/assets/mod_data.txt") { + MOD("/assets/mod_data.txt", "translation", "mod_data", "1") { @Override public String getMcmodUrl(Mod mod) { return String.format("https://www.mcmod.cn/class/%s.html", mod.getMcmod()); } }, - MODPACK("/assets/modpack_data.txt") { + MODPACK("/assets/modpack_data.txt", "translation", "modpack_data", "1") { @Override public String getMcmodUrl(Mod mod) { return String.format("https://www.mcmod.cn/modpack/%s.html", mod.getMcmod()); } }, - EMPTY("") { + EMPTY("", "", "", "") { @Override public String getMcmodUrl(Mod mod) { return ""; @@ -66,15 +68,18 @@ public enum ModTranslations { } } - private final String resourceName; + private final String defaultResourceName; + private final RemoteResourceManager.RemoteResourceKey remoteResourceKey; private List mods; private Map modIdMap; // mod id -> mod private Map curseForgeMap; // curseforge id -> mod private List> keywords; private int maxKeywordLength = -1; - ModTranslations(String resourceName) { - this.resourceName = resourceName; + ModTranslations(String defaultResourceName, String namespace, String name, String version) { + this.defaultResourceName = defaultResourceName; + + remoteResourceKey = RemoteResourceManager.get(namespace, name, version, () -> ModTranslations.class.getResourceAsStream(defaultResourceName)); } @Nullable @@ -96,9 +101,9 @@ public enum ModTranslations { public List searchMod(String query) { if (!loadKeywords()) return Collections.emptyList(); - StringBuilder newQuery = query.chars() + StringBuilder newQuery = ((CharSequence) query).chars() .filter(ch -> !Character.isSpaceChar(ch)) - .collect(StringBuilder::new, (sb, value) -> sb.append((char)value), StringBuilder::append); + .collect(StringBuilder::new, (sb, value) -> sb.append((char) value), StringBuilder::append); query = newQuery.toString(); StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength); @@ -115,18 +120,24 @@ public enum ModTranslations { .collect(Collectors.toList()); } - private boolean loadFromResource() { + private boolean loaded() { if (mods != null) return true; - if (StringUtils.isBlank(resourceName)) { + if (StringUtils.isBlank(defaultResourceName)) { mods = Collections.emptyList(); return true; } + try { - String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName)); + InputStream inputStream = remoteResourceKey.getResource(); + if (inputStream == null) { + return false; + } + + String modData = IOUtils.readFullyAsString(inputStream); mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList()); return true; } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to load " + resourceName, e); + LOG.log(Level.WARNING, "Failed to load " + defaultResourceName, e); return false; } } @@ -137,7 +148,7 @@ public enum ModTranslations { } if (mods == null) { - if (!loadFromResource()) return false; + if (!loaded()) return false; } curseForgeMap = new HashMap<>(); @@ -155,7 +166,7 @@ public enum ModTranslations { } if (mods == null) { - if (!loadFromResource()) return false; + if (!loaded()) return false; } modIdMap = new HashMap<>(); @@ -175,7 +186,7 @@ public enum ModTranslations { } if (mods == null) { - if (!loadFromResource()) return false; + if (!loaded()) return false; } keywords = new ArrayList<>(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java index bcfbec688..91ee40d05 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java @@ -49,6 +49,15 @@ public class ModpackDownloadListPage extends DownloadListPage { } } + @Override + protected SortType getBackedRemoteModRepositorySortOrder() { + if ("mods.modrinth".equals(downloadSource.get())) { + return SortType.NAME; + } else { + return SortType.POPULARITY; + } + } + @Override public Type getType() { return Type.MODPACK; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackDownloadListPage.java index f449fe7e8..05ba07bcc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackDownloadListPage.java @@ -49,6 +49,15 @@ public class ResourcePackDownloadListPage extends DownloadListPage { } } + @Override + protected SortType getBackedRemoteModRepositorySortOrder() { + if ("mods.modrinth".equals(downloadSource.get())) { + return SortType.NAME; + } else { + return SortType.POPULARITY; + } + } + @Override public Type getType() { return Type.MOD; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java index a323c23ae..01f26789c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui.versions; import javafx.scene.Node; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.stage.FileChooser; @@ -104,7 +103,7 @@ public class VersionIconDialog extends DialogPane { } private Node createIcon(VersionIconType type) { - ImageView imageView = new ImageView(new Image(type.getResourceUrl())); + ImageView imageView = new ImageView(FXUtils.newBuiltinImage(type.getResourceUrl())); imageView.setMouseTransparent(true); RipplerContainer container = new RipplerContainer(imageView); FXUtils.setLimitWidth(container, 36); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index 89d116c7e..45b835743 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -27,7 +27,6 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; -import javafx.scene.image.Image; import javafx.scene.layout.*; import javafx.scene.text.Text; import javafx.stage.FileChooser; @@ -146,7 +145,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag rootPane.getChildren().add(iconPickerItemWrapper); iconPickerItem = new ImagePickerItem(); - iconPickerItem.setImage(new Image("/assets/img/icon.png")); + iconPickerItem.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png")); iconPickerItem.setTitle(i18n("settings.icon")); iconPickerItem.setOnSelectButtonClicked(e -> onExploreIcon()); iconPickerItem.setOnDeleteButtonClicked(e -> onDeleteIcon()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/ExecutableHeaderHelper.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/ExecutableHeaderHelper.java index 7c47f586d..e5c9ccb5f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/ExecutableHeaderHelper.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import java.io.IOException; import java.io.InputStream; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/HMCLDownloadTask.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/HMCLDownloadTask.java index 8b6fdc06c..0ae8358a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/HMCLDownloadTask.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.util.Pack200Utils; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/IntegrityChecker.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/IntegrityChecker.java index 06c3829b2..a45bc6f1e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/IntegrityChecker.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.util.DigestUtils; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/RemoteVersion.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/RemoteVersion.java index c3ac2caaf..776881aca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/RemoteVersion.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import com.google.gson.JsonElement; import com.google.gson.JsonObject; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateChannel.java similarity index 96% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateChannel.java index 998a3da7d..f56d645dd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateChannel.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import org.jackhuang.hmcl.Metadata; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateChecker.java similarity index 97% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateChecker.java index cc7ce8f2e..d8d3fc708 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateChecker.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -85,7 +85,7 @@ public final class UpdateChecker { throw new IOException("Self verification failed"); } - String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf( + String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, mapOf( pair("version", Metadata.VERSION), pair("channel", channel.channelName))); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateHandler.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java rename to HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateHandler.java index fef6802ca..9b3fc9e6f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/hmcl/UpdateHandler.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.upgrade; +package org.jackhuang.hmcl.upgrade.hmcl; import com.google.gson.Gson; import com.google.gson.JsonParseException; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/resource/RemoteResourceManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/resource/RemoteResourceManager.java new file mode 100644 index 000000000..b9c4a31db --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/resource/RemoteResourceManager.java @@ -0,0 +1,204 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.upgrade.resource; + +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.versions.ModTranslations; +import org.jackhuang.hmcl.upgrade.hmcl.IntegrityChecker; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.function.ExceptionalSupplier; +import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jackhuang.hmcl.util.io.IOUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public final class RemoteResourceManager { + private RemoteResourceManager() { + } + + private static final class RemoteResource { + @SerializedName("sha1") + private final String sha1; + + @SerializedName("urls") + private final String[] urls; + + private transient byte[] data = null; + + private RemoteResource(String sha1, String[] urls) { + this.sha1 = sha1; + this.urls = urls; + } + + public void download(Path path, Runnable callback) { + if (data != null) { + return; + } + + new FileDownloadTask(Arrays.stream(urls).map(NetworkUtils::toURL).collect(Collectors.toList()), path.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", sha1)) + .whenComplete(Schedulers.defaultScheduler(), (result, exception) -> { + if (exception != null) { + data = Files.readAllBytes(path); + callback.run(); + } + }).start(); + } + } + + public static final class RemoteResourceKey { + private final String namespace; + private final String name; + private final String version; + private final Path cachePath; + private final ExceptionalSupplier localResourceSupplier; + private String localResourceSha1 = null; + + public RemoteResourceKey(String namespace, String name, String version, ExceptionalSupplier localResourceSupplier) { + this.namespace = namespace; + this.name = name; + this.version = version; + this.localResourceSupplier = localResourceSupplier; + + this.cachePath = Metadata.HMCL_DIRECTORY.resolve("remoteResources").resolve(namespace).resolve(name).resolve(version).resolve(String.format("%s-%s-%s.resource", namespace, name, version)); + } + + private InputStream getLocalResource() throws IOException { + if (Files.isReadable(cachePath)) { + return Files.newInputStream(cachePath); + } + return localResourceSupplier.get(); + } + + private String getLocalResourceSha1() throws IOException { + if (localResourceSha1 == null) { + localResourceSha1 = DigestUtils.digestToString("SHA-1", IOUtils.readFullyAsByteArray(getLocalResource())); + } + + return localResourceSha1; + } + + @Nullable + private RemoteResource getRemoteResource() { + return Optional.ofNullable(remoteResources.get(namespace)).map(map -> map.get(name)).map(map -> map.get(version)).orElse(null); + } + + @Nullable + public InputStream getResource() throws IOException { + RemoteResource remoteResource = getRemoteResource(); + + if (remoteResource == null) { + return getLocalResource(); + } + + if (remoteResource.sha1.equals(getLocalResourceSha1())) { + return getLocalResource(); + } + + if (remoteResource.data == null) { + return null; + } + + return new ByteArrayInputStream(remoteResource.data); + } + + public void downloadRemoteResourceIfNecessary() throws IOException { + RemoteResource remoteResource = getRemoteResource(); + + if (remoteResource == null) { + return; + } + + if (remoteResource.sha1.equals(getLocalResourceSha1())) { + return; + } + + remoteResource.download(cachePath, () -> localResourceSha1 = null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RemoteResourceKey that = (RemoteResourceKey) o; + + if (!namespace.equals(that.namespace)) return false; + if (!name.equals(that.name)) return false; + return version.equals(that.version); + } + + @Override + public int hashCode() { + int result = namespace.hashCode(); + result = 31 * result + name.hashCode(); + result = 31 * result + version.hashCode(); + return result; + } + } + + private static final Map>> remoteResources = new ConcurrentHashMap<>(); + + private static final Map keys = new ConcurrentHashMap<>(); + + public static void init() { + Task.>>>supplyAsync(() -> + IntegrityChecker.isSelfVerified() ? HttpRequest.GET(Metadata.RESOURCE_UPDATE_URL).getJson( + new TypeToken>>>() { + }.getType() + ) : null + ).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> { + if (exception == null && result != null) { + remoteResources.clear(); + remoteResources.putAll(result); + + for (RemoteResourceKey key : keys.values()) { + key.downloadRemoteResourceIfNecessary(); + } + } + }).start(); + } + + public static void register() { + ModTranslations.values(); + } + + public static RemoteResourceKey get(@NotNull String namespace, @NotNull String name, @NotNull String version, ExceptionalSupplier defaultSupplier) { + String stringKey = String.format("%s:%s:%s", namespace, name, version); + RemoteResourceKey key = keys.containsKey(stringKey) ? keys.get(stringKey) : new RemoteResourceKey(namespace, name, version, defaultSupplier); + Task.runAsync(key::downloadRemoteResourceIfNecessary).start(); + keys.put(stringKey, key); + return key; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java index bb097beca..8314a2cee 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java @@ -23,8 +23,8 @@ import javafx.scene.control.Alert.AlertType; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.countly.CrashReport; import org.jackhuang.hmcl.ui.CrashWindow; -import org.jackhuang.hmcl.upgrade.IntegrityChecker; -import org.jackhuang.hmcl.upgrade.UpdateChecker; +import org.jackhuang.hmcl.upgrade.hmcl.IntegrityChecker; +import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker; import org.jackhuang.hmcl.util.io.NetworkUtils; import java.io.IOException; diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b92f3bcc7..0a702148d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -745,6 +745,8 @@ modpack.choose.local=Import from local file modpack.choose.local.detail=You can drag and drop the modpack file here modpack.choose.remote=Download from the Internet modpack.choose.remote.detail=A direct download link to the remote modpack file is required +modpack.choose.repository=Download a modpack from Curseforge or Modrinth +modpack.choose.repository.detail=Remember to go back to this page and drop the modpack file here after the modpack is downloaded modpack.choose.remote.tooltip=Please enter your modpack URL modpack.completion=Downloading dependencies modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported. @@ -873,10 +875,17 @@ mods.check_updates.target_version=Target Version mods.check_updates.update=Update mods.choose_mod=Choose a mod mods.curseforge=CurseForge -mods.dependencies=Dependencies +mods.dependency.embedded=built-in pre-mod (already packaged in the mod file by the author, no need to download separately) +mods.dependency.optional=optional pre-mod (if the game is missing, but mod functionality may be missing) +mods.dependency.required=required pre-mod (must be downloaded separately, missing may cause the game to fail to launch) +mods.dependency.tool=precursor library (must be downloaded separately, missing may cause the game to fail to launch) +mods.dependency.include=built-in pre-mod (already packaged in the mod file by the author, no need to download separately) +mods.dependency.incompatible=incompatible mod (installing both the mod and the mod being downloaded will cause the game to fail to launch) +mods.dependency.broken=Broken pre-mod (This premod used to exist on the mod repository, but is now deleted.) Try a different download source. mods.disable=Disable mods.download=Mod Download mods.download.title=Mod Download - %1s +mods.download.recommend=Recommended Mod Version - Minecraft %1s mods.enable=Enable mods.manage=Manage Mods mods.mcbbs=MCBBS @@ -981,6 +990,11 @@ search.hint.chinese=Search queries support both Chinese and English search.hint.english=Only English is supported search.enter=Enter text here search.sort=Sort By +search.first_page=The first page +search.previous_page=The previous page +search.next_page=The next page +search.last_page=The last page +search.page_n=%d / %s selector.choose=Choose selector.choose_file=Select a file diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index ba3c630a8..977943b65 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -802,7 +802,6 @@ mods.check_updates.target_version=Versión de destino mods.check_updates.update=Actualización mods.choose_mod=Elige un mod mods.curseforge=CurseForge -mods.dependencies=Dependencias mods.disable=Desactivar mods.download=Descarga de mods mods.download.title=Descarga de mods - %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 240b08dea..9526aec7b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -634,7 +634,6 @@ mods.check_updates.target_version=Target mods.check_updates.update=更新 mods.choose_mod=modを選択してください mods.curseforge=CurseForge -mods.dependencies=Dependencies mods.disable=無効にする mods.download=Modのダウンロード mods.download.title=Modダウンロード- %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 94ed861fa..9b5b81bd8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -639,7 +639,6 @@ mods.check_updates.target_version=Цель mods.check_updates.update=Обновить mods.choose_mod=Выбрать свои моды mods.curseforge=CurseForge -mods.dependencies=Зависимости mods.disable=Отключить mods.download=Скачивание модов mods.download.title=Скачивание модов - %1s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 9b09676cc..f5709f509 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -617,6 +617,8 @@ modpack.choose.local=匯入本機模組包檔案 modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以安裝 modpack.choose.remote=從網路下載模組包 modpack.choose.remote.detail=需要提供模組包的下載連結 +modpack.choose.repository= 從 Curseforge / Modrinth 下載整合包 +modpack.choose.repository.detail=下載后記得回到這個界面,把整合包拖進來哦 modpack.choose.remote.tooltip=要下載的模組包的連結 modpack.completion=下載模組包相關檔案 modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。 @@ -729,8 +731,8 @@ mods=模組 mods.add=新增模組 mods.add.failed=新增模組 %s 失敗。 mods.add.success=成功新增模組 %s。 -mods.broken_dependency.title=損壞前置模組 -mods.broken_dependency.desc=該前置模組曾經在該模組倉庫上存在過,但現在被刪除了。換個下載源試試吧。 +mods.broken_dependency.title=損壞的前置模組 +mods.broken_dependency.desc=該前置模組曾經在該模組倉庫上存在過,但現在被刪除了,換個下載源試試吧。 mods.category=類別 mods.check_updates=檢查模組更新 mods.check_updates.current_version=當前版本 @@ -742,10 +744,17 @@ mods.check_updates.target_version=目標版本 mods.check_updates.update=更新 mods.choose_mod=選擇模組 mods.curseforge=CurseForge -mods.dependencies=前置 Mod +mods.dependency.embedded=內置前端模組(作者已經打包在模組檔中,無需額外下載) +mods.dependency.optional=可選的前模組(如果缺少遊戲,遊戲可以運行,但模組功能可能缺失) +mods.dependency.required=必需的預模式(必須單獨下載,缺少可能會導致遊戲無法啟動) +mods.dependency.tool=前端庫(必須單獨下載,缺少可能會導致遊戲無法啟動) +mods.dependency.include=內置前綴模組(作者已經打包在模組檔中,無需額外下載) +mods.dependency.incompatible=模組不相容(同時安裝模組和模組都下載會導致遊戲無法啟動) +mods.dependency.broken=損壞的前綴(這個前綴曾經存在於 mod 倉庫中,但現在已被刪除)。嘗試其他下載源。 mods.disable=停用 mods.download=模組下載 mods.download.title=模組下載 - %1s +mods.download.recommend=推薦版本 - Minecraft %1s mods.enable=啟用 mods.manage=模組管理 mods.mcbbs=MCBBS @@ -846,6 +855,11 @@ search.hint.chinese=支援中英文搜尋 search.hint.english=僅支援英文搜尋 search.enter=請在此處輸入 search.sort=排序 +search.first_page=第一頁 +search.previous_page=上一頁 +search.next_page=下一頁 +search.last_page=最後一頁 +search.page_n=%d / %s selector.choose=選擇 selector.choose_file=選擇檔案 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 1255ea16e..fac777857 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -616,6 +616,8 @@ modpack.choose.local=导入本地整合包文件 modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以安装 modpack.choose.remote=从互联网下载整合包 modpack.choose.remote.detail=需要提供整合包的下载链接 +modpack.choose.repository=从 Curseforge / Modrinth 下载整合包 +modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦 modpack.choose.remote.tooltip=要下载的整合包的链接 modpack.completion=下载整合包相关文件 modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图) @@ -728,7 +730,7 @@ mods=模组 mods.add=添加模组 mods.add.failed=添加模组 %s 失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 mods.add.success=成功添加模组 %s。 -mods.broken_dependency.title=损坏前置模组 +mods.broken_dependency.title=损坏的前置模组 mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。 mods.category=类别 mods.check_updates=检查模组更新 @@ -741,10 +743,17 @@ mods.check_updates.target_version=目标版本 mods.check_updates.update=更新 mods.choose_mod=选择模组 mods.curseforge=CurseForge -mods.dependencies=前置 Mod +mods.dependency.embedded=内置的前置模组(已经由作者打包在模组文件中,无需另外下载) +mods.dependency.optional=可选的前置模组(若缺失游戏能够正常运行,但模组功能可能缺失) +mods.dependency.required=必须的前置模组(必须另外下载,缺失可能会导致游戏无法启动) +mods.dependency.tool=前置库(必须另外下载,缺失可能会导致游戏无法启动) +mods.dependency.include=内置的前置模组(已经由作者打包在模组文件中,无需另外下载) +mods.dependency.incompatible=不兼容的模组(同时安装该模组和正在下载的模组会导致游戏无法启动) +mods.dependency.broken=损坏的前置模组(该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。) mods.disable=禁用 mods.download=模组下载 mods.download.title=模组下载 - %1s +mods.download.recommend=推荐版本 - Minecraft %1s mods.enable=启用 mods.manage=模组管理 mods.mcbbs=MCBBS @@ -845,6 +854,11 @@ search.hint.chinese=支持中英文搜索 search.hint.english=仅支持英文搜索 search.enter=可在此处输入 search.sort=排序 +search.first_page=第一页 +search.previous_page=上一页 +search.next_page=下一页 +search.last_page=最后一页 +search.page_n=%d / %s selector.choose=选择 selector.choose_file=选择文件 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java index bca60404b..e1d78f0f2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download; import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.VersionProvider; +import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -161,11 +162,20 @@ public final class LibraryAnalyzer implements Iterable getModLoaders() { + return Arrays.stream(LibraryType.values()) + .filter(LibraryType::isModLoader) + .filter(this::has) + .map(LibraryType::getModLoaderType) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + public enum LibraryType { - MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$")), - FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader")), - FABRIC_API(true, "fabric-api", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-api")), - FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)")) { + MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$"), null), + FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader"), ModLoaderType.FABRIC), + FABRIC_API(true, "fabric-api", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-api"), null), + FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)"), ModLoaderType.FORGE) { private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?[0-9.]+)(-([0-9.]+))?$"); @Override @@ -177,21 +187,23 @@ public final class LibraryAnalyzer implements Iterable { public static Version unique(Version version) { List libraries = new ArrayList<>(); - SimpleMultimap multimap = new SimpleMultimap(HashMap::new, ArrayList::new); + SimpleMultimap> multimap = new SimpleMultimap<>(HashMap::new, ArrayList::new); for (Library library : version.getLibraries()) { String id = library.getGroupId() + ":" + library.getArtifactId(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java index a68d1463f..701380b7d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java @@ -37,7 +37,7 @@ public abstract class VersionList { * key: game version. * values: corresponding remote versions. */ - protected final SimpleMultimap versions = new SimpleMultimap(HashMap::new, TreeSet::new); + protected final SimpleMultimap> versions = new SimpleMultimap<>(HashMap::new, TreeSet::new); /** * True if the version list has been loaded. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java index dc8d1693a..d0767fc3d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/EventManager.java @@ -30,7 +30,7 @@ import java.util.function.Consumer; */ public final class EventManager { - private final SimpleMultimap> handlers + private final SimpleMultimap, CopyOnWriteArraySet>> handlers = new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new); public Consumer registerWeak(Consumer consumer) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java index 7ec30301c..4506179e5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java @@ -23,6 +23,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import org.jackhuang.hmcl.mod.modinfo.PackMcMeta; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java index e36d78136..f4fcf4d39 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModLoaderType.java @@ -18,10 +18,20 @@ package org.jackhuang.hmcl.mod; public enum ModLoaderType { - UNKNOWN, - FORGE, - FABRIC, - QUILT, - LITE_LOADER, - PACK + UNKNOWN("Unknown"), + FORGE("Forge"), + FABRIC("Fabric"), + QUILT("Quilt"), + LITE_LOADER("LiteLoader"), + PACK("Pack"); + + private final String loaderName; + + ModLoaderType(String loaderName) { + this.loaderName = loaderName; + } + + public final String getLoaderName() { + return loaderName; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index 538eeabf2..16c207c14 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -17,7 +17,10 @@ */ package org.jackhuang.hmcl.mod; +import com.google.gson.JsonParseException; import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.mod.modinfo.*; +import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; @@ -25,12 +28,32 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.IOException; import java.nio.file.*; -import java.util.Collection; -import java.util.HashMap; -import java.util.Objects; -import java.util.TreeSet; +import java.util.*; public final class ModManager { + @FunctionalInterface + private interface ModMetadataReader { + LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException; + } + + private static final Map> READERS; + + static { + TreeMap> readers = new TreeMap<>(); + readers.put("zip", Pair.pair(new ModMetadataReader[]{ + ForgeOldModMetadata::fromFile, + ForgeNewModMetadata::fromFile, + FabricModMetadata::fromFile, + QuiltModMetadata::fromFile, + PackMcMeta::fromFile, + }, "")); + readers.put("jar", readers.get("zip")); + readers.put("litemod", Pair.pair(new ModMetadataReader[]{ + LiteModMetadata::fromFile + }, "LiteLoader Mod")); + READERS = Collections.unmodifiableMap(readers); + } + private final GameRepository repository; private final String id; private final TreeSet localModFiles = new TreeSet<>(); @@ -71,46 +94,28 @@ public final class ModManager { public LocalModFile getModInfo(Path modFile) { String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION); - String description; - if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { - try { - return ForgeOldModMetadata.fromFile(this, modFile, fs); - } catch (Exception ignore) { - } - - try { - return ForgeNewModMetadata.fromFile(this, modFile, fs); - } catch (Exception ignore) { - } - - try { - return FabricModMetadata.fromFile(this, modFile, fs); - } catch (Exception ignore) { - } - - try { - return PackMcMeta.fromFile(this, modFile, fs); - } catch (Exception ignore) { - } - } catch (Exception ignored) { - } - - description = ""; - } else if (fileName.endsWith(".litemod")) { - try { - return LiteModMetadata.fromFile(this, modFile); - } catch (Exception ignore) { - description = "LiteLoader Mod"; - } - } else { + String extension = fileName.substring(fileName.lastIndexOf(".") + 1); + Pair currentReader = READERS.get(extension); + if (currentReader == null) { throw new IllegalArgumentException("File " + modFile + " is not a mod file."); } + + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { + for (ModMetadataReader reader : currentReader.getKey()) { + try { + return reader.fromFile(this, modFile, fs); + } catch (Exception ignore) { + } + } + } catch (Exception ignored) { + } + return new LocalModFile(this, getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN), modFile, FileUtils.getNameWithoutExtension(modFile), - new LocalModFile.Description(description)); + new LocalModFile.Description(currentReader.getValue()) + ); } public void refreshMods() throws IOException { @@ -281,6 +286,11 @@ public final class ModManager { return true; } + if (Files.exists(fs.getPath("quilt.mod.json"))) { + // Quilt mod + return true; + } + if (Files.exists(fs.getPath("litemod.json"))) { // Liteloader mod return true; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java index 87ce46bc7..db7190864 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteMod.java @@ -101,6 +101,72 @@ public class RemoteMod { Alpha } + public enum DependencyType { + EMBEDDED, + OPTIONAL, + REQUIRED, + TOOL, + INCLUDE, + INCOMPATIBLE, + BROKEN + } + + public static final class Dependency { + private static Dependency BROKEN_DEPENDENCY = null; + + private final DependencyType type; + + private final RemoteModRepository remoteModRepository; + + private final String id; + + private RemoteMod remoteMod = null; + + private Dependency(DependencyType type, RemoteModRepository remoteModRepository, String modid) { + this.type = type; + this.remoteModRepository = remoteModRepository; + this.id = modid; + } + + public static Dependency ofGeneral(DependencyType type, RemoteModRepository remoteModRepository, String modid) { + if (type == DependencyType.BROKEN) { + return ofBroken(); + } else { + return new Dependency(type, remoteModRepository, modid); + } + } + + public static Dependency ofBroken() { + if (BROKEN_DEPENDENCY == null) { + BROKEN_DEPENDENCY = new Dependency(DependencyType.BROKEN, null, null); + } + return BROKEN_DEPENDENCY; + } + + public DependencyType getType() { + return this.type; + } + + public RemoteModRepository getRemoteModRepository() { + return this.remoteModRepository; + } + + public String getId() { + return this.id; + } + + public RemoteMod load() throws IOException { + if (this.remoteMod == null) { + if (this.type == DependencyType.BROKEN) { + this.remoteMod = RemoteMod.getEmptyRemoteMod(); + } else { + this.remoteMod = this.remoteModRepository.getModById(this.id); + } + } + return this.remoteMod; + } + } + public enum Type { CURSEFORGE(CurseForgeRemoteModRepository.MODS), MODRINTH(ModrinthRemoteModRepository.MODS); @@ -135,11 +201,11 @@ public class RemoteMod { private final Date datePublished; private final VersionType versionType; private final File file; - private final List dependencies; + private final List dependencies; private final List gameVersions; private final List loaders; - public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { + public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List dependencies, List gameVersions, List loaders) { this.self = self; this.modid = modid; this.name = name; @@ -185,7 +251,7 @@ public class RemoteMod { return file; } - public List getDependencies() { + public List getDependencies() { return dependencies; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 119ea85f5..a45f8618a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -51,7 +51,39 @@ public interface RemoteModRepository { DESC } - Stream search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) + class SearchResult { + private final Stream sortedResults; + + private final Stream unsortedResults; + + private final int totalPages; + + public SearchResult(Stream sortedResults, Stream unsortedResults, int totalPages) { + this.sortedResults = sortedResults; + this.unsortedResults = unsortedResults; + this.totalPages = totalPages; + } + + public SearchResult(Stream sortedResults, int pages) { + this.sortedResults = sortedResults; + this.unsortedResults = sortedResults; + this.totalPages = pages; + } + + public Stream getResults() { + return this.sortedResults; + } + + public Stream getUnsortedResults() { + return this.unsortedResults; + } + + public int getTotalPages() { + return this.totalPages; + } + } + + SearchResult search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException; Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index 5b8fbae6c..5f714e73a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -21,6 +21,8 @@ import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Pair; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -30,6 +32,15 @@ import java.util.stream.Stream; @Immutable public class CurseAddon implements RemoteMod.IMod { + public static final Map RELATION_TYPE = Lang.mapOf( + Pair.pair(1, RemoteMod.DependencyType.EMBEDDED), + Pair.pair(2, RemoteMod.DependencyType.OPTIONAL), + Pair.pair(3, RemoteMod.DependencyType.REQUIRED), + Pair.pair(4, RemoteMod.DependencyType.TOOL), + Pair.pair(5, RemoteMod.DependencyType.INCOMPATIBLE), + Pair.pair(6, RemoteMod.DependencyType.INCLUDE) + ); + private final int id; private final int gameId; private final String name; @@ -566,7 +577,12 @@ public class CurseAddon implements RemoteMod.IMod { getFileDate(), versionType, new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), - Collections.emptyList(), + dependencies.stream().map(dependency -> { + if (!RELATION_TYPE.containsKey(dependency.getRelationType())) { + throw new IllegalStateException("Broken datas."); + } + return RemoteMod.Dependency.ofGeneral(RELATION_TYPE.get(dependency.getRelationType()), CurseForgeRemoteModRepository.MODS, Integer.toString(dependency.getModId())); + }).filter(Objects::nonNull).collect(Collectors.toList()), gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()), gameVersions.stream().flatMap(version -> { if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index b67104e40..e2379b870 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -22,6 +22,8 @@ import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.MurmurHash2; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.JarUtils; import org.jetbrains.annotations.Nullable; @@ -42,6 +44,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository private static final String PREFIX = "https://api.curseforge.com"; private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getManifestAttribute("CurseForge-Api-Key", "")); + private static final int WORD_PERFECT_MATCH_WEIGHT = 50; + public static boolean isAvailable() { return !apiKey.isEmpty(); } @@ -91,7 +95,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository } @Override - public Stream search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { + public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { int categoryId = 0; if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId(); Response> response = HttpRequest.GET(PREFIX + "/v1/mods/search", @@ -102,12 +106,51 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository pair("searchFilter", searchFilter), pair("sortField", Integer.toString(toModsSearchSortField(sortType))), pair("sortOrder", toSortOrder(sortOrder)), - pair("index", Integer.toString(pageOffset)), + pair("index", Integer.toString(pageOffset * pageSize)), pair("pageSize", Integer.toString(pageSize))) .header("X-API-KEY", apiKey) .getJson(new TypeToken>>() { }.getType()); - return response.getData().stream().map(CurseAddon::toMod); + Stream res = response.getData().stream().map(CurseAddon::toMod); + if (sortType != SortType.NAME || searchFilter.length() == 0) { + return new SearchResult(res, (int)Math.ceil((double)response.pagination.totalCount / pageSize)); + } + + // https://github.com/huanghongxun/HMCL/issues/1549 + String lowerCaseSearchFilter = searchFilter.toLowerCase(); + Map searchFilterWords = new HashMap<>(); + for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) { + searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1); + } + + return new SearchResult(res.map(remoteMod -> { + String lowerCaseResult = remoteMod.getTitle().toLowerCase(); + int[][] lev = new int[lowerCaseSearchFilter.length() + 1][lowerCaseResult.length() + 1]; + for (int i = 0; i < lowerCaseResult.length() + 1; i++) { + lev[0][i] = i; + } + for (int i = 0; i < lowerCaseSearchFilter.length() + 1; i++) { + lev[i][0] = i; + } + for (int i = 1; i < lowerCaseSearchFilter.length() + 1; i++) { + for (int j = 1; j < lowerCaseResult.length() + 1; j++) { + int countByInsert = lev[i][j - 1] + 1; + int countByDel = lev[i - 1][j] + 1; + int countByReplace = lowerCaseSearchFilter.charAt(i - 1) == lowerCaseResult.charAt(j - 1) ? lev[i - 1][j - 1] : lev[i - 1][j - 1] + 1; + lev[i][j] = Math.min(countByInsert, Math.min(countByDel, countByReplace)); + } + } + + int diff = lev[lowerCaseSearchFilter.length()][lowerCaseResult.length()]; + + for (String s : StringUtils.tokenize(lowerCaseResult)) { + if (searchFilterWords.containsKey(s)) { + diff -= WORD_PERFECT_MATCH_WEIGHT * searchFilterWords.get(s) * s.length(); + } + } + + return pair(remoteMod, diff); + }).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), res, response.pagination.totalCount); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java similarity index 95% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java index caf915a30..9b0a721b3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/FabricModMetadata.java @@ -15,10 +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.mod; +package org.jackhuang.hmcl.mod.modinfo; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java similarity index 93% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java index 84ede9dfd..c74adfb24 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeNewModMetadata.java @@ -1,7 +1,10 @@ -package org.jackhuang.hmcl.mod; +package org.jackhuang.hmcl.mod.modinfo; import com.google.gson.JsonParseException; import com.moandjiezana.toml.Toml; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.io.FileUtils; @@ -20,7 +23,6 @@ import static org.jackhuang.hmcl.util.Logging.LOG; @Immutable public final class ForgeNewModMetadata { - private final String modLoader; private final String loaderVersion; @@ -134,7 +136,7 @@ public final class ForgeNewModMetadata { } } return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()), - mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "", + mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "", mod.getDisplayURL(), metadata.getLogoFile()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java similarity index 96% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java index 9269b0d9b..067c5b8c2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/ForgeOldModMetadata.java @@ -15,11 +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.mod; +package org.jackhuang.hmcl.mod.modinfo; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -37,7 +40,6 @@ import java.util.List; */ @Immutable public final class ForgeOldModMetadata { - @SerializedName("modid") private final String modId; private final String name; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java similarity index 93% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java index f80393a55..d29f43dfc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/LiteModMetadata.java @@ -15,13 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.mod; +package org.jackhuang.hmcl.mod.modinfo; import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.JsonUtils; import java.io.IOException; +import java.nio.file.FileSystem; import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -106,7 +110,7 @@ public final class LiteModMetadata { return updateURI; } - public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { + public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException { try (ZipFile zipFile = new ZipFile(modFile.toFile())) { ZipEntry entry = zipFile.getEntry("litemod.json"); if (entry == null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java similarity index 97% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java index a973870ef..7ad2b2806 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java @@ -15,11 +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.mod; +package org.jackhuang.hmcl.mod.modinfo; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.Validation; @@ -36,7 +39,6 @@ import java.util.List; @Immutable public class PackMcMeta implements Validation { - @SerializedName("pack") private final PackInfo pack; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java new file mode 100644 index 000000000..d2515d192 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/QuiltModMetadata.java @@ -0,0 +1,81 @@ +package org.jackhuang.hmcl.mod.modinfo; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; +import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.stream.Collectors; + +@Immutable +public final class QuiltModMetadata { + private static final class QuiltLoader { + private static final class Metadata { + private final String name; + private final String description; + private final JsonObject contributors; + private final String icon; + private final JsonObject contact; + + public Metadata(String name, String description, JsonObject contributors, String icon, JsonObject contact) { + this.name = name; + this.description = description; + this.contributors = contributors; + this.icon = icon; + this.contact = contact; + } + } + + private final String id; + private final String version; + private final Metadata metadata; + + public QuiltLoader(String id, String version, Metadata metadata) { + this.id = id; + this.version = version; + this.metadata = metadata; + } + } + + private final int schema_version; + private final QuiltLoader quilt_loader; + + public QuiltModMetadata(int schemaVersion, QuiltLoader quiltLoader) { + this.schema_version = schemaVersion; + this.quilt_loader = quiltLoader; + } + + public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException { + Path path = fs.getPath("quilt.mod.json"); + if (Files.notExists(path)) { + throw new IOException("File " + modFile + " is not a Quilt mod."); + } + + QuiltModMetadata root = JsonUtils.fromNonNullJson(FileUtils.readText(path), QuiltModMetadata.class); + if (root.schema_version != 1) { + throw new IOException("File " + modFile + " is not a supported Quilt mod."); + } + + return new LocalModFile( + modManager, + modManager.getLocalMod(root.quilt_loader.id, ModLoaderType.QUILT), + modFile, + root.quilt_loader.metadata.name, + new LocalModFile.Description(root.quilt_loader.metadata.description), + root.quilt_loader.metadata.contributors.entrySet().stream().map(entry -> String.format("%s (%s)", entry.getKey(), entry.getValue().getAsJsonPrimitive().getAsString())).collect(Collectors.joining(", ")), + root.quilt_loader.version, + "", + Optional.ofNullable(root.quilt_loader.metadata.contact.get("homepage")).map(jsonElement -> jsonElement.getAsJsonPrimitive().getAsString()).orElse(""), + root.quilt_loader.metadata.icon + ); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index f367489e2..b66b4729a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -75,7 +75,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { } @Override - public Stream search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { + public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { List> facets = new ArrayList<>(); facets.add(Collections.singletonList("project_type:" + projectType)); if (StringUtils.isNotBlank(gameVersion)) { @@ -87,14 +87,14 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { Map query = mapOf( pair("query", searchFilter), pair("facets", JsonUtils.UGLY_GSON.toJson(facets)), - pair("offset", Integer.toString(pageOffset)), + pair("offset", Integer.toString(pageOffset * pageSize)), pair("limit", Integer.toString(pageSize)), pair("index", convertSortType(sort)) ); Response response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query)) .getJson(new TypeToken>() { }.getType()); - return response.getHits().stream().map(ProjectSearchResult::toMod); + return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int)Math.ceil((double)response.totalHits / pageSize)); } @Override @@ -286,17 +286,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { @Override public List loadDependencies(RemoteModRepository modRepository) throws IOException { - Set dependencies = modRepository.getRemoteVersionsById(getId()) + Set dependencies = modRepository.getRemoteVersionsById(getId()) .flatMap(version -> version.getDependencies().stream()) .collect(Collectors.toSet()); List mods = new ArrayList<>(); - for (String dependencyId : dependencies) { - if (dependencyId == null) { - mods.add(RemoteMod.getEmptyRemoteMod()); - } - if (StringUtils.isNotBlank(dependencyId)) { - mods.add(modRepository.getModById(dependencyId)); - } + for (RemoteMod.Dependency dependency : dependencies) { + mods.add(dependency.load()); } return mods; } @@ -313,9 +308,9 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { title, description, categories, - null, + String.format("https://modrinth.com/%s/%s", projectType, id), iconUrl, - (RemoteMod.IMod) this + this ); } } @@ -351,6 +346,13 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { } public static class ProjectVersion implements RemoteMod.IVersion { + private static final Map DEPENDENCY_TYPE = Lang.mapOf( + Pair.pair("required", RemoteMod.DependencyType.REQUIRED), + Pair.pair("optional", RemoteMod.DependencyType.OPTIONAL), + Pair.pair("embedded", RemoteMod.DependencyType.EMBEDDED), + Pair.pair("incompatible", RemoteMod.DependencyType.INCOMPATIBLE) + ); + private final String name; @SerializedName("version_number") @@ -496,7 +498,17 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { datePublished, type, files.get(0).toFile(), - dependencies.stream().map(dependency -> dependency.getVersionId() == null ? null : dependency.getProjectId()).collect(Collectors.toList()), + dependencies.stream().map(dependency -> { + if (dependency.projectId == null) { + return RemoteMod.Dependency.ofBroken(); + } + + if (!DEPENDENCY_TYPE.containsKey(dependency.dependencyType)) { + throw new IllegalStateException("Broken datas"); + } + + return RemoteMod.Dependency.ofGeneral(DEPENDENCY_TYPE.get(dependency.dependencyType), MODS, dependency.projectId); + }).filter(Objects::nonNull).collect(Collectors.toList()), gameVersions, loaders.stream().flatMap(loader -> { if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC); @@ -651,17 +663,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository { @Override public List loadDependencies(RemoteModRepository modRepository) throws IOException { - Set dependencies = modRepository.getRemoteVersionsById(getProjectId()) + Set dependencies = modRepository.getRemoteVersionsById(getProjectId()) .flatMap(version -> version.getDependencies().stream()) .collect(Collectors.toSet()); List mods = new ArrayList<>(); - for (String dependencyId : dependencies) { - if (dependencyId == null) { - mods.add(RemoteMod.getEmptyRemoteMod()); - } - if (StringUtils.isNotBlank(dependencyId)) { - mods.add(modRepository.getModById(dependencyId)); - } + for (RemoteMod.Dependency dependency : dependencies) { + mods.add(dependency.load()); } return mods; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java index 03f4aee30..4ac8ab06b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java @@ -28,12 +28,12 @@ import java.util.function.Supplier; * * @author huangyuhui */ -public final class SimpleMultimap { +public final class SimpleMultimap> { - private final Map> map; - private final Supplier> valuer; + private final Map map; + private final Supplier valuer; - public SimpleMultimap(Supplier>> mapper, Supplier> valuer) { + public SimpleMultimap(Supplier> mapper, Supplier valuer) { this.map = mapper.get(); this.valuer = valuer; } @@ -48,7 +48,7 @@ public final class SimpleMultimap { public Collection values() { Collection res = valuer.get(); - for (Map.Entry> entry : map.entrySet()) + for (Map.Entry entry : map.entrySet()) res.addAll(entry.getValue()); return res; } @@ -61,27 +61,27 @@ public final class SimpleMultimap { return map.containsKey(key) && !map.get(key).isEmpty(); } - public Collection get(K key) { + public M get(K key) { return map.computeIfAbsent(key, any -> valuer.get()); } public void put(K key, V value) { - Collection set = get(key); + M set = get(key); set.add(value); } public void putAll(K key, Collection value) { - Collection set = get(key); + M set = get(key); set.addAll(value); } - public Collection removeKey(K key) { + public M removeKey(K key) { return map.remove(key); } public boolean removeValue(V value) { boolean flag = false; - for (Collection c : map.values()) + for (M c : map.values()) flag |= c.remove(value); return flag; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 5a02cfbb6..e4735b180 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -357,6 +357,32 @@ public final class StringUtils { return true; } + public static class DynamicCommonSubsequence { + private LongestCommonSubsequence calculator; + + public DynamicCommonSubsequence(int intLengthA, int intLengthB) { + if (intLengthA > intLengthB) { + calculator = new LongestCommonSubsequence(intLengthA, intLengthB); + } else { + calculator = new LongestCommonSubsequence(intLengthB, intLengthA); + } + } + + public int calc(CharSequence a, CharSequence b) { + if (a.length() < b.length()) { + CharSequence t = a; + a = b; + b = t; + } + + if (calculator.maxLengthA < a.length() || calculator.maxLengthB < b.length()) { + calculator = new LongestCommonSubsequence(a.length(), b.length()); + } + + return calculator.calc(a, b); + } + } + /** * Class for computing the longest common subsequence between strings. */ diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java index 4f2016ce7..c8dbc8792 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/HttpRequest.java @@ -141,7 +141,7 @@ public abstract class HttpRequest { return getStringWithRetry(() -> { HttpURLConnection con = createConnection(); con = resolveConnection(con); - return IOUtils.readFullyAsString(con.getInputStream()); + return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(con.getInputStream()) : con.getInputStream()); }, retryTimes); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java index 5afe2c6cc..fc9e26594 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.util.io; import java.io.*; +import java.util.zip.GZIPInputStream; /** * This utility class consists of some util methods operating on InputStream/OutputStream. @@ -85,4 +86,8 @@ public final class IOUtils { dest.write(buf, 0, len); } } + + public static InputStream wrapFromGZip(InputStream inputStream) throws IOException { + return new GZIPInputStream(inputStream); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java index f16ba9ded..09832d69e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java @@ -144,8 +144,8 @@ public final class NetworkUtils { while (true) { conn.setUseCaches(false); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); + conn.setConnectTimeout(8000); + conn.setReadTimeout(8000); conn.setInstanceFollowRedirects(false); Map> properties = conn.getRequestProperties(); String method = conn.getRequestMethod(); @@ -209,13 +209,13 @@ public final class NetworkUtils { public static String readData(HttpURLConnection con) throws IOException { try { try (InputStream stdout = con.getInputStream()) { - return IOUtils.readFullyAsString(stdout); + return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stdout) : stdout); } } catch (IOException e) { try (InputStream stderr = con.getErrorStream()) { if (stderr == null) throw e; - return IOUtils.readFullyAsString(stderr); + return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stderr) : stderr); } } } diff --git a/README.md b/README.md index 55fdb5682..1e6c412f7 100644 --- a/README.md +++ b/README.md @@ -59,16 +59,18 @@ Make sure you have Java installed with JavaFX 8 at least. Liberica Full JDK 8 or ## JVM Options (for debugging) -| Parameter | Description | -|----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `-Dhmcl.home=` | Override HMCL directory. | -| `-Dhmcl.self_integrity_check.disable=true` | Bypass the self integrity check when checking for update. | -| `-Dhmcl.bmclapi.override=` | Override API Root of BMCLAPI download provider, defaults to `https://bmclapi2.bangbang93.com`. e.g. `https://download.mcbbs.net`. | -| `-Dhmcl.font.override=` | Override font family. | -| `-Dhmcl.version.override=` | Override the version number. | -| `-Dhmcl.update_source.override=` | Override the update source. | -| `-Dhmcl.authlibinjector.location=` | Use specified authlib-injector (instead of downloading one). | -| `-Dhmcl.openjfx.repo=` | Add custom Maven repository for download OpenJFX. | -| `-Dhmcl.native.encoding=` | Override the native encoding. | -| `-Dhmcl.microsoft.auth.id=` | Override Microsoft OAuth App ID. | -| `-Dhmcl.microsoft.auth.secret=` | Override Microsoft OAuth App secret. | +| Parameter | Description | +|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `-Dhmcl.home=` | Override HMCL directory. | +| `-Dhmcl.self_integrity_check.disable=true` | Bypass the self integrity check when checking for update. | +| `-Dhmcl.bmclapi.override=` | Override API Root of BMCLAPI download provider, defaults to `https://bmclapi2.bangbang93.com`. e.g. `https://download.mcbbs.net`. | +| `-Dhmcl.font.override=` | Override font family. | +| `-Dhmcl.version.override=` | Override the version number. | +| ~~`-Dhmcl.update_source.override=`~~ | Override the update source for HMCL itself. (Deprecated, please use `hmcl.hmcl_update_source.override` instead.) | +| `-Dhmcl.hmcl_update_source.override=` | Override the update source for HMCL itself. | +| `-Dhmcl.resource_update_source.override=` | Override the update source for dynamic remote resources. | +| `-Dhmcl.authlibinjector.location=` | Use specified authlib-injector (instead of downloading one). | +| `-Dhmcl.openjfx.repo=` | Add custom Maven repository for download OpenJFX. | +| `-Dhmcl.native.encoding=` | Override the native encoding. | +| `-Dhmcl.microsoft.auth.id=` | Override Microsoft OAuth App ID. | +| `-Dhmcl.microsoft.auth.secret=` | Override Microsoft OAuth App secret. | diff --git a/README_cn.md b/README_cn.md index 4cd8331a9..e79929915 100644 --- a/README_cn.md +++ b/README_cn.md @@ -57,16 +57,18 @@ HMCL 有着强大的跨平台能力. 它不仅支持 Windows、Linux、macOS 等 ## JVM 选项 (用于调试) -| 参数 | 简介 | -|----------------------------------------------|-------------------------------------------------------------------------------------------------| -| `-Dhmcl.home=` | 覆盖 HMCL 数据文件夹. | -| `-Dhmcl.self_integrity_check.disable=true` | 检查更新时绕过本体完整性检查. | -| `-Dhmcl.bmclapi.override=` | 覆盖 BMCLAPI 的 API Root, 默认值为 `https://bmclapi2.bangbang93.com`. 例如 `https://download.mcbbs.net`. | -| `-Dhmcl.font.override=` | 覆盖字族. | -| `-Dhmcl.version.override=` | 覆盖版本号. | -| `-Dhmcl.update_source.override=` | 覆盖更新源. | -| `-Dhmcl.authlibinjector.location=` | 使用指定的 authlib-injector (而非下载一个). | -| `-Dhmcl.openjfx.repo=` | 添加用于下载 OpenJFX 的自定义 Maven 仓库 | -| `-Dhmcl.native.encoding=` | 覆盖原生编码. | -| `-Dhmcl.microsoft.auth.id=` | 覆盖 Microsoft OAuth App ID. | -| `-Dhmcl.microsoft.auth.secret=` | 覆盖 Microsoft OAuth App 密钥. | +| 参数 | 简介 | +|------------------------------------------------|-------------------------------------------------------------------------------------------------| +| `-Dhmcl.home=` | 覆盖 HMCL 数据文件夹. | +| `-Dhmcl.self_integrity_check.disable=true` | 检查更新时绕过本体完整性检查. | +| `-Dhmcl.bmclapi.override=` | 覆盖 BMCLAPI 的 API Root, 默认值为 `https://bmclapi2.bangbang93.com`. 例如 `https://download.mcbbs.net`. | +| `-Dhmcl.font.override=` | 覆盖字族. | +| `-Dhmcl.version.override=` | 覆盖版本号. | +| ~~`-Dhmcl.update_source.override=`~~ | 覆盖 HMCL 更新源(已弃用,请使用 `hmcl.hmcl_update_source.override`). | +| `-Dhmcl.hmcl_update_source.override=` | 覆盖 HMCL 更新源. | +| `-Dhmcl.resource_update_source.override=` | 覆盖动态远程资源更新源. | +| `-Dhmcl.authlibinjector.location=` | 使用指定的 authlib-injector (而非下载一个). | +| `-Dhmcl.openjfx.repo=` | 添加用于下载 OpenJFX 的自定义 Maven 仓库 | +| `-Dhmcl.native.encoding=` | 覆盖原生编码. | +| `-Dhmcl.microsoft.auth.id=` | 覆盖 Microsoft OAuth App ID. | +| `-Dhmcl.microsoft.auth.secret=` | 覆盖 Microsoft OAuth App 密钥. | diff --git a/data-json/dynamic-remote-resources-raw.json b/data-json/dynamic-remote-resources-raw.json new file mode 100644 index 000000000..a76575206 --- /dev/null +++ b/data-json/dynamic-remote-resources-raw.json @@ -0,0 +1,24 @@ +{ + "translation": { + "mod_data": { + "1": { + "urls": [ + "https://github.com/huanghongxun/HMCL/raw/javafx/HMCL/src/main/resources/assets/mod_data.txt", + "https://rgp.zkitefly.repl.co/https://github.com/huanghongxun/HMCL/raw/javafx/HMCL/src/main/resources/assets/mod_data.txt" + ], + "local_path": "HMCL/src/main/resources/assets/mod_data.txt", + "sha1": "0ae36a65a00b00176358bd6b0d3c8787b3668c23" + } + }, + "modpack_data": { + "1": { + "urls": [ + "https://github.com/huanghongxun/HMCL/blob/javafx/HMCL/src/main/resources/assets/modpack_data.txt", + "https://rgp.zkitefly.repl.co/https://github.com/huanghongxun/HMCL/blob/javafx/HMCL/src/main/resources/assets/modpack_data.txt" + ], + "local_path": "HMCL/src/main/resources/assets/modpack_data.txt", + "sha1": "b0e771db170835e1154da4c21b7417a688836162" + } + } + } +} \ No newline at end of file diff --git a/data-json/dynamic-remote-resources.json b/data-json/dynamic-remote-resources.json new file mode 100644 index 000000000..b14477ade --- /dev/null +++ b/data-json/dynamic-remote-resources.json @@ -0,0 +1 @@ +{"translation":{"mod_data":{"1":{"urls":["https://github.com/huanghongxun/HMCL/raw/javafx/HMCL/src/main/resources/assets/mod_data.txt","https://rgp.zkitefly.repl.co/https://github.com/huanghongxun/HMCL/raw/javafx/HMCL/src/main/resources/assets/mod_data.txt"],"local_path":"HMCL/src/main/resources/assets/mod_data.txt","sha1":"0ae36a65a00b00176358bd6b0d3c8787b3668c23"}},"modpack_data":{"1":{"urls":["https://github.com/huanghongxun/HMCL/blob/javafx/HMCL/src/main/resources/assets/modpack_data.txt","https://rgp.zkitefly.repl.co/https://github.com/huanghongxun/HMCL/blob/javafx/HMCL/src/main/resources/assets/modpack_data.txt"],"local_path":"HMCL/src/main/resources/assets/modpack_data.txt","sha1":"b0e771db170835e1154da4c21b7417a688836162"}}}} \ No newline at end of file