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