Enhance mod download (#2411)
* Support #2376 * Add necessary @Nullable annotations * Display different types of dependencies in different sections. * Fix checkstyle * Add I18N for different types of dependencies. * Enhance UI * Code cleanup * Enhance UI * Manually sort the result from curseforge when searching mods by name. * Render the search results from remote mod repositories in several pages. * Fix merge * Fix * Add a button which navigates to the modpack download page in the modpack installl page * Fix I18N * Render the mod loaders supported by the version in mod info page. * Fix #2104 * Enhance TwoLineListItem * Render the mod loader supported by this mod file on the ModListPage * Fix chinese searching and curseforge searching * Update I18N * Fix * Fix * Select the specific game version when clicking the 'download' button on ModListPage * Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json * Enhance :HMCL:build.gradle.kts * Revert parse_mcmod_data.py * Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError. FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden. * Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage. * Fix checkstyle * Fix * Fix * Fix * Add license for RemoteResourceManager * Remove TODO * Enhance Chinese searching * Support to decode metadata for local quilt mod. * Enhance ModManager * Fix checkstyle * Refactor * Fix * Fix * Refactor DownloadPage * Fix * Revert "Refactor DownloadPage" This reverts commit 953558da77af5a0fe3153e77cdcb9b6affa30ffa. * Refactor DownloadPage * Refactor * Fix * Fix checkstyle * Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class. * Fix * Fix * Fix * Enhance SimpleMultimap * Revert TwoLineListItem * Fix * Code cleanup * Code cleanup * Fix * Code cleanup * Add license for IModMetadataReader * Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage * Fix #2498 * Update README_cn.md * Opti ModMananger * Log a warning message when 'hmcl.update_source.override' is used. * Fix chinese searching * Enhance chinese searching. * Enhance memory usage * Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty. * Cache builtin images. * Enhance FXUtils (Make tooltip installer faster). * Fix * Fix * Fix #2560 * Fix typo * Fix remote image cache. * Fix javadoc * Fix checkstyle * Optimize FXUtils::shutdown * Fix merge * I have no idea on why the sha1 was matched. * Revert "Enhance FXUtils (Make tooltip installer faster)." This reverts commit 0a49eb2c1204e4be7dc0df3084faa59fdf9b0394. * Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland. * Modify dynamic remote resource urls. * Optimize codes with StringUtils.DynamicCommonSubsequence. * Prevent unofficial HMCL to access HMCL Resource Update URL. * Zip the dynamic-remote-resources json by Gradle automatically. * Remove unnecessary getters. --------- Co-authored-by: Burning_TNT <pangyl08@163.com“>
This commit is contained in:
@@ -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.net.URI
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@@ -7,12 +10,23 @@ import java.security.Signature
|
|||||||
import java.security.spec.PKCS8EncodedKeySpec
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("com.google.code.gson:gson:2.10.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
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 buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||||
@@ -80,6 +94,62 @@ fun attachSignature(jar: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.getByName<JavaCompile>("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") {
|
val java11 = sourceSets.create("java11") {
|
||||||
java {
|
java {
|
||||||
srcDir("src/main/java11")
|
srcDir("src/main/java11")
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import javafx.scene.transform.Rotate;
|
|||||||
import javafx.scene.transform.Scale;
|
import javafx.scene.transform.Scale;
|
||||||
import javafx.scene.transform.Translate;
|
import javafx.scene.transform.Translate;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public class SkinCanvas extends Group {
|
public class SkinCanvas extends Group {
|
||||||
|
|
||||||
public static final Image ALEX = new Image("/assets/img/skin/alex.png");
|
public static final Image ALEX = FXUtils.newBuiltinImage("/assets/img/skin/alex.png");
|
||||||
public static final Image STEVE = new Image("/assets/img/skin/steve.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_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);
|
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ import org.jackhuang.hmcl.setting.SambaException;
|
|||||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
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.CrashReporter;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||||
@@ -71,42 +73,7 @@ public final class Launcher extends Application {
|
|||||||
|
|
||||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||||
|
|
||||||
Skin.registerDefaultSkinLoader((type) -> {
|
register();
|
||||||
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<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||||
try {
|
try {
|
||||||
@@ -156,6 +123,10 @@ public final class Launcher extends Application {
|
|||||||
|
|
||||||
UpdateChecker.init();
|
UpdateChecker.init();
|
||||||
|
|
||||||
|
RemoteResourceManager.init();
|
||||||
|
|
||||||
|
RemoteResourceManager.register();
|
||||||
|
|
||||||
primaryStage.show();
|
primaryStage.show();
|
||||||
});
|
});
|
||||||
} catch (Throwable e) {
|
} 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<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
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)
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
|
||||||
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
|
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);
|
launch(Launcher.class, args);
|
||||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ public final class Metadata {
|
|||||||
public static final String TITLE = NAME + " " + VERSION;
|
public static final String TITLE = NAME + " " + VERSION;
|
||||||
public static final String FULL_TITLE = FULL_NAME + " v" + 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 CONTACT_URL = "https://docs.hmcl.net/help.html";
|
||||||
public static final String HELP_URL = "https://docs.hmcl.net";
|
public static final String HELP_URL = "https://docs.hmcl.net";
|
||||||
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
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.Logging.LOG;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
|||||||
|
|
||||||
public Image getVersionIconImage(String id) {
|
public Image getVersionIconImage(String id) {
|
||||||
if (id == null || !isLoaded())
|
if (id == null || !isLoaded())
|
||||||
return newImage("/assets/img/grass.png");
|
return newBuiltinImage("/assets/img/grass.png");
|
||||||
|
|
||||||
VersionSetting vs = getLocalVersionSettingOrCreate(id);
|
VersionSetting vs = getLocalVersionSettingOrCreate(id);
|
||||||
VersionIconType iconType = Optional.ofNullable(vs).map(VersionSetting::getVersionIcon).orElse(VersionIconType.DEFAULT);
|
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)) {
|
else if (LibraryAnalyzer.isModded(this, version)) {
|
||||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
|
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
|
||||||
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
|
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))
|
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))
|
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))
|
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))
|
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
|
||||||
return newImage("/assets/img/chicken.png");
|
return newBuiltinImage("/assets/img/chicken.png");
|
||||||
else
|
else
|
||||||
return newImage("/assets/img/furnace.png");
|
return newBuiltinImage("/assets/img/furnace.png");
|
||||||
} else
|
} else
|
||||||
return newImage("/assets/img/grass.png");
|
return newBuiltinImage("/assets/img/grass.png");
|
||||||
} else {
|
} else {
|
||||||
return newImage(iconType.getResourceUrl());
|
return newBuiltinImage(iconType.getResourceUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,44 +21,86 @@ import org.jackhuang.hmcl.mod.LocalModFile;
|
|||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||||
import org.jackhuang.hmcl.ui.versions.ModTranslations;
|
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 org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
|
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 RemoteModRepository getBackedRemoteModRepository();
|
||||||
|
|
||||||
|
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||||
String newSearchFilter;
|
if (!StringUtils.containsChinese(searchFilter)) {
|
||||||
if (StringUtils.containsChinese(searchFilter)) {
|
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||||
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
|
|
||||||
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
|
|
||||||
List<String> 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder);
|
Set<String> 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.<Pair<RemoteMod, Integer>>comparingInt(Pair::getValue).reversed()).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -51,10 +51,7 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
|||||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||||
import org.jackhuang.hmcl.util.FutureCallback;
|
import org.jackhuang.hmcl.util.*;
|
||||||
import org.jackhuang.hmcl.util.Lazy;
|
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
|
||||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
@@ -65,7 +62,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
|
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;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public final class Controllers {
|
public final class Controllers {
|
||||||
@@ -204,7 +201,7 @@ public final class Controllers {
|
|||||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||||
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
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.setTitle(Metadata.FULL_TITLE);
|
||||||
stage.initStyle(StageStyle.TRANSPARENT);
|
stage.initStyle(StageStyle.TRANSPARENT);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
@@ -357,5 +354,7 @@ public final class Controllers {
|
|||||||
stage = null;
|
stage = null;
|
||||||
scene = null;
|
scene = null;
|
||||||
onApplicationStop();
|
onApplicationStop();
|
||||||
|
|
||||||
|
FXUtils.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.countly.CrashReport;
|
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;
|
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);
|
Scene scene = new Scene(pane, 800, 480);
|
||||||
setScene(scene);
|
setScene(scene);
|
||||||
getIcons().add(newImage("/assets/img/icon.png"));
|
getIcons().add(newBuiltinImage("/assets/img/icon.png"));
|
||||||
setTitle(i18n("message.error"));
|
setTitle(i18n("message.error"));
|
||||||
|
|
||||||
setOnCloseRequest(e -> System.exit(1));
|
setOnCloseRequest(e -> System.exit(1));
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ import javafx.beans.value.WritableValue;
|
|||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.image.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.*;
|
import javafx.scene.input.*;
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
import javafx.scene.layout.ColumnConstraints;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
@@ -46,7 +46,11 @@ import javafx.scene.text.TextFlow;
|
|||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
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.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||||
import org.jackhuang.hmcl.util.Holder;
|
import org.jackhuang.hmcl.util.Holder;
|
||||||
@@ -74,11 +78,11 @@ import java.lang.ref.WeakReference;
|
|||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
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.BooleanSupplier;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -94,7 +98,24 @@ public final class FXUtils {
|
|||||||
private 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<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static void shutdown() {
|
||||||
|
for (Map.Entry<String, Path> 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) {
|
public static void runInFX(Runnable runnable) {
|
||||||
if (Platform.isFxApplicationThread()) {
|
if (Platform.isFxApplicationThread()) {
|
||||||
@@ -449,7 +470,8 @@ public final class FXUtils {
|
|||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", 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) {
|
for (String browser : linuxBrowsers) {
|
||||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||||
if (is.read() != -1) {
|
if (is.read() != -1) {
|
||||||
@@ -663,12 +685,109 @@ public final class FXUtils {
|
|||||||
* @see org.jackhuang.hmcl.util.CrashReporter
|
* @see org.jackhuang.hmcl.util.CrashReporter
|
||||||
* @see ResourceNotFoundError
|
* @see ResourceNotFoundError
|
||||||
*/
|
*/
|
||||||
public static Image newImage(String url) {
|
public static Image newBuiltinImage(String url) {
|
||||||
try {
|
return newBuiltinImage(url, 0, 0, false, false);
|
||||||
return new Image(url);
|
}
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw new ResourceNotFoundError("Cannot access image: " + url, e);
|
/**
|
||||||
|
* 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) {
|
public static JFXButton newRaisedButton(String text) {
|
||||||
@@ -678,6 +797,13 @@ public final class FXUtils {
|
|||||||
return button;
|
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<List<File>> callback) {
|
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||||
applyDragListener(node, filter, callback, null);
|
applyDragListener(node, filter, callback, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
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.ui.FXUtils.runInFX;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
@@ -116,7 +116,7 @@ public class GameCrashWindow extends Stage {
|
|||||||
setScene(new Scene(view, 800, 480));
|
setScene(new Scene(view, 800, 480));
|
||||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||||
setTitle(i18n("game.crash.title"));
|
setTitle(i18n("game.crash.title"));
|
||||||
getIcons().add(newImage("/assets/img/icon.png"));
|
getIcons().add(newBuiltinImage("/assets/img/icon.png"));
|
||||||
|
|
||||||
analyzeCrashReport();
|
analyzeCrashReport();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import javafx.scene.control.Control;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
@@ -207,7 +206,7 @@ public class InstallerItem extends Control {
|
|||||||
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
||||||
|
|
||||||
if (control.imageUrl != null) {
|
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 node = FXUtils.limitingSize(view, 32, 32);
|
||||||
node.setMouseTransparent(true);
|
node.setMouseTransparent(true);
|
||||||
node.getStyleClass().add("installer-item-image");
|
node.getStyleClass().add("installer-item-image");
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
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.Lang.thread;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
|
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
|
||||||
@@ -94,7 +94,7 @@ public final class LogWindow extends Stage {
|
|||||||
setScene(new Scene(impl, 800, 480));
|
setScene(new Scene(impl, 800, 480));
|
||||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||||
setTitle(i18n("logwindow.title"));
|
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()));
|
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import javafx.scene.web.WebEngine;
|
|||||||
import javafx.scene.web.WebView;
|
import javafx.scene.web.WebView;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
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;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.Metadata;
|
|||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
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 {
|
public class WebStage extends Stage {
|
||||||
protected final StackPane pane = new StackPane();
|
protected final StackPane pane = new StackPane();
|
||||||
@@ -44,7 +44,7 @@ public class WebStage extends Stage {
|
|||||||
public WebStage(int width, int height) {
|
public WebStage(int width, int height) {
|
||||||
setScene(new Scene(pane, width, height));
|
setScene(new Scene(pane, width, height));
|
||||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
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.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||||
webView.setContextMenuEnabled(false);
|
webView.setContextMenuEnabled(false);
|
||||||
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
|
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
|||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
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.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import javafx.geometry.Insets;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||||
@@ -38,9 +39,6 @@ public class TwoLineListItem extends VBox {
|
|||||||
private final ObservableList<String> tags = FXCollections.observableArrayList();
|
private final ObservableList<String> tags = FXCollections.observableArrayList();
|
||||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||||
|
|
||||||
private final ObservableList<Node> tagLabels;
|
|
||||||
private final AggregatedObservableList<Node> firstLineChildren;
|
|
||||||
|
|
||||||
public TwoLineListItem(String titleString, String subtitleString) {
|
public TwoLineListItem(String titleString, String subtitleString) {
|
||||||
this();
|
this();
|
||||||
|
|
||||||
@@ -58,14 +56,14 @@ public class TwoLineListItem extends VBox {
|
|||||||
lblTitle.getStyleClass().add("title");
|
lblTitle.getStyleClass().add("title");
|
||||||
lblTitle.textProperty().bind(title);
|
lblTitle.textProperty().bind(title);
|
||||||
|
|
||||||
tagLabels = MappedObservableList.create(tags, tag -> {
|
ObservableList<Node> tagLabels = MappedObservableList.create(tags, tag -> {
|
||||||
Label tagLabel = new Label();
|
Label tagLabel = new Label();
|
||||||
tagLabel.getStyleClass().add("tag");
|
tagLabel.getStyleClass().add("tag");
|
||||||
tagLabel.setText(tag);
|
tagLabel.setText(tag);
|
||||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||||
return tagLabel;
|
return tagLabel;
|
||||||
});
|
});
|
||||||
firstLineChildren = new AggregatedObservableList<>();
|
AggregatedObservableList<Node> firstLineChildren = new AggregatedObservableList<>();
|
||||||
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
|
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
|
||||||
firstLineChildren.appendList(tagLabels);
|
firstLineChildren.appendList(tagLabels);
|
||||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||||
@@ -85,6 +83,9 @@ public class TwoLineListItem extends VBox {
|
|||||||
});
|
});
|
||||||
|
|
||||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||||
|
|
||||||
|
this.minWidthProperty().set(0);
|
||||||
|
HBox.setHgrow(this, Priority.SOMETIMES);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import java.util.stream.Stream;
|
|||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
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.ui.FXUtils.onEscPressed;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
|
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
|
||||||
@@ -172,7 +172,7 @@ public class DecoratorController {
|
|||||||
image = tryLoadImage(backgroundImageUrl).orElse(null);
|
image = tryLoadImage(backgroundImageUrl).orElse(null);
|
||||||
break;
|
break;
|
||||||
case CLASSIC:
|
case CLASSIC:
|
||||||
image = newImage("/assets/img/background-classic.jpg");
|
image = newBuiltinImage("/assets/img/background-classic.jpg");
|
||||||
break;
|
break;
|
||||||
case TRANSLUCENT:
|
case TRANSLUCENT:
|
||||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
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(() -> {
|
return image.orElseGet(() -> {
|
||||||
if (defaultBackground == null)
|
if (defaultBackground == null)
|
||||||
defaultBackground = newImage("/assets/img/background.jpg");
|
defaultBackground = newBuiltinImage("/assets/img/background.jpg");
|
||||||
return defaultBackground;
|
return defaultBackground;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,8 +232,13 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
|||||||
tab.select(newGameTab);
|
tab.select(newGameTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showModDownloads() {
|
public void showModpackDownloads() {
|
||||||
|
tab.select(modpackTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadListPage showModDownloads() {
|
||||||
tab.select(modTab);
|
tab.select(modTab);
|
||||||
|
return modTab.getNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showWorldDownloads() {
|
public void showWorldDownloads() {
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
|||||||
this.getChildren().setAll(
|
this.getChildren().setAll(
|
||||||
title,
|
title,
|
||||||
createButton("local", this::onChooseLocalFile),
|
createButton("local", this::onChooseLocalFile),
|
||||||
createButton("remote", this::onChooseRemoteFile)
|
createButton("remote", this::onChooseRemoteFile),
|
||||||
|
createButton("repository", this::onChooseRepository)
|
||||||
);
|
);
|
||||||
|
|
||||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
Optional<File> 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
|
@Override
|
||||||
public void cleanup(Map<String, Object> settings) {
|
public void cleanup(Map<String, Object> settings) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
|||||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||||
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
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) {
|
private Image getIcon(VersionIconType type) {
|
||||||
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl()));
|
return icons.computeIfAbsent(type, iconType -> FXUtils.newBuiltinImage(iconType.getResourceUrl()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main;
|
|||||||
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
@@ -35,13 +34,13 @@ public class AboutPage extends StackPane {
|
|||||||
ComponentList about = new ComponentList();
|
ComponentList about = new ComponentList();
|
||||||
{
|
{
|
||||||
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
|
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.setTitle("Hello Minecraft! Launcher");
|
||||||
launcher.setSubtitle(Metadata.VERSION);
|
launcher.setSubtitle(Metadata.VERSION);
|
||||||
launcher.setExternalLink("https://hmcl.huangyuhui.net");
|
launcher.setExternalLink("https://hmcl.huangyuhui.net");
|
||||||
|
|
||||||
IconedTwoLineListItem author = new IconedTwoLineListItem();
|
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.setTitle("huanghongxun");
|
||||||
author.setSubtitle(i18n("about.author.statement"));
|
author.setSubtitle(i18n("about.author.statement"));
|
||||||
author.setExternalLink("https://space.bilibili.com/1445341");
|
author.setExternalLink("https://space.bilibili.com/1445341");
|
||||||
@@ -52,54 +51,54 @@ public class AboutPage extends StackPane {
|
|||||||
ComponentList thanks = new ComponentList();
|
ComponentList thanks = new ComponentList();
|
||||||
{
|
{
|
||||||
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
|
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
|
||||||
yushijinhun.setImage(new Image("/assets/img/yushijinhun.png"));
|
yushijinhun.setImage(FXUtils.newBuiltinImage("/assets/img/yushijinhun.png"));
|
||||||
yushijinhun.setTitle("yushijinhun");
|
yushijinhun.setTitle("yushijinhun");
|
||||||
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
|
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
|
||||||
yushijinhun.setExternalLink("https://yushi.moe/");
|
yushijinhun.setExternalLink("https://yushi.moe/");
|
||||||
|
|
||||||
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
|
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
|
||||||
bangbang93.setImage(new Image("/assets/img/bangbang93.png"));
|
bangbang93.setImage(FXUtils.newBuiltinImage("/assets/img/bangbang93.png"));
|
||||||
bangbang93.setTitle("bangbang93");
|
bangbang93.setTitle("bangbang93");
|
||||||
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
|
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
|
||||||
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
|
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
|
||||||
|
|
||||||
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
|
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
|
||||||
glavo.setImage(new Image("/assets/img/glavo.png"));
|
glavo.setImage(FXUtils.newBuiltinImage("/assets/img/glavo.png"));
|
||||||
glavo.setTitle("Glavo");
|
glavo.setTitle("Glavo");
|
||||||
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
|
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
|
||||||
glavo.setExternalLink("https://github.com/Glavo");
|
glavo.setExternalLink("https://github.com/Glavo");
|
||||||
|
|
||||||
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
|
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
|
||||||
gamerteam.setTitle("gamerteam");
|
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.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
|
||||||
gamerteam.setExternalLink("http://www.zhaisoul.com/");
|
gamerteam.setExternalLink("http://www.zhaisoul.com/");
|
||||||
|
|
||||||
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
|
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
|
||||||
redLnn.setTitle("Red_lnn");
|
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"));
|
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
|
||||||
|
|
||||||
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
|
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.setTitle(i18n("about.thanks_to.mcbbs"));
|
||||||
mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement"));
|
mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement"));
|
||||||
mcbbs.setExternalLink("https://www.mcbbs.net/");
|
mcbbs.setExternalLink("https://www.mcbbs.net/");
|
||||||
|
|
||||||
IconedTwoLineListItem mcmod = new IconedTwoLineListItem();
|
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.setTitle(i18n("about.thanks_to.mcmod"));
|
||||||
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
|
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
|
||||||
mcmod.setExternalLink("https://www.mcmod.cn/");
|
mcmod.setExternalLink("https://www.mcmod.cn/");
|
||||||
|
|
||||||
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
|
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.setTitle(i18n("about.thanks_to.contributors"));
|
||||||
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
|
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
|
||||||
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
|
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
|
||||||
|
|
||||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
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.setTitle(i18n("about.thanks_to.users"));
|
||||||
users.setSubtitle(i18n("about.thanks_to.users.statement"));
|
users.setSubtitle(i18n("about.thanks_to.users.statement"));
|
||||||
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main;
|
|||||||
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||||
@@ -43,25 +42,25 @@ public class FeedbackPage extends SpinnerPane {
|
|||||||
ComponentList community = new ComponentList();
|
ComponentList community = new ComponentList();
|
||||||
{
|
{
|
||||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
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.setTitle(i18n("feedback.qq_group"));
|
||||||
users.setSubtitle(i18n("feedback.qq_group.statement"));
|
users.setSubtitle(i18n("feedback.qq_group.statement"));
|
||||||
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||||
|
|
||||||
IconedTwoLineListItem github = new IconedTwoLineListItem();
|
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.setTitle(i18n("feedback.github"));
|
||||||
github.setSubtitle(i18n("feedback.github.statement"));
|
github.setSubtitle(i18n("feedback.github.statement"));
|
||||||
github.setExternalLink("https://github.com/huanghongxun/HMCL/issues/new/choose");
|
github.setExternalLink("https://github.com/huanghongxun/HMCL/issues/new/choose");
|
||||||
|
|
||||||
IconedTwoLineListItem discord = new IconedTwoLineListItem();
|
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.setTitle(i18n("feedback.discord"));
|
||||||
discord.setSubtitle(i18n("feedback.discord.statement"));
|
discord.setSubtitle(i18n("feedback.discord.statement"));
|
||||||
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
||||||
|
|
||||||
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
|
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.setTitle(i18n("feedback.kookapp"));
|
||||||
kookapp.setSubtitle(i18n("feedback.kookapp.statement"));
|
kookapp.setSubtitle(i18n("feedback.kookapp.statement"));
|
||||||
kookapp.setExternalLink("https://kook.top/Kx7n3t");
|
kookapp.setExternalLink("https://kook.top/Kx7n3t");
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
|||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
import org.jackhuang.hmcl.upgrade.hmcl.RemoteVersion;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateHandler;
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
@@ -105,7 +105,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
|||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
Label titleLabel = new Label(Metadata.FULL_TITLE);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
|||||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||||
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
||||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
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.TaskCancellationAction;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ import org.jackhuang.hmcl.setting.Settings;
|
|||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
import org.jackhuang.hmcl.upgrade.hmcl.RemoteVersion;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChannel;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateHandler;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import javafx.scene.control.Control;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import org.jackhuang.hmcl.game.GameVersion;
|
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 BooleanProperty failed = new SimpleBooleanProperty(false);
|
||||||
private final boolean versionSelection;
|
private final boolean versionSelection;
|
||||||
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
|
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
|
||||||
|
private final IntegerProperty pageOffset = new SimpleIntegerProperty(0);
|
||||||
|
private final IntegerProperty pageCount = new SimpleIntegerProperty(-1);
|
||||||
private final ListProperty<RemoteMod> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
private final ListProperty<RemoteMod> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||||
private final ObservableList<String> versions = FXCollections.observableArrayList();
|
private final ObservableList<String> versions = FXCollections.observableArrayList();
|
||||||
private final StringProperty selectedVersion = new SimpleStringProperty();
|
private final StringProperty selectedVersion = new SimpleStringProperty();
|
||||||
@@ -154,6 +155,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
this.loading.set(loading);
|
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) {
|
public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, RemoteModRepository.SortType sort) {
|
||||||
retrySearch = null;
|
retrySearch = null;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -178,10 +183,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
items.setAll(result.collect(Collectors.toList()));
|
items.setAll(result.getResults().collect(Collectors.toList()));
|
||||||
|
pageCount.set(result.getTotalPages());
|
||||||
failed.set(false);
|
failed.set(false);
|
||||||
} else {
|
} else {
|
||||||
failed.set(true);
|
failed.set(true);
|
||||||
|
pageCount.set(-1);
|
||||||
retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort);
|
retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort);
|
||||||
}
|
}
|
||||||
}).executor(true);
|
}).executor(true);
|
||||||
@@ -221,8 +228,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||||
private final AggregatedObservableList<Node> actions = new AggregatedObservableList<>();
|
|
||||||
|
|
||||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||||
super(control);
|
super(control);
|
||||||
|
|
||||||
@@ -341,25 +346,90 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
sortComboBox.getSelectionModel().select(0);
|
sortComboBox.getSelectionModel().select(0);
|
||||||
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
|
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
|
||||||
|
|
||||||
JFXButton searchButton = FXUtils.newRaisedButton(i18n("search"));
|
StringProperty previousSearchFilter = new SimpleStringProperty(this, "Previous Seach Filter", "");
|
||||||
ObservableList<Node> last = FXCollections.observableArrayList(searchButton);
|
EventHandler<ActionEvent> searchAction = e -> {
|
||||||
HBox searchBox = new HBox(8);
|
if (!previousSearchFilter.get().equals(nameField.getText())) {
|
||||||
actions.appendList(control.actions);
|
control.pageOffset.set(0);
|
||||||
actions.appendList(last);
|
}
|
||||||
Bindings.bindContent(searchBox.getChildren(), actions.getAggregatedList());
|
|
||||||
GridPane.setColumnSpan(searchBox, 4);
|
previousSearchFilter.set(nameField.getText());
|
||||||
searchBox.setAlignment(Pos.CENTER_RIGHT);
|
getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(),
|
||||||
searchPane.addRow(rowIndex++, searchBox);
|
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<Node> 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<ActionEvent> 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);
|
nameField.setOnAction(searchAction);
|
||||||
gameVersionField.setOnAction(searchAction);
|
gameVersionField.setOnAction(searchAction);
|
||||||
categoryComboBox.setOnAction(searchAction);
|
categoryComboBox.setOnAction(searchAction);
|
||||||
@@ -402,6 +472,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
pane.getChildren().add(container);
|
pane.getChildren().add(container);
|
||||||
|
|
||||||
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||||
|
HBox.setHgrow(content, Priority.ALWAYS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -415,7 +486,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
|||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
|
import com.jfoenix.controls.JFXDialogLayout;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||||
@@ -27,14 +27,12 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Control;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.ScrollPane;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.control.SkinBase;
|
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.stage.FileChooser;
|
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.ModLoaderType;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
import org.jackhuang.hmcl.mod.ModManager;
|
||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
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.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
import org.jackhuang.hmcl.ui.construct.*;
|
||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
import org.jackhuang.hmcl.util.*;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -65,8 +62,8 @@ import java.util.*;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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.ui.FXUtils.runInFX;
|
||||||
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public class DownloadPage extends Control implements DecoratorPage {
|
public class DownloadPage extends Control implements DecoratorPage {
|
||||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||||
@@ -81,8 +78,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
private final DownloadCallback callback;
|
private final DownloadCallback callback;
|
||||||
private final DownloadListPage page;
|
private final DownloadListPage page;
|
||||||
|
|
||||||
private List<RemoteMod> dependencies;
|
private SimpleMultimap<String, RemoteMod.Version, List<RemoteMod.Version>> versions;
|
||||||
private SimpleMultimap<String, RemoteMod.Version> versions;
|
|
||||||
|
|
||||||
public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
|
public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
@@ -105,10 +101,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setFailed(false);
|
setFailed(false);
|
||||||
|
|
||||||
Task.allOf(
|
Task.supplyAsync(() -> {
|
||||||
Task.supplyAsync(() -> addon.getData().loadDependencies(repository)),
|
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
||||||
Task.supplyAsync(() -> {
|
|
||||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
|
||||||
// if (StringUtils.isNotBlank(version.getVersion())) {
|
// if (StringUtils.isNotBlank(version.getVersion())) {
|
||||||
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||||
// if (gameVersion.isPresent()) {
|
// if (gameVersion.isPresent()) {
|
||||||
@@ -116,30 +110,23 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
|
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
return sortVersions(versions);
|
return sortVersions(versions);
|
||||||
}))
|
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
if (exception == null) {
|
||||||
if (exception == null) {
|
this.versions = result;
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<RemoteMod> dependencies = (List<RemoteMod>) result.get(0);
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
SimpleMultimap<String, RemoteMod.Version> versions = (SimpleMultimap<String, RemoteMod.Version>) result.get(1);
|
|
||||||
|
|
||||||
this.dependencies = dependencies;
|
loaded.set(true);
|
||||||
this.versions = versions;
|
setFailed(false);
|
||||||
|
} else {
|
||||||
loaded.set(true);
|
setFailed(true);
|
||||||
setFailed(false);
|
}
|
||||||
} else {
|
setLoading(false);
|
||||||
setFailed(true);
|
}).start();
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SimpleMultimap<String, RemoteMod.Version> sortVersions(Stream<RemoteMod.Version> versions) {
|
private SimpleMultimap<String, RemoteMod.Version, List<RemoteMod.Version>> sortVersions(Stream<RemoteMod.Version> versions) {
|
||||||
SimpleMultimap<String, RemoteMod.Version> classifiedVersions
|
SimpleMultimap<String, RemoteMod.Version, List<RemoteMod.Version>> classifiedVersions
|
||||||
= new SimpleMultimap<String, RemoteMod.Version>(HashMap::new, ArrayList::new);
|
= new SimpleMultimap<>(HashMap::new, ArrayList::new);
|
||||||
versions.forEach(version -> {
|
versions.forEach(version -> {
|
||||||
for (String gameVersion : version.getGameVersions()) {
|
for (String gameVersion : version.getGameVersions()) {
|
||||||
classifiedVersions.put(gameVersion, version);
|
classifiedVersions.put(gameVersion, version);
|
||||||
@@ -147,7 +134,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (String gameVersion : classifiedVersions.keys()) {
|
for (String gameVersion : classifiedVersions.keys()) {
|
||||||
List<RemoteMod.Version> versionList = (List<RemoteMod.Version>) classifiedVersions.get(gameVersion);
|
List<RemoteMod.Version> versionList = classifiedVersions.get(gameVersion);
|
||||||
versionList.sort(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed());
|
versionList.sort(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed());
|
||||||
}
|
}
|
||||||
return classifiedVersions;
|
return classifiedVersions;
|
||||||
@@ -246,7 +233,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
{
|
{
|
||||||
ImageView imageView = new ImageView();
|
ImageView imageView = new ImageView();
|
||||||
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
|
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));
|
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"));
|
JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod"));
|
||||||
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
|
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
|
||||||
descriptionPane.getChildren().add(openMcmodButton);
|
descriptionPane.getChildren().add(openMcmodButton);
|
||||||
|
openMcmodButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||||
runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod")));
|
runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod")));
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
|
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
|
||||||
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
|
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
|
||||||
openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs()));
|
openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs()));
|
||||||
descriptionPane.getChildren().add(openMcbbsButton);
|
descriptionPane.getChildren().add(openMcbbsButton);
|
||||||
|
openMcbbsButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||||
runInFX(() -> FXUtils.installFastTooltip(openMcbbsButton, i18n("mods.mcbbs")));
|
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());
|
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
|
||||||
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
|
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
|
||||||
descriptionPane.getChildren().add(openUrlButton);
|
descriptionPane.getChildren().add(openUrlButton);
|
||||||
|
openUrlButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||||
runInFX(() -> FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage()));
|
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();
|
SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
VBox.setVgrow(spinnerPane, Priority.ALWAYS);
|
VBox.setVgrow(spinnerPane, Priority.ALWAYS);
|
||||||
pane.getChildren().add(spinnerPane);
|
pane.getChildren().add(spinnerPane);
|
||||||
@@ -324,6 +291,23 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
||||||
if (control.versions == null) return;
|
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<ModLoaderType> 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()
|
for (String gameVersion : control.versions.keys().stream()
|
||||||
.sorted(VersionNumber.VERSION_COMPARATOR.reversed())
|
.sorted(VersionNumber.VERSION_COMPARATOR.reversed())
|
||||||
.collect(Collectors.toList())) {
|
.collect(Collectors.toList())) {
|
||||||
@@ -332,7 +316,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
.map(version -> new ModItem(version, control))
|
.map(version -> new ModItem(version, control))
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
sublist.getStyleClass().add("no-padding");
|
sublist.getStyleClass().add("no-padding");
|
||||||
sublist.setTitle(gameVersion);
|
sublist.setTitle("Minecraft " + gameVersion);
|
||||||
|
|
||||||
list.getContent().add(sublist);
|
list.getContent().add(sublist);
|
||||||
}
|
}
|
||||||
@@ -344,10 +328,19 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class DependencyModItem extends StackPane {
|
private static final class DependencyModItem extends StackPane {
|
||||||
|
public static final EnumMap<RemoteMod.DependencyType, String> 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) {
|
DependencyModItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, DownloadCallback callback) {
|
||||||
HBox pane = new HBox(8);
|
HBox pane = new HBox(8);
|
||||||
pane.setPadding(new Insets(8));
|
pane.setPadding(new Insets(0, 8, 0, 8));
|
||||||
pane.setAlignment(Pos.CENTER_LEFT);
|
pane.setAlignment(Pos.CENTER_LEFT);
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
HBox.setHgrow(content, Priority.ALWAYS);
|
HBox.setHgrow(content, Priority.ALWAYS);
|
||||||
@@ -366,7 +359,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
|||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
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 {
|
private static final class ModItem extends StackPane {
|
||||||
|
|
||||||
ModItem(RemoteMod.Version dataItem, DownloadPage selfPage) {
|
ModItem(RemoteMod.Version dataItem, DownloadPage selfPage) {
|
||||||
HBox pane = new HBox(8);
|
VBox pane = new VBox(8);
|
||||||
pane.setPadding(new Insets(8));
|
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||||
pane.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
{
|
||||||
StackPane graphicPane = new StackPane();
|
HBox descPane = new HBox(8);
|
||||||
JFXButton saveAsButton = new JFXButton();
|
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);
|
RipplerContainer container = new RipplerContainer(pane);
|
||||||
container.setOnMouseClicked(e -> selfPage.download(dataItem));
|
container.setOnMouseClicked(e -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
|
||||||
getChildren().setAll(container);
|
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
|
// Workaround for https://github.com/huanghongxun/HMCL/issues/2129
|
||||||
this.setMinHeight(50);
|
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<RemoteMod.DependencyType, List<Node>> 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<Node> 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());
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
|
||||||
|
|
||||||
public interface DownloadCallback {
|
public interface DownloadCallback {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import org.jackhuang.hmcl.util.Pair;
|
|||||||
|
|
||||||
import java.util.function.Consumer;
|
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;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public class GameAdvancedListItem extends AdvancedListItem {
|
public class GameAdvancedListItem extends AdvancedListItem {
|
||||||
@@ -72,7 +72,7 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
|||||||
Tooltip.uninstall(this,tooltip);
|
Tooltip.uninstall(this,tooltip);
|
||||||
setTitle(i18n("version.empty"));
|
setTitle(i18n("version.empty"));
|
||||||
setSubtitle(i18n("version.empty.add"));
|
setSubtitle(i18n("version.empty.add"));
|
||||||
imageView.setImage(newImage("/assets/img/grass.png"));
|
imageView.setImage(newBuiltinImage("/assets/img/grass.png"));
|
||||||
tooltip.setText("");
|
tooltip.setText("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public Type getType() {
|
public Type getType() {
|
||||||
return Type.MOD;
|
return Type.MOD;
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void download() {
|
public void download() {
|
||||||
Controllers.getDownloadPage().showModDownloads();
|
Controllers.getDownloadPage().showModDownloads().selectVersion(versionId);
|
||||||
Controllers.navigate(Controllers.getDownloadPage());
|
Controllers.navigate(Controllers.getDownloadPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ import javafx.scene.image.ImageView;
|
|||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
import org.jackhuang.hmcl.mod.*;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
|
||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
|
||||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
|
||||||
import org.jackhuang.hmcl.mod.curse.CurseAddon;
|
|
||||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
@@ -64,7 +60,6 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -307,7 +302,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
imageView.setImage(new Image(stream, 40, 40, true, true));
|
imageView.setImage(new Image(stream, 40, 40, true, true));
|
||||||
} else {
|
} 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();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -326,26 +321,32 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
setBody(description);
|
setBody(description);
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||||
Lang.<Pair<String, Pair<RemoteModRepository, Function<RemoteMod.Version, String>>>>immutableListOf(
|
Lang.<Pair<String, RemoteModRepository>>immutableListOf(
|
||||||
pair("mods.curseforge", pair(
|
pair("mods.curseforge", CurseForgeRemoteModRepository.MODS),
|
||||||
CurseForgeRemoteModRepository.MODS,
|
pair("mods.modrinth", ModrinthRemoteModRepository.MODS)
|
||||||
(remoteVersion) -> Integer.toString(((CurseAddon.LatestFile) remoteVersion.getSelf()).getModId())
|
|
||||||
)),
|
|
||||||
pair("mods.modrinth", pair(
|
|
||||||
ModrinthRemoteModRepository.MODS,
|
|
||||||
(remoteVersion) -> ((ModrinthRemoteModRepository.ProjectVersion) remoteVersion.getSelf()).getProjectId()
|
|
||||||
))
|
|
||||||
).forEach(item -> {
|
).forEach(item -> {
|
||||||
String text = item.getKey();
|
String text = item.getKey();
|
||||||
RemoteModRepository remoteModRepository = item.getValue().getKey();
|
RemoteModRepository remoteModRepository = item.getValue();
|
||||||
Function<RemoteMod.Version, String> projectIDProvider = item.getValue().getValue();
|
|
||||||
|
|
||||||
JFXHyperlink button = new JFXHyperlink(i18n(text));
|
JFXHyperlink button = new JFXHyperlink(i18n(text));
|
||||||
Task.runAsync(() -> {
|
Task.runAsync(() -> {
|
||||||
Optional<RemoteMod.Version> versionOptional = remoteModRepository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
|
Optional<RemoteMod.Version> versionOptional = remoteModRepository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
|
||||||
if (versionOptional.isPresent()) {
|
if (versionOptional.isPresent()) {
|
||||||
RemoteMod remoteMod = remoteModRepository.getModById(projectIDProvider.apply(versionOptional.get()));
|
RemoteMod remoteMod = remoteModRepository.getModById(versionOptional.get().getModid());
|
||||||
FXUtils.runInFX(() -> {
|
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 -> {
|
button.setOnAction(e -> {
|
||||||
fireEvent(new DialogCloseEvent());
|
fireEvent(new DialogCloseEvent());
|
||||||
Controllers.navigate(new DownloadPage(
|
Controllers.navigate(new DownloadPage(
|
||||||
@@ -455,10 +456,12 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||||
if (empty) return;
|
if (empty) return;
|
||||||
content.setTitle(dataItem.getTitle());
|
content.setTitle(dataItem.getTitle());
|
||||||
if (dataItem.getMod() != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA) {
|
content.getTags().clear();
|
||||||
content.getTags().setAll(dataItem.getMod().getDisplayName());
|
content.getTags().add(dataItem.getModInfo().getModLoaderType().getLoaderName());
|
||||||
} else {
|
if (dataItem.getMod() != null) {
|
||||||
content.getTags().clear();
|
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA) {
|
||||||
|
content.getTags().add(dataItem.getMod().getDisplayName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content.setSubtitle(dataItem.getSubtitle());
|
content.setSubtitle(dataItem.getSubtitle());
|
||||||
if (booleanProperty != null) {
|
if (booleanProperty != null) {
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||||
|
import org.jackhuang.hmcl.upgrade.resource.RemoteResourceManager;
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -36,19 +38,19 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
|||||||
* @see <a href="https://www.mcmod.cn">mcmod.cn</a>
|
* @see <a href="https://www.mcmod.cn">mcmod.cn</a>
|
||||||
*/
|
*/
|
||||||
public enum ModTranslations {
|
public enum ModTranslations {
|
||||||
MOD("/assets/mod_data.txt") {
|
MOD("/assets/mod_data.txt", "translation", "mod_data", "1") {
|
||||||
@Override
|
@Override
|
||||||
public String getMcmodUrl(Mod mod) {
|
public String getMcmodUrl(Mod mod) {
|
||||||
return String.format("https://www.mcmod.cn/class/%s.html", mod.getMcmod());
|
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
|
@Override
|
||||||
public String getMcmodUrl(Mod mod) {
|
public String getMcmodUrl(Mod mod) {
|
||||||
return String.format("https://www.mcmod.cn/modpack/%s.html", mod.getMcmod());
|
return String.format("https://www.mcmod.cn/modpack/%s.html", mod.getMcmod());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EMPTY("") {
|
EMPTY("", "", "", "") {
|
||||||
@Override
|
@Override
|
||||||
public String getMcmodUrl(Mod mod) {
|
public String getMcmodUrl(Mod mod) {
|
||||||
return "";
|
return "";
|
||||||
@@ -66,15 +68,18 @@ public enum ModTranslations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String resourceName;
|
private final String defaultResourceName;
|
||||||
|
private final RemoteResourceManager.RemoteResourceKey remoteResourceKey;
|
||||||
private List<Mod> mods;
|
private List<Mod> mods;
|
||||||
private Map<String, Mod> modIdMap; // mod id -> mod
|
private Map<String, Mod> modIdMap; // mod id -> mod
|
||||||
private Map<String, Mod> curseForgeMap; // curseforge id -> mod
|
private Map<String, Mod> curseForgeMap; // curseforge id -> mod
|
||||||
private List<Pair<String, Mod>> keywords;
|
private List<Pair<String, Mod>> keywords;
|
||||||
private int maxKeywordLength = -1;
|
private int maxKeywordLength = -1;
|
||||||
|
|
||||||
ModTranslations(String resourceName) {
|
ModTranslations(String defaultResourceName, String namespace, String name, String version) {
|
||||||
this.resourceName = resourceName;
|
this.defaultResourceName = defaultResourceName;
|
||||||
|
|
||||||
|
remoteResourceKey = RemoteResourceManager.get(namespace, name, version, () -> ModTranslations.class.getResourceAsStream(defaultResourceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -96,9 +101,9 @@ public enum ModTranslations {
|
|||||||
public List<Mod> searchMod(String query) {
|
public List<Mod> searchMod(String query) {
|
||||||
if (!loadKeywords()) return Collections.emptyList();
|
if (!loadKeywords()) return Collections.emptyList();
|
||||||
|
|
||||||
StringBuilder newQuery = query.chars()
|
StringBuilder newQuery = ((CharSequence) query).chars()
|
||||||
.filter(ch -> !Character.isSpaceChar(ch))
|
.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();
|
query = newQuery.toString();
|
||||||
|
|
||||||
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength);
|
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength);
|
||||||
@@ -115,18 +120,24 @@ public enum ModTranslations {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean loadFromResource() {
|
private boolean loaded() {
|
||||||
if (mods != null) return true;
|
if (mods != null) return true;
|
||||||
if (StringUtils.isBlank(resourceName)) {
|
if (StringUtils.isBlank(defaultResourceName)) {
|
||||||
mods = Collections.emptyList();
|
mods = Collections.emptyList();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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());
|
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.log(Level.WARNING, "Failed to load " + resourceName, e);
|
LOG.log(Level.WARNING, "Failed to load " + defaultResourceName, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +148,7 @@ public enum ModTranslations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mods == null) {
|
if (mods == null) {
|
||||||
if (!loadFromResource()) return false;
|
if (!loaded()) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
curseForgeMap = new HashMap<>();
|
curseForgeMap = new HashMap<>();
|
||||||
@@ -155,7 +166,7 @@ public enum ModTranslations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mods == null) {
|
if (mods == null) {
|
||||||
if (!loadFromResource()) return false;
|
if (!loaded()) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
modIdMap = new HashMap<>();
|
modIdMap = new HashMap<>();
|
||||||
@@ -175,7 +186,7 @@ public enum ModTranslations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mods == null) {
|
if (mods == null) {
|
||||||
if (!loadFromResource()) return false;
|
if (!loaded()) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
keywords = new ArrayList<>();
|
keywords = new ArrayList<>();
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public Type getType() {
|
public Type getType() {
|
||||||
return Type.MODPACK;
|
return Type.MODPACK;
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public Type getType() {
|
public Type getType() {
|
||||||
return Type.MOD;
|
return Type.MOD;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.FlowPane;
|
import javafx.scene.layout.FlowPane;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
@@ -104,7 +103,7 @@ public class VersionIconDialog extends DialogPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Node createIcon(VersionIconType type) {
|
private Node createIcon(VersionIconType type) {
|
||||||
ImageView imageView = new ImageView(new Image(type.getResourceUrl()));
|
ImageView imageView = new ImageView(FXUtils.newBuiltinImage(type.getResourceUrl()));
|
||||||
imageView.setMouseTransparent(true);
|
imageView.setMouseTransparent(true);
|
||||||
RipplerContainer container = new RipplerContainer(imageView);
|
RipplerContainer container = new RipplerContainer(imageView);
|
||||||
FXUtils.setLimitWidth(container, 36);
|
FXUtils.setLimitWidth(container, 36);
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import javafx.geometry.Insets;
|
|||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
@@ -146,7 +145,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
rootPane.getChildren().add(iconPickerItemWrapper);
|
rootPane.getChildren().add(iconPickerItemWrapper);
|
||||||
|
|
||||||
iconPickerItem = new ImagePickerItem();
|
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.setTitle(i18n("settings.icon"));
|
||||||
iconPickerItem.setOnSelectButtonClicked(e -> onExploreIcon());
|
iconPickerItem.setOnSelectButtonClicked(e -> onExploreIcon());
|
||||||
iconPickerItem.setOnDeleteButtonClicked(e -> onDeleteIcon());
|
iconPickerItem.setOnDeleteButtonClicked(e -> onDeleteIcon());
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.util.DigestUtils;
|
import org.jackhuang.hmcl.util.DigestUtils;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
@@ -85,7 +85,7 @@ public final class UpdateChecker {
|
|||||||
throw new IOException("Self verification failed");
|
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("version", Metadata.VERSION),
|
||||||
pair("channel", channel.channelName)));
|
pair("channel", channel.channelName)));
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade.hmcl;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<InputStream, IOException> localResourceSupplier;
|
||||||
|
private String localResourceSha1 = null;
|
||||||
|
|
||||||
|
public RemoteResourceKey(String namespace, String name, String version, ExceptionalSupplier<InputStream, IOException> 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<String, Map<String, Map<String, RemoteResource>>> remoteResources = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final Map<String, RemoteResourceKey> keys = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
Task.<Map<String, Map<String, Map<String, RemoteResource>>>>supplyAsync(() ->
|
||||||
|
IntegrityChecker.isSelfVerified() ? HttpRequest.GET(Metadata.RESOURCE_UPDATE_URL).getJson(
|
||||||
|
new TypeToken<Map<String, Map<String, Map<String, RemoteResource>>>>() {
|
||||||
|
}.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<InputStream, IOException> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,8 +23,8 @@ import javafx.scene.control.Alert.AlertType;
|
|||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.countly.CrashReport;
|
import org.jackhuang.hmcl.countly.CrashReport;
|
||||||
import org.jackhuang.hmcl.ui.CrashWindow;
|
import org.jackhuang.hmcl.ui.CrashWindow;
|
||||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
import org.jackhuang.hmcl.upgrade.hmcl.IntegrityChecker;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@@ -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.local.detail=You can drag and drop the modpack file here
|
||||||
modpack.choose.remote=Download from the Internet
|
modpack.choose.remote=Download from the Internet
|
||||||
modpack.choose.remote.detail=A direct download link to the remote modpack file is required
|
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.choose.remote.tooltip=Please enter your modpack URL
|
||||||
modpack.completion=Downloading dependencies
|
modpack.completion=Downloading dependencies
|
||||||
modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported.
|
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.check_updates.update=Update
|
||||||
mods.choose_mod=Choose a mod
|
mods.choose_mod=Choose a mod
|
||||||
mods.curseforge=CurseForge
|
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.disable=Disable
|
||||||
mods.download=Mod Download
|
mods.download=Mod Download
|
||||||
mods.download.title=Mod Download - %1s
|
mods.download.title=Mod Download - %1s
|
||||||
|
mods.download.recommend=Recommended Mod Version - Minecraft %1s
|
||||||
mods.enable=Enable
|
mods.enable=Enable
|
||||||
mods.manage=Manage Mods
|
mods.manage=Manage Mods
|
||||||
mods.mcbbs=MCBBS
|
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.hint.english=Only English is supported
|
||||||
search.enter=Enter text here
|
search.enter=Enter text here
|
||||||
search.sort=Sort By
|
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=Choose
|
||||||
selector.choose_file=Select a file
|
selector.choose_file=Select a file
|
||||||
|
|||||||
@@ -802,7 +802,6 @@ mods.check_updates.target_version=Versión de destino
|
|||||||
mods.check_updates.update=Actualización
|
mods.check_updates.update=Actualización
|
||||||
mods.choose_mod=Elige un mod
|
mods.choose_mod=Elige un mod
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
mods.dependencies=Dependencias
|
|
||||||
mods.disable=Desactivar
|
mods.disable=Desactivar
|
||||||
mods.download=Descarga de mods
|
mods.download=Descarga de mods
|
||||||
mods.download.title=Descarga de mods - %1s
|
mods.download.title=Descarga de mods - %1s
|
||||||
|
|||||||
@@ -634,7 +634,6 @@ mods.check_updates.target_version=Target
|
|||||||
mods.check_updates.update=更新
|
mods.check_updates.update=更新
|
||||||
mods.choose_mod=modを選択してください
|
mods.choose_mod=modを選択してください
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
mods.dependencies=Dependencies
|
|
||||||
mods.disable=無効にする
|
mods.disable=無効にする
|
||||||
mods.download=Modのダウンロード
|
mods.download=Modのダウンロード
|
||||||
mods.download.title=Modダウンロード- %1s
|
mods.download.title=Modダウンロード- %1s
|
||||||
|
|||||||
@@ -639,7 +639,6 @@ mods.check_updates.target_version=Цель
|
|||||||
mods.check_updates.update=Обновить
|
mods.check_updates.update=Обновить
|
||||||
mods.choose_mod=Выбрать свои моды
|
mods.choose_mod=Выбрать свои моды
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
mods.dependencies=Зависимости
|
|
||||||
mods.disable=Отключить
|
mods.disable=Отключить
|
||||||
mods.download=Скачивание модов
|
mods.download=Скачивание модов
|
||||||
mods.download.title=Скачивание модов - %1s
|
mods.download.title=Скачивание модов - %1s
|
||||||
|
|||||||
@@ -617,6 +617,8 @@ modpack.choose.local=匯入本機模組包檔案
|
|||||||
modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以安裝
|
modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以安裝
|
||||||
modpack.choose.remote=從網路下載模組包
|
modpack.choose.remote=從網路下載模組包
|
||||||
modpack.choose.remote.detail=需要提供模組包的下載連結
|
modpack.choose.remote.detail=需要提供模組包的下載連結
|
||||||
|
modpack.choose.repository= 從 Curseforge / Modrinth 下載整合包
|
||||||
|
modpack.choose.repository.detail=下載后記得回到這個界面,把整合包拖進來哦
|
||||||
modpack.choose.remote.tooltip=要下載的模組包的連結
|
modpack.choose.remote.tooltip=要下載的模組包的連結
|
||||||
modpack.completion=下載模組包相關檔案
|
modpack.completion=下載模組包相關檔案
|
||||||
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。
|
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。
|
||||||
@@ -729,8 +731,8 @@ mods=模組
|
|||||||
mods.add=新增模組
|
mods.add=新增模組
|
||||||
mods.add.failed=新增模組 %s 失敗。
|
mods.add.failed=新增模組 %s 失敗。
|
||||||
mods.add.success=成功新增模組 %s。
|
mods.add.success=成功新增模組 %s。
|
||||||
mods.broken_dependency.title=損壞前置模組
|
mods.broken_dependency.title=損壞的前置模組
|
||||||
mods.broken_dependency.desc=該前置模組曾經在該模組倉庫上存在過,但現在被刪除了。換個下載源試試吧。
|
mods.broken_dependency.desc=該前置模組曾經在該模組倉庫上存在過,但現在被刪除了,換個下載源試試吧。
|
||||||
mods.category=類別
|
mods.category=類別
|
||||||
mods.check_updates=檢查模組更新
|
mods.check_updates=檢查模組更新
|
||||||
mods.check_updates.current_version=當前版本
|
mods.check_updates.current_version=當前版本
|
||||||
@@ -742,10 +744,17 @@ mods.check_updates.target_version=目標版本
|
|||||||
mods.check_updates.update=更新
|
mods.check_updates.update=更新
|
||||||
mods.choose_mod=選擇模組
|
mods.choose_mod=選擇模組
|
||||||
mods.curseforge=CurseForge
|
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.disable=停用
|
||||||
mods.download=模組下載
|
mods.download=模組下載
|
||||||
mods.download.title=模組下載 - %1s
|
mods.download.title=模組下載 - %1s
|
||||||
|
mods.download.recommend=推薦版本 - Minecraft %1s
|
||||||
mods.enable=啟用
|
mods.enable=啟用
|
||||||
mods.manage=模組管理
|
mods.manage=模組管理
|
||||||
mods.mcbbs=MCBBS
|
mods.mcbbs=MCBBS
|
||||||
@@ -846,6 +855,11 @@ search.hint.chinese=支援中英文搜尋
|
|||||||
search.hint.english=僅支援英文搜尋
|
search.hint.english=僅支援英文搜尋
|
||||||
search.enter=請在此處輸入
|
search.enter=請在此處輸入
|
||||||
search.sort=排序
|
search.sort=排序
|
||||||
|
search.first_page=第一頁
|
||||||
|
search.previous_page=上一頁
|
||||||
|
search.next_page=下一頁
|
||||||
|
search.last_page=最後一頁
|
||||||
|
search.page_n=%d / %s
|
||||||
|
|
||||||
selector.choose=選擇
|
selector.choose=選擇
|
||||||
selector.choose_file=選擇檔案
|
selector.choose_file=選擇檔案
|
||||||
|
|||||||
@@ -616,6 +616,8 @@ modpack.choose.local=导入本地整合包文件
|
|||||||
modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以安装
|
modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以安装
|
||||||
modpack.choose.remote=从互联网下载整合包
|
modpack.choose.remote=从互联网下载整合包
|
||||||
modpack.choose.remote.detail=需要提供整合包的下载链接
|
modpack.choose.remote.detail=需要提供整合包的下载链接
|
||||||
|
modpack.choose.repository=从 Curseforge / Modrinth 下载整合包
|
||||||
|
modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦
|
||||||
modpack.choose.remote.tooltip=要下载的整合包的链接
|
modpack.choose.remote.tooltip=要下载的整合包的链接
|
||||||
modpack.completion=下载整合包相关文件
|
modpack.completion=下载整合包相关文件
|
||||||
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图)
|
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图)
|
||||||
@@ -728,7 +730,7 @@ mods=模组
|
|||||||
mods.add=添加模组
|
mods.add=添加模组
|
||||||
mods.add.failed=添加模组 %s 失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
mods.add.failed=添加模组 %s 失败。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||||
mods.add.success=成功添加模组 %s。
|
mods.add.success=成功添加模组 %s。
|
||||||
mods.broken_dependency.title=损坏前置模组
|
mods.broken_dependency.title=损坏的前置模组
|
||||||
mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。
|
mods.broken_dependency.desc=该前置模组曾经在该模组仓库上存在过,但现在被删除了。换个下载源试试吧。
|
||||||
mods.category=类别
|
mods.category=类别
|
||||||
mods.check_updates=检查模组更新
|
mods.check_updates=检查模组更新
|
||||||
@@ -741,10 +743,17 @@ mods.check_updates.target_version=目标版本
|
|||||||
mods.check_updates.update=更新
|
mods.check_updates.update=更新
|
||||||
mods.choose_mod=选择模组
|
mods.choose_mod=选择模组
|
||||||
mods.curseforge=CurseForge
|
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.disable=禁用
|
||||||
mods.download=模组下载
|
mods.download=模组下载
|
||||||
mods.download.title=模组下载 - %1s
|
mods.download.title=模组下载 - %1s
|
||||||
|
mods.download.recommend=推荐版本 - Minecraft %1s
|
||||||
mods.enable=启用
|
mods.enable=启用
|
||||||
mods.manage=模组管理
|
mods.manage=模组管理
|
||||||
mods.mcbbs=MCBBS
|
mods.mcbbs=MCBBS
|
||||||
@@ -845,6 +854,11 @@ search.hint.chinese=支持中英文搜索
|
|||||||
search.hint.english=仅支持英文搜索
|
search.hint.english=仅支持英文搜索
|
||||||
search.enter=可在此处输入
|
search.enter=可在此处输入
|
||||||
search.sort=排序
|
search.sort=排序
|
||||||
|
search.first_page=第一页
|
||||||
|
search.previous_page=上一页
|
||||||
|
search.next_page=下一页
|
||||||
|
search.last_page=最后一页
|
||||||
|
search.page_n=%d / %s
|
||||||
|
|
||||||
selector.choose=选择
|
selector.choose=选择
|
||||||
selector.choose_file=选择文件
|
selector.choose_file=选择文件
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download;
|
|||||||
import org.jackhuang.hmcl.game.Library;
|
import org.jackhuang.hmcl.game.Library;
|
||||||
import org.jackhuang.hmcl.game.Version;
|
import org.jackhuang.hmcl.game.Version;
|
||||||
import org.jackhuang.hmcl.game.VersionProvider;
|
import org.jackhuang.hmcl.game.VersionProvider;
|
||||||
|
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -161,11 +162,20 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
|||||||
|| mainClass.startsWith("cpw.mods"));
|
|| mainClass.startsWith("cpw.mods"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<ModLoaderType> getModLoaders() {
|
||||||
|
return Arrays.stream(LibraryType.values())
|
||||||
|
.filter(LibraryType::isModLoader)
|
||||||
|
.filter(this::has)
|
||||||
|
.map(LibraryType::getModLoaderType)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
public enum LibraryType {
|
public enum LibraryType {
|
||||||
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$")),
|
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$"), null),
|
||||||
FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader")),
|
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")),
|
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)")) {
|
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)"), ModLoaderType.FORGE) {
|
||||||
private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -177,21 +187,23 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
|||||||
return super.patchVersion(libraryVersion);
|
return super.patchVersion(libraryVersion);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
|
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader"), ModLoaderType.LITE_LOADER),
|
||||||
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$")),
|
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$"), null),
|
||||||
QUILT(true, "quilt", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-loader")),
|
QUILT(true, "quilt", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-loader"), ModLoaderType.QUILT),
|
||||||
QUILT_API(true, "quilt-api", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-api")),
|
QUILT_API(true, "quilt-api", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-api"), null),
|
||||||
BOOTSTRAP_LAUNCHER(false, "", Pattern.compile("cpw\\.mods"), Pattern.compile("bootstraplauncher"));
|
BOOTSTRAP_LAUNCHER(false, "", Pattern.compile("cpw\\.mods"), Pattern.compile("bootstraplauncher"), null);
|
||||||
|
|
||||||
private final boolean modLoader;
|
private final boolean modLoader;
|
||||||
private final String patchId;
|
private final String patchId;
|
||||||
private final Pattern group, artifact;
|
private final Pattern group, artifact;
|
||||||
|
private final ModLoaderType modLoaderType;
|
||||||
|
|
||||||
LibraryType(boolean modLoader, String patchId, Pattern group, Pattern artifact) {
|
LibraryType(boolean modLoader, String patchId, Pattern group, Pattern artifact, ModLoaderType modLoaderType) {
|
||||||
this.modLoader = modLoader;
|
this.modLoader = modLoader;
|
||||||
this.patchId = patchId;
|
this.patchId = patchId;
|
||||||
this.group = group;
|
this.group = group;
|
||||||
this.artifact = artifact;
|
this.artifact = artifact;
|
||||||
|
this.modLoaderType = modLoaderType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isModLoader() {
|
public boolean isModLoader() {
|
||||||
@@ -202,6 +214,10 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
|||||||
return patchId;
|
return patchId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ModLoaderType getModLoaderType() {
|
||||||
|
return modLoaderType;
|
||||||
|
}
|
||||||
|
|
||||||
public static LibraryType fromPatchId(String patchId) {
|
public static LibraryType fromPatchId(String patchId) {
|
||||||
for (LibraryType type : values())
|
for (LibraryType type : values())
|
||||||
if (type.getPatchId().equals(patchId))
|
if (type.getPatchId().equals(patchId))
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ public class MaintainTask extends Task<Version> {
|
|||||||
public static Version unique(Version version) {
|
public static Version unique(Version version) {
|
||||||
List<Library> libraries = new ArrayList<>();
|
List<Library> libraries = new ArrayList<>();
|
||||||
|
|
||||||
SimpleMultimap<String, Integer> multimap = new SimpleMultimap<String, Integer>(HashMap::new, ArrayList::new);
|
SimpleMultimap<String, Integer, List<Integer>> multimap = new SimpleMultimap<>(HashMap::new, ArrayList::new);
|
||||||
|
|
||||||
for (Library library : version.getLibraries()) {
|
for (Library library : version.getLibraries()) {
|
||||||
String id = library.getGroupId() + ":" + library.getArtifactId();
|
String id = library.getGroupId() + ":" + library.getArtifactId();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public abstract class VersionList<T extends RemoteVersion> {
|
|||||||
* key: game version.
|
* key: game version.
|
||||||
* values: corresponding remote versions.
|
* values: corresponding remote versions.
|
||||||
*/
|
*/
|
||||||
protected final SimpleMultimap<String, T> versions = new SimpleMultimap<String, T>(HashMap::new, TreeSet::new);
|
protected final SimpleMultimap<String, T, TreeSet<T>> versions = new SimpleMultimap<>(HashMap::new, TreeSet::new);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the version list has been loaded.
|
* True if the version list has been loaded.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import java.util.function.Consumer;
|
|||||||
*/
|
*/
|
||||||
public final class EventManager<T extends Event> {
|
public final class EventManager<T extends Event> {
|
||||||
|
|
||||||
private final SimpleMultimap<EventPriority, Consumer<T>> handlers
|
private final SimpleMultimap<EventPriority, Consumer<T>, CopyOnWriteArraySet<Consumer<T>>> handlers
|
||||||
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new);
|
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new);
|
||||||
|
|
||||||
public Consumer<T> registerWeak(Consumer<T> consumer) {
|
public Consumer<T> registerWeak(Consumer<T> consumer) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import javafx.beans.property.BooleanProperty;
|
|||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import org.jackhuang.hmcl.mod.modinfo.PackMcMeta;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
|||||||
@@ -18,10 +18,20 @@
|
|||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod;
|
||||||
|
|
||||||
public enum ModLoaderType {
|
public enum ModLoaderType {
|
||||||
UNKNOWN,
|
UNKNOWN("Unknown"),
|
||||||
FORGE,
|
FORGE("Forge"),
|
||||||
FABRIC,
|
FABRIC("Fabric"),
|
||||||
QUILT,
|
QUILT("Quilt"),
|
||||||
LITE_LOADER,
|
LITE_LOADER("LiteLoader"),
|
||||||
PACK
|
PACK("Pack");
|
||||||
|
|
||||||
|
private final String loaderName;
|
||||||
|
|
||||||
|
ModLoaderType(String loaderName) {
|
||||||
|
this.loaderName = loaderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String getLoaderName() {
|
||||||
|
return loaderName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod;
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.game.GameRepository;
|
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.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
@@ -25,12 +28,32 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.Collection;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
public final class ModManager {
|
public final class ModManager {
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface ModMetadataReader {
|
||||||
|
LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<String, Pair<ModMetadataReader[], String>> READERS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
TreeMap<String, Pair<ModMetadataReader[], String>> 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 GameRepository repository;
|
||||||
private final String id;
|
private final String id;
|
||||||
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
||||||
@@ -71,46 +94,28 @@ public final class ModManager {
|
|||||||
|
|
||||||
public LocalModFile getModInfo(Path modFile) {
|
public LocalModFile getModInfo(Path modFile) {
|
||||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||||
String description;
|
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||||
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
|
Pair<ModMetadataReader[], String> currentReader = READERS.get(extension);
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
if (currentReader == null) {
|
||||||
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 {
|
|
||||||
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
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,
|
return new LocalModFile(this,
|
||||||
getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN),
|
getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN),
|
||||||
modFile,
|
modFile,
|
||||||
FileUtils.getNameWithoutExtension(modFile),
|
FileUtils.getNameWithoutExtension(modFile),
|
||||||
new LocalModFile.Description(description));
|
new LocalModFile.Description(currentReader.getValue())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshMods() throws IOException {
|
public void refreshMods() throws IOException {
|
||||||
@@ -281,6 +286,11 @@ public final class ModManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Files.exists(fs.getPath("quilt.mod.json"))) {
|
||||||
|
// Quilt mod
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (Files.exists(fs.getPath("litemod.json"))) {
|
if (Files.exists(fs.getPath("litemod.json"))) {
|
||||||
// Liteloader mod
|
// Liteloader mod
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -101,6 +101,72 @@ public class RemoteMod {
|
|||||||
Alpha
|
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 {
|
public enum Type {
|
||||||
CURSEFORGE(CurseForgeRemoteModRepository.MODS),
|
CURSEFORGE(CurseForgeRemoteModRepository.MODS),
|
||||||
MODRINTH(ModrinthRemoteModRepository.MODS);
|
MODRINTH(ModrinthRemoteModRepository.MODS);
|
||||||
@@ -135,11 +201,11 @@ public class RemoteMod {
|
|||||||
private final Date datePublished;
|
private final Date datePublished;
|
||||||
private final VersionType versionType;
|
private final VersionType versionType;
|
||||||
private final File file;
|
private final File file;
|
||||||
private final List<String> dependencies;
|
private final List<Dependency> dependencies;
|
||||||
private final List<String> gameVersions;
|
private final List<String> gameVersions;
|
||||||
private final List<ModLoaderType> loaders;
|
private final List<ModLoaderType> loaders;
|
||||||
|
|
||||||
public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List<String> dependencies, List<String> gameVersions, List<ModLoaderType> loaders) {
|
public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List<Dependency> dependencies, List<String> gameVersions, List<ModLoaderType> loaders) {
|
||||||
this.self = self;
|
this.self = self;
|
||||||
this.modid = modid;
|
this.modid = modid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -185,7 +251,7 @@ public class RemoteMod {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getDependencies() {
|
public List<Dependency> getDependencies() {
|
||||||
return dependencies;
|
return dependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,39 @@ public interface RemoteModRepository {
|
|||||||
DESC
|
DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<RemoteMod> search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
class SearchResult {
|
||||||
|
private final Stream<RemoteMod> sortedResults;
|
||||||
|
|
||||||
|
private final Stream<RemoteMod> unsortedResults;
|
||||||
|
|
||||||
|
private final int totalPages;
|
||||||
|
|
||||||
|
public SearchResult(Stream<RemoteMod> sortedResults, Stream<RemoteMod> unsortedResults, int totalPages) {
|
||||||
|
this.sortedResults = sortedResults;
|
||||||
|
this.unsortedResults = unsortedResults;
|
||||||
|
this.totalPages = totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchResult(Stream<RemoteMod> sortedResults, int pages) {
|
||||||
|
this.sortedResults = sortedResults;
|
||||||
|
this.unsortedResults = sortedResults;
|
||||||
|
this.totalPages = pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<RemoteMod> getResults() {
|
||||||
|
return this.sortedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<RemoteMod> 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;
|
throws IOException;
|
||||||
|
|
||||||
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import org.jackhuang.hmcl.mod.ModLoaderType;
|
|||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -30,6 +32,15 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
public class CurseAddon implements RemoteMod.IMod {
|
public class CurseAddon implements RemoteMod.IMod {
|
||||||
|
public static final Map<Integer, RemoteMod.DependencyType> 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 id;
|
||||||
private final int gameId;
|
private final int gameId;
|
||||||
private final String name;
|
private final String name;
|
||||||
@@ -566,7 +577,12 @@ public class CurseAddon implements RemoteMod.IMod {
|
|||||||
getFileDate(),
|
getFileDate(),
|
||||||
versionType,
|
versionType,
|
||||||
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
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().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
||||||
gameVersions.stream().flatMap(version -> {
|
gameVersions.stream().flatMap(version -> {
|
||||||
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import org.jackhuang.hmcl.mod.LocalModFile;
|
|||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||||
import org.jackhuang.hmcl.util.MurmurHash2;
|
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.HttpRequest;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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 PREFIX = "https://api.curseforge.com";
|
||||||
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getManifestAttribute("CurseForge-Api-Key", ""));
|
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() {
|
public static boolean isAvailable() {
|
||||||
return !apiKey.isEmpty();
|
return !apiKey.isEmpty();
|
||||||
}
|
}
|
||||||
@@ -91,7 +95,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<RemoteMod> 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;
|
int categoryId = 0;
|
||||||
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
||||||
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
||||||
@@ -102,12 +106,51 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
|||||||
pair("searchFilter", searchFilter),
|
pair("searchFilter", searchFilter),
|
||||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||||
pair("sortOrder", toSortOrder(sortOrder)),
|
pair("sortOrder", toSortOrder(sortOrder)),
|
||||||
pair("index", Integer.toString(pageOffset)),
|
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||||
pair("pageSize", Integer.toString(pageSize)))
|
pair("pageSize", Integer.toString(pageSize)))
|
||||||
.header("X-API-KEY", apiKey)
|
.header("X-API-KEY", apiKey)
|
||||||
.getJson(new TypeToken<Response<List<CurseAddon>>>() {
|
.getJson(new TypeToken<Response<List<CurseAddon>>>() {
|
||||||
}.getType());
|
}.getType());
|
||||||
return response.getData().stream().map(CurseAddon::toMod);
|
Stream<RemoteMod> 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<String, Integer> 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
|
@Override
|
||||||
|
|||||||
@@ -15,10 +15,13 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod.modinfo;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
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.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod.modinfo;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.moandjiezana.toml.Toml;
|
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.Immutable;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
@@ -20,7 +23,6 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
public final class ForgeNewModMetadata {
|
public final class ForgeNewModMetadata {
|
||||||
|
|
||||||
private final String modLoader;
|
private final String modLoader;
|
||||||
|
|
||||||
private final String loaderVersion;
|
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()),
|
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(),
|
mod.getDisplayURL(),
|
||||||
metadata.getLogoFile());
|
metadata.getLogoFile());
|
||||||
}
|
}
|
||||||
@@ -15,11 +15,14 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod.modinfo;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.google.gson.reflect.TypeToken;
|
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.Immutable;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
@@ -37,7 +40,6 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
public final class ForgeOldModMetadata {
|
public final class ForgeOldModMetadata {
|
||||||
|
|
||||||
@SerializedName("modid")
|
@SerializedName("modid")
|
||||||
private final String modId;
|
private final String modId;
|
||||||
private final String name;
|
private final String name;
|
||||||
@@ -15,13 +15,17 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod.modinfo;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
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.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
@@ -106,7 +110,7 @@ public final class LiteModMetadata {
|
|||||||
return updateURI;
|
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())) {
|
try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
|
||||||
ZipEntry entry = zipFile.getEntry("litemod.json");
|
ZipEntry entry = zipFile.getEntry("litemod.json");
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
@@ -15,11 +15,14 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod.modinfo;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import com.google.gson.annotations.SerializedName;
|
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.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.Validation;
|
import org.jackhuang.hmcl.util.gson.Validation;
|
||||||
@@ -36,7 +39,6 @@ import java.util.List;
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
public class PackMcMeta implements Validation {
|
public class PackMcMeta implements Validation {
|
||||||
|
|
||||||
@SerializedName("pack")
|
@SerializedName("pack")
|
||||||
private final PackInfo pack;
|
private final PackInfo pack;
|
||||||
|
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<RemoteMod> 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<List<String>> facets = new ArrayList<>();
|
List<List<String>> facets = new ArrayList<>();
|
||||||
facets.add(Collections.singletonList("project_type:" + projectType));
|
facets.add(Collections.singletonList("project_type:" + projectType));
|
||||||
if (StringUtils.isNotBlank(gameVersion)) {
|
if (StringUtils.isNotBlank(gameVersion)) {
|
||||||
@@ -87,14 +87,14 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
Map<String, String> query = mapOf(
|
Map<String, String> query = mapOf(
|
||||||
pair("query", searchFilter),
|
pair("query", searchFilter),
|
||||||
pair("facets", JsonUtils.UGLY_GSON.toJson(facets)),
|
pair("facets", JsonUtils.UGLY_GSON.toJson(facets)),
|
||||||
pair("offset", Integer.toString(pageOffset)),
|
pair("offset", Integer.toString(pageOffset * pageSize)),
|
||||||
pair("limit", Integer.toString(pageSize)),
|
pair("limit", Integer.toString(pageSize)),
|
||||||
pair("index", convertSortType(sort))
|
pair("index", convertSortType(sort))
|
||||||
);
|
);
|
||||||
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
||||||
.getJson(new TypeToken<Response<ProjectSearchResult>>() {
|
.getJson(new TypeToken<Response<ProjectSearchResult>>() {
|
||||||
}.getType());
|
}.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
|
@Override
|
||||||
@@ -286,17 +286,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||||
Set<String> dependencies = modRepository.getRemoteVersionsById(getId())
|
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(getId())
|
||||||
.flatMap(version -> version.getDependencies().stream())
|
.flatMap(version -> version.getDependencies().stream())
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
List<RemoteMod> mods = new ArrayList<>();
|
List<RemoteMod> mods = new ArrayList<>();
|
||||||
for (String dependencyId : dependencies) {
|
for (RemoteMod.Dependency dependency : dependencies) {
|
||||||
if (dependencyId == null) {
|
mods.add(dependency.load());
|
||||||
mods.add(RemoteMod.getEmptyRemoteMod());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(dependencyId)) {
|
|
||||||
mods.add(modRepository.getModById(dependencyId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return mods;
|
return mods;
|
||||||
}
|
}
|
||||||
@@ -313,9 +308,9 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
categories,
|
categories,
|
||||||
null,
|
String.format("https://modrinth.com/%s/%s", projectType, id),
|
||||||
iconUrl,
|
iconUrl,
|
||||||
(RemoteMod.IMod) this
|
this
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,6 +346,13 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class ProjectVersion implements RemoteMod.IVersion {
|
public static class ProjectVersion implements RemoteMod.IVersion {
|
||||||
|
private static final Map<String, RemoteMod.@Nullable DependencyType> 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;
|
private final String name;
|
||||||
|
|
||||||
@SerializedName("version_number")
|
@SerializedName("version_number")
|
||||||
@@ -496,7 +498,17 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
datePublished,
|
datePublished,
|
||||||
type,
|
type,
|
||||||
files.get(0).toFile(),
|
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,
|
gameVersions,
|
||||||
loaders.stream().flatMap(loader -> {
|
loaders.stream().flatMap(loader -> {
|
||||||
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
||||||
@@ -651,17 +663,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||||
Set<String> dependencies = modRepository.getRemoteVersionsById(getProjectId())
|
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(getProjectId())
|
||||||
.flatMap(version -> version.getDependencies().stream())
|
.flatMap(version -> version.getDependencies().stream())
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
List<RemoteMod> mods = new ArrayList<>();
|
List<RemoteMod> mods = new ArrayList<>();
|
||||||
for (String dependencyId : dependencies) {
|
for (RemoteMod.Dependency dependency : dependencies) {
|
||||||
if (dependencyId == null) {
|
mods.add(dependency.load());
|
||||||
mods.add(RemoteMod.getEmptyRemoteMod());
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(dependencyId)) {
|
|
||||||
mods.add(modRepository.getModById(dependencyId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return mods;
|
return mods;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ import java.util.function.Supplier;
|
|||||||
*
|
*
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
public final class SimpleMultimap<K, V> {
|
public final class SimpleMultimap<K, V, M extends Collection<V>> {
|
||||||
|
|
||||||
private final Map<K, Collection<V>> map;
|
private final Map<K, M> map;
|
||||||
private final Supplier<Collection<V>> valuer;
|
private final Supplier<M> valuer;
|
||||||
|
|
||||||
public SimpleMultimap(Supplier<Map<K, Collection<V>>> mapper, Supplier<Collection<V>> valuer) {
|
public SimpleMultimap(Supplier<Map<K, M>> mapper, Supplier<M> valuer) {
|
||||||
this.map = mapper.get();
|
this.map = mapper.get();
|
||||||
this.valuer = valuer;
|
this.valuer = valuer;
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ public final class SimpleMultimap<K, V> {
|
|||||||
|
|
||||||
public Collection<V> values() {
|
public Collection<V> values() {
|
||||||
Collection<V> res = valuer.get();
|
Collection<V> res = valuer.get();
|
||||||
for (Map.Entry<K, Collection<V>> entry : map.entrySet())
|
for (Map.Entry<K, M> entry : map.entrySet())
|
||||||
res.addAll(entry.getValue());
|
res.addAll(entry.getValue());
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -61,27 +61,27 @@ public final class SimpleMultimap<K, V> {
|
|||||||
return map.containsKey(key) && !map.get(key).isEmpty();
|
return map.containsKey(key) && !map.get(key).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<V> get(K key) {
|
public M get(K key) {
|
||||||
return map.computeIfAbsent(key, any -> valuer.get());
|
return map.computeIfAbsent(key, any -> valuer.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void put(K key, V value) {
|
public void put(K key, V value) {
|
||||||
Collection<V> set = get(key);
|
M set = get(key);
|
||||||
set.add(value);
|
set.add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putAll(K key, Collection<? extends V> value) {
|
public void putAll(K key, Collection<? extends V> value) {
|
||||||
Collection<V> set = get(key);
|
M set = get(key);
|
||||||
set.addAll(value);
|
set.addAll(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<V> removeKey(K key) {
|
public M removeKey(K key) {
|
||||||
return map.remove(key);
|
return map.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean removeValue(V value) {
|
public boolean removeValue(V value) {
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
for (Collection<V> c : map.values())
|
for (M c : map.values())
|
||||||
flag |= c.remove(value);
|
flag |= c.remove(value);
|
||||||
return flag;
|
return flag;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,6 +357,32 @@ public final class StringUtils {
|
|||||||
return true;
|
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.
|
* Class for computing the longest common subsequence between strings.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public abstract class HttpRequest {
|
|||||||
return getStringWithRetry(() -> {
|
return getStringWithRetry(() -> {
|
||||||
HttpURLConnection con = createConnection();
|
HttpURLConnection con = createConnection();
|
||||||
con = resolveConnection(con);
|
con = resolveConnection(con);
|
||||||
return IOUtils.readFullyAsString(con.getInputStream());
|
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(con.getInputStream()) : con.getInputStream());
|
||||||
}, retryTimes);
|
}, retryTimes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.util.io;
|
package org.jackhuang.hmcl.util.io;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
* 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);
|
dest.write(buf, 0, len);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static InputStream wrapFromGZip(InputStream inputStream) throws IOException {
|
||||||
|
return new GZIPInputStream(inputStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ public final class NetworkUtils {
|
|||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
conn.setUseCaches(false);
|
conn.setUseCaches(false);
|
||||||
conn.setConnectTimeout(5000);
|
conn.setConnectTimeout(8000);
|
||||||
conn.setReadTimeout(5000);
|
conn.setReadTimeout(8000);
|
||||||
conn.setInstanceFollowRedirects(false);
|
conn.setInstanceFollowRedirects(false);
|
||||||
Map<String, List<String>> properties = conn.getRequestProperties();
|
Map<String, List<String>> properties = conn.getRequestProperties();
|
||||||
String method = conn.getRequestMethod();
|
String method = conn.getRequestMethod();
|
||||||
@@ -209,13 +209,13 @@ public final class NetworkUtils {
|
|||||||
public static String readData(HttpURLConnection con) throws IOException {
|
public static String readData(HttpURLConnection con) throws IOException {
|
||||||
try {
|
try {
|
||||||
try (InputStream stdout = con.getInputStream()) {
|
try (InputStream stdout = con.getInputStream()) {
|
||||||
return IOUtils.readFullyAsString(stdout);
|
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stdout) : stdout);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
try (InputStream stderr = con.getErrorStream()) {
|
try (InputStream stderr = con.getErrorStream()) {
|
||||||
if (stderr == null)
|
if (stderr == null)
|
||||||
throw e;
|
throw e;
|
||||||
return IOUtils.readFullyAsString(stderr);
|
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stderr) : stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
README.md
28
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)
|
## JVM Options (for debugging)
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `-Dhmcl.home=<path>` | Override HMCL directory. |
|
| `-Dhmcl.home=<path>` | Override HMCL directory. |
|
||||||
| `-Dhmcl.self_integrity_check.disable=true` | Bypass the self integrity check when checking for update. |
|
| `-Dhmcl.self_integrity_check.disable=true` | Bypass the self integrity check when checking for update. |
|
||||||
| `-Dhmcl.bmclapi.override=<version>` | Override API Root of BMCLAPI download provider, defaults to `https://bmclapi2.bangbang93.com`. e.g. `https://download.mcbbs.net`. |
|
| `-Dhmcl.bmclapi.override=<version>` | Override API Root of BMCLAPI download provider, defaults to `https://bmclapi2.bangbang93.com`. e.g. `https://download.mcbbs.net`. |
|
||||||
| `-Dhmcl.font.override=<font family>` | Override font family. |
|
| `-Dhmcl.font.override=<font family>` | Override font family. |
|
||||||
| `-Dhmcl.version.override=<version>` | Override the version number. |
|
| `-Dhmcl.version.override=<version>` | Override the version number. |
|
||||||
| `-Dhmcl.update_source.override=<url>` | Override the update source. |
|
| ~~`-Dhmcl.update_source.override=<url>`~~ | Override the update source for HMCL itself. (Deprecated, please use `hmcl.hmcl_update_source.override` instead.) |
|
||||||
| `-Dhmcl.authlibinjector.location=<path>` | Use specified authlib-injector (instead of downloading one). |
|
| `-Dhmcl.hmcl_update_source.override=<url>` | Override the update source for HMCL itself. |
|
||||||
| `-Dhmcl.openjfx.repo=<maven repository url>` | Add custom Maven repository for download OpenJFX. |
|
| `-Dhmcl.resource_update_source.override=<url>` | Override the update source for dynamic remote resources. |
|
||||||
| `-Dhmcl.native.encoding=<encoding>` | Override the native encoding. |
|
| `-Dhmcl.authlibinjector.location=<path>` | Use specified authlib-injector (instead of downloading one). |
|
||||||
| `-Dhmcl.microsoft.auth.id=<App ID>` | Override Microsoft OAuth App ID. |
|
| `-Dhmcl.openjfx.repo=<maven repository url>` | Add custom Maven repository for download OpenJFX. |
|
||||||
| `-Dhmcl.microsoft.auth.secret=<App Secret>` | Override Microsoft OAuth App secret. |
|
| `-Dhmcl.native.encoding=<encoding>` | Override the native encoding. |
|
||||||
|
| `-Dhmcl.microsoft.auth.id=<App ID>` | Override Microsoft OAuth App ID. |
|
||||||
|
| `-Dhmcl.microsoft.auth.secret=<App Secret>` | Override Microsoft OAuth App secret. |
|
||||||
|
|||||||
28
README_cn.md
28
README_cn.md
@@ -57,16 +57,18 @@ HMCL 有着强大的跨平台能力. 它不仅支持 Windows、Linux、macOS 等
|
|||||||
|
|
||||||
## JVM 选项 (用于调试)
|
## JVM 选项 (用于调试)
|
||||||
|
|
||||||
| 参数 | 简介 |
|
| 参数 | 简介 |
|
||||||
|----------------------------------------------|-------------------------------------------------------------------------------------------------|
|
|------------------------------------------------|-------------------------------------------------------------------------------------------------|
|
||||||
| `-Dhmcl.home=<path>` | 覆盖 HMCL 数据文件夹. |
|
| `-Dhmcl.home=<path>` | 覆盖 HMCL 数据文件夹. |
|
||||||
| `-Dhmcl.self_integrity_check.disable=true` | 检查更新时绕过本体完整性检查. |
|
| `-Dhmcl.self_integrity_check.disable=true` | 检查更新时绕过本体完整性检查. |
|
||||||
| `-Dhmcl.bmclapi.override=<version>` | 覆盖 BMCLAPI 的 API Root, 默认值为 `https://bmclapi2.bangbang93.com`. 例如 `https://download.mcbbs.net`. |
|
| `-Dhmcl.bmclapi.override=<version>` | 覆盖 BMCLAPI 的 API Root, 默认值为 `https://bmclapi2.bangbang93.com`. 例如 `https://download.mcbbs.net`. |
|
||||||
| `-Dhmcl.font.override=<font family>` | 覆盖字族. |
|
| `-Dhmcl.font.override=<font family>` | 覆盖字族. |
|
||||||
| `-Dhmcl.version.override=<version>` | 覆盖版本号. |
|
| `-Dhmcl.version.override=<version>` | 覆盖版本号. |
|
||||||
| `-Dhmcl.update_source.override=<url>` | 覆盖更新源. |
|
| ~~`-Dhmcl.update_source.override=<url>`~~ | 覆盖 HMCL 更新源(已弃用,请使用 `hmcl.hmcl_update_source.override`). |
|
||||||
| `-Dhmcl.authlibinjector.location=<path>` | 使用指定的 authlib-injector (而非下载一个). |
|
| `-Dhmcl.hmcl_update_source.override=<url>` | 覆盖 HMCL 更新源. |
|
||||||
| `-Dhmcl.openjfx.repo=<maven repository url>` | 添加用于下载 OpenJFX 的自定义 Maven 仓库 |
|
| `-Dhmcl.resource_update_source.override=<url>` | 覆盖动态远程资源更新源. |
|
||||||
| `-Dhmcl.native.encoding=<encoding>` | 覆盖原生编码. |
|
| `-Dhmcl.authlibinjector.location=<path>` | 使用指定的 authlib-injector (而非下载一个). |
|
||||||
| `-Dhmcl.microsoft.auth.id=<App ID>` | 覆盖 Microsoft OAuth App ID. |
|
| `-Dhmcl.openjfx.repo=<maven repository url>` | 添加用于下载 OpenJFX 的自定义 Maven 仓库 |
|
||||||
| `-Dhmcl.microsoft.auth.secret=<App Secret>` | 覆盖 Microsoft OAuth App 密钥. |
|
| `-Dhmcl.native.encoding=<encoding>` | 覆盖原生编码. |
|
||||||
|
| `-Dhmcl.microsoft.auth.id=<App ID>` | 覆盖 Microsoft OAuth App ID. |
|
||||||
|
| `-Dhmcl.microsoft.auth.secret=<App Secret>` | 覆盖 Microsoft OAuth App 密钥. |
|
||||||
|
|||||||
24
data-json/dynamic-remote-resources-raw.json
Normal file
24
data-json/dynamic-remote-resources-raw.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
data-json/dynamic-remote-resources.json
Normal file
1
data-json/dynamic-remote-resources.json
Normal file
@@ -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"}}}}
|
||||||
Reference in New Issue
Block a user