Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86085031f6 | ||
|
|
30f47704f1 | ||
|
|
82e0cd1ca1 | ||
|
|
5374127241 | ||
|
|
df489c0cfe | ||
|
|
d83f6106f5 | ||
|
|
b7361c8da4 | ||
|
|
73531dbf60 | ||
|
|
5af6858d4c | ||
|
|
1a3b473356 | ||
|
|
67e4dddda1 | ||
|
|
55816032b7 | ||
|
|
b9fd0e50a7 | ||
|
|
71d54955ad | ||
|
|
d34dc82db6 | ||
|
|
73214ad599 | ||
|
|
1ddb73595d | ||
|
|
f9ee9c8592 | ||
|
|
0fdfc945c3 | ||
|
|
97219c951e | ||
|
|
64cc64095d | ||
|
|
91a613d966 | ||
|
|
06b7b1e574 | ||
|
|
933b7cab2f | ||
|
|
885e25d07a | ||
|
|
421f9a3b2b | ||
|
|
e52cfb036e | ||
|
|
87a3a4ece3 | ||
|
|
b152e2bfab | ||
|
|
c9c97a45b7 | ||
|
|
cba4ba2799 | ||
|
|
ab9eea3620 | ||
|
|
96f2d011ca | ||
|
|
9ee55cd17d | ||
|
|
07ee7867da | ||
|
|
940d4f722c | ||
|
|
af56096162 | ||
|
|
0e12c1a132 | ||
|
|
ca20509cb1 | ||
|
|
58c93ff349 | ||
|
|
aab3b22290 | ||
|
|
6cf933741f | ||
|
|
9d17834227 | ||
|
|
7805508d4c | ||
|
|
72f6c2faf3 | ||
|
|
7035767828 | ||
|
|
13a2777c87 | ||
|
|
a4f9438d9e | ||
|
|
4445021d66 | ||
|
|
f6a3b76a0b | ||
|
|
42bb602ff5 | ||
|
|
4547448718 | ||
|
|
10bda7e6f4 | ||
|
|
f239140055 | ||
|
|
6f4b557f63 | ||
|
|
5118ce4e62 | ||
|
|
a705b7cf95 | ||
|
|
0d5d1ae4e6 | ||
|
|
b57547415b | ||
|
|
b60d889bf7 | ||
|
|
2f463c1eda | ||
|
|
d1e006ed29 | ||
|
|
ba58905c9c | ||
|
|
de221b9dae | ||
|
|
0913c5e710 | ||
|
|
41fd38d8d1 | ||
|
|
7ec880852f | ||
|
|
9213b7fb2e | ||
|
|
80f1367d9a | ||
|
|
f521a041a9 | ||
|
|
be9efbe6ee | ||
|
|
8f3c716ca7 |
No files matched your search
@@ -13,6 +13,7 @@ hs_err_pid*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
|
||||
+77
-33
@@ -37,6 +37,8 @@ val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
val embedResources by configurations.registering
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
@@ -47,6 +49,8 @@ dependencies {
|
||||
if (launcherExe == null) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
embedResources(libs.authlib.injector)
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
@@ -104,6 +108,50 @@ tasks.compileJava {
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
val hmclProperties = buildList {
|
||||
add("hmcl.version" to project.version.toString())
|
||||
System.getenv("GITHUB_SHA")?.let {
|
||||
add("hmcl.version.hash" to it)
|
||||
}
|
||||
add("hmcl.version.type" to versionType)
|
||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||
}
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
val createPropertiesFile by tasks.registering {
|
||||
outputs.file(hmclPropertiesFile)
|
||||
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
||||
|
||||
doLast {
|
||||
val targetFile = hmclPropertiesFile.get().asFile
|
||||
targetFile.parentFile.mkdir()
|
||||
targetFile.bufferedWriter().use {
|
||||
for ((k, v) in hmclProperties) {
|
||||
it.write("$k=$v\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val addOpens = listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
)
|
||||
|
||||
tasks.jar {
|
||||
enabled = false
|
||||
dependsOn(tasks["shadowJar"])
|
||||
@@ -112,6 +160,8 @@ tasks.jar {
|
||||
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
|
||||
tasks.shadowJar {
|
||||
dependsOn(createPropertiesFile)
|
||||
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
exclude("**/package-info.class")
|
||||
@@ -132,39 +182,14 @@ tasks.shadowJar {
|
||||
exclude(project(":HMCLBoot"))
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Implementation-Version" to project.version,
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Build-Channel" to versionType,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
).joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
System.getenv("GITHUB_SHA")?.also {
|
||||
attributes("GitHub-SHA" to it)
|
||||
}
|
||||
}
|
||||
manifest.attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
"Implementation-Version" to project.version.toString(),
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Add-Opens" to addOpens.joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
if (launcherExe != null) {
|
||||
into("assets") {
|
||||
@@ -178,6 +203,15 @@ tasks.shadowJar {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn(createPropertiesFile)
|
||||
|
||||
into("assets/") {
|
||||
from(hmclPropertiesFile)
|
||||
from(embedResources)
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
val extensions = listOf("exe", "sh")
|
||||
|
||||
@@ -260,6 +294,16 @@ fun parseToolOptions(options: String?): MutableList<String> {
|
||||
return result
|
||||
}
|
||||
|
||||
// For IntelliJ IDEA
|
||||
tasks.withType<JavaExec> {
|
||||
if (name != "run") {
|
||||
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
|
||||
// if (javaVersion >= JavaVersion.VERSION_24) {
|
||||
// jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("run") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -19,10 +19,13 @@ package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
@@ -33,11 +36,15 @@ public final class Metadata {
|
||||
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 11;
|
||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||
@@ -49,8 +56,8 @@ public final class Metadata {
|
||||
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
||||
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
|
||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||
|
||||
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
@@ -94,4 +101,30 @@ public final class Metadata {
|
||||
return !isStable() && !isDev();
|
||||
}
|
||||
|
||||
public static @Nullable String getSuggestedJavaDownloadLink() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
|
||||
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
|
||||
else {
|
||||
EnumSet<Architecture> supportedArchitectures;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
|
||||
supportedArchitectures = EnumSet.of(
|
||||
Architecture.X86_64, Architecture.X86,
|
||||
Architecture.ARM64, Architecture.ARM32,
|
||||
Architecture.RISCV64, Architecture.LOONGARCH64
|
||||
);
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
|
||||
else
|
||||
supportedArchitectures = EnumSet.noneOf(Architecture.class);
|
||||
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
|
||||
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
|
||||
OperatingSystem.CURRENT_OS.getCheckedName(),
|
||||
Architecture.SYSTEM_ARCH.getCheckedName()
|
||||
);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,18 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
@@ -58,61 +61,83 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
private void generateOptionsTxt() {
|
||||
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
|
||||
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
|
||||
|
||||
if (optionsFile.exists()) {
|
||||
if (config().isDisableAutoGameOptions())
|
||||
return;
|
||||
}
|
||||
|
||||
if (configFolder.isDirectory()) {
|
||||
if (findFiles(configFolder, "options.txt")) {
|
||||
return;
|
||||
Path runDir = repository.getRunDirectory(version.getId()).toPath();
|
||||
Path optionsFile = runDir.resolve("options.txt");
|
||||
Path configFolder = runDir.resolve("config");
|
||||
|
||||
if (Files.exists(optionsFile))
|
||||
return;
|
||||
|
||||
if (Files.isDirectory(configFolder)) {
|
||||
try (Stream<Path> stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) {
|
||||
if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file))))
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to visit config folder", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!I18n.isUseChinese()) {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (Locales.isEnglish(locale))
|
||||
return;
|
||||
}
|
||||
|
||||
String lang;
|
||||
/*
|
||||
1.0- :没有语言选项,遇到这些版本时不设置
|
||||
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
|
||||
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
|
||||
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
|
||||
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
|
||||
* 1.0 : No language option, do not set for these versions
|
||||
* 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash)
|
||||
* 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English
|
||||
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
||||
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
||||
*/
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
|
||||
if (gameVersion.compareTo("1.1") < 0) {
|
||||
lang = null;
|
||||
} else if (gameVersion.compareTo("1.11") < 0) {
|
||||
lang = "zh_CN";
|
||||
} else {
|
||||
lang = "zh_cn";
|
||||
}
|
||||
if (gameVersion.compareTo("1.1") < 0)
|
||||
return;
|
||||
|
||||
if (lang != null) {
|
||||
try {
|
||||
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
String lang = normalizedLanguageTag(locale, gameVersion);
|
||||
if (lang.isEmpty())
|
||||
return;
|
||||
|
||||
if (gameVersion.compareTo("1.11") >= 0)
|
||||
lang = lang.toLowerCase(Locale.ROOT);
|
||||
|
||||
try {
|
||||
Files.createDirectories(optionsFile.getParent());
|
||||
Files.writeString(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean findFiles(File folder, String fileName) {
|
||||
File[] fs = folder.listFiles();
|
||||
if (fs != null) {
|
||||
for (File f : fs) {
|
||||
if (f.isDirectory())
|
||||
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
|
||||
return true;
|
||||
if (f.getName().equals(fileName))
|
||||
return true;
|
||||
}
|
||||
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
||||
String language = locale.getLanguage();
|
||||
String region = locale.getCountry();
|
||||
|
||||
switch (language) {
|
||||
case "zh":
|
||||
case "cmn":
|
||||
if (Locales.isSimplifiedChinese(locale))
|
||||
return "zh_CN";
|
||||
if (gameVersion.compareTo("1.16") >= 0
|
||||
&& (region.equals("HK") || region.equals("MO")))
|
||||
return "zh_HK";
|
||||
return "zh_TW";
|
||||
case "ru":
|
||||
return "ru_RU";
|
||||
case "uk":
|
||||
return "uk_UA";
|
||||
case "es":
|
||||
return "es_ES";
|
||||
case "ja":
|
||||
return "ja_JP";
|
||||
case "lzh":
|
||||
return gameVersion.compareTo("1.16") >= 0
|
||||
? "lzh"
|
||||
: "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -160,8 +160,10 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getMesaLoader(java);
|
||||
if (renderer != Renderer.DEFAULT
|
||||
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& OperatingSystem.WINDOWS_VERSION != null) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||
{
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
|
||||
@@ -50,10 +50,7 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -210,14 +207,17 @@ public final class ModpackHelper {
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.javafx(), success, failure);
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
|
||||
@@ -158,13 +158,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -56,6 +56,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
|
||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
|
||||
|
||||
public static String getMojangJavaPlatform(Platform platform) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||
@@ -571,6 +572,10 @@ public final class JavaManager {
|
||||
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
|
||||
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
@@ -68,18 +69,6 @@ public final class Accounts {
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
@@ -337,8 +326,6 @@ public final class Accounts {
|
||||
});
|
||||
}
|
||||
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
@@ -371,24 +358,18 @@ public final class Accounts {
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
|
||||
DownloadProviders::getDownloadProvider) {
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
|
||||
if (local.isPresent()) {
|
||||
return local;
|
||||
}
|
||||
// search authlib-injector.jar in current directory, it's used as a fallback
|
||||
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (authlibinjectorLocation != null) {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
|
||||
String authlibInjectorVersion = JarUtils.getAttribute("hmcl.authlib-injector.version", null);
|
||||
if (authlibInjectorVersion == null)
|
||||
throw new AssertionError("Missing hmcl.authlib-injector.version");
|
||||
|
||||
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
|
||||
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
|
||||
@@ -186,6 +186,9 @@ public final class Config implements Observable {
|
||||
@SerializedName("addedLittleSkin")
|
||||
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("disableAutoGameOptions")
|
||||
private BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("promptedVersion")
|
||||
private StringProperty promptedVersion = new SimpleStringProperty();
|
||||
|
||||
@@ -631,6 +634,18 @@ public final class Config implements Observable {
|
||||
this.addedLittleSkin.set(addedLittleSkin);
|
||||
}
|
||||
|
||||
public BooleanProperty disableAutoGameOptionsProperty() {
|
||||
return disableAutoGameOptions;
|
||||
}
|
||||
|
||||
public boolean isDisableAutoGameOptions() {
|
||||
return disableAutoGameOptions.get();
|
||||
}
|
||||
|
||||
public void setDisableAutoGameOptions(boolean disableAutoGameOptions) {
|
||||
this.disableAutoGameOptions.set(disableAutoGameOptions);
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
return configVersion.get();
|
||||
}
|
||||
|
||||
@@ -129,8 +129,12 @@ public final class FontManager {
|
||||
return null;
|
||||
|
||||
try {
|
||||
Locale locale = Locale.getDefault();
|
||||
if (locale.getLanguage().equals("lzh"))
|
||||
locale = Locale.TRADITIONAL_CHINESE;
|
||||
|
||||
String result = SystemUtils.run(fcMatch.toString(),
|
||||
":lang=" + Locale.getDefault().toLanguageTag(),
|
||||
":lang=" + locale.toLanguageTag(),
|
||||
"--format", "%{family}\\n%{file}").trim();
|
||||
|
||||
String[] results = result.split("\\n");
|
||||
|
||||
@@ -19,22 +19,28 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ProxyManager {
|
||||
|
||||
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
|
||||
private static final SimpleProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT;
|
||||
|
||||
static {
|
||||
ProxySelector systemProxySelector = ProxySelector.getDefault();
|
||||
SYSTEM_DEFAULT = systemProxySelector != null
|
||||
? new ProxySelectorWrapper(systemProxySelector)
|
||||
: NO_PROXY;
|
||||
}
|
||||
|
||||
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
|
||||
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
|
||||
@@ -51,7 +57,7 @@ public final class ProxyManager {
|
||||
LOG.warning("Illegal proxy port: " + port);
|
||||
return NO_PROXY;
|
||||
} else {
|
||||
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
|
||||
return new ProxySelectorWrapper(new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port))));
|
||||
}
|
||||
} else {
|
||||
return ProxyManager.SYSTEM_DEFAULT;
|
||||
@@ -106,15 +112,41 @@ public final class ProxyManager {
|
||||
config().proxyPassProperty().addListener(updateAuthenticator);
|
||||
}
|
||||
|
||||
private static final class SimpleProxySelector extends ProxySelector {
|
||||
private static abstract class AbstractProxySelector extends ProxySelector {
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SimpleProxySelector extends AbstractProxySelector {
|
||||
private final List<Proxy> proxies;
|
||||
|
||||
SimpleProxySelector(Proxy proxy) {
|
||||
this(Collections.singletonList(proxy));
|
||||
this.proxies = List.of(proxy);
|
||||
}
|
||||
|
||||
SimpleProxySelector(List<Proxy> proxies) {
|
||||
this.proxies = proxies;
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
return proxies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps another ProxySelector to avoid using proxy for loopback addresses.
|
||||
private static final class ProxySelectorWrapper extends AbstractProxySelector {
|
||||
private final ProxySelector source;
|
||||
|
||||
ProxySelectorWrapper(ProxySelector source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,19 +154,10 @@ public final class ProxyManager {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
|
||||
return proxies;
|
||||
}
|
||||
if (NetworkUtils.isLoopbackAddress(uri))
|
||||
return NO_PROXY.proxies;
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
return source.select(uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ public enum VersionIconType {
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
QUILT("/assets/img/quilt.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
|
||||
@@ -797,14 +797,6 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
|
||||
if (index == null)
|
||||
return defaultValue;
|
||||
|
||||
int idx = index.getAsInt();
|
||||
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject))
|
||||
@@ -839,14 +831,14 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
|
||||
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
|
||||
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setGameDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("gameDirType"), GameDirectoryType.class, GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
|
||||
vs.setNativesDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("nativesDirType"), NativesDirectoryType.class, NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(parseJsonPrimitive(obj.getAsJsonPrimitive("versionIcon"), VersionIconType.class, VersionIconType.DEFAULT));
|
||||
|
||||
if (obj.get("javaVersionType") != null) {
|
||||
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
@@ -216,7 +217,11 @@ public final class Controllers {
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
&& !Controllers.stage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
|
||||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
|
||||
) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
@@ -321,6 +326,27 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
|
||||
Number shownTipVersion = null;
|
||||
try {
|
||||
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
|
||||
} catch (ClassCastException e) {
|
||||
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
|
||||
}
|
||||
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
|
||||
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
|
||||
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
|
||||
if (downloadLink != null)
|
||||
builder.addHyperLink(
|
||||
i18n("fatal.deprecated_java_version.download_link", Metadata.RECOMMENDED_JAVA_VERSION),
|
||||
downloadLink
|
||||
);
|
||||
Controllers.dialog(builder
|
||||
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
if (globalConfig().getAgreementVersion() < 1) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
|
||||
@@ -25,10 +25,7 @@ import javafx.beans.Observable;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
@@ -832,10 +829,144 @@ public final class FXUtils {
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindColorPicker(ColorPicker colorPicker, Property<Paint> property) {
|
||||
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
|
||||
colorPicker.valueProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<JFXComboBox<String>> comboBoxRef;
|
||||
private final WeakReference<IntegerProperty> widthPropertyRef;
|
||||
private final WeakReference<IntegerProperty> heightPropertyRef;
|
||||
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
|
||||
IntegerProperty widthProperty,
|
||||
IntegerProperty heightProperty) {
|
||||
this.comboBoxRef = new WeakReference<>(comboBox);
|
||||
this.widthPropertyRef = new WeakReference<>(widthProperty);
|
||||
this.heightPropertyRef = new WeakReference<>(heightProperty);
|
||||
this.hashCode = System.identityHashCode(comboBox)
|
||||
^ System.identityHashCode(widthProperty)
|
||||
^ System.identityHashCode(heightProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (!updating) {
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null) {
|
||||
if (comboBox != null) {
|
||||
comboBox.focusedProperty().removeListener(this);
|
||||
comboBox.sceneProperty().removeListener(this);
|
||||
}
|
||||
if (widthProperty != null)
|
||||
widthProperty.removeListener(this);
|
||||
if (heightProperty != null)
|
||||
heightProperty.removeListener(this);
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
int width = widthProperty.get();
|
||||
int height = heightProperty.get();
|
||||
|
||||
if (observable instanceof ReadOnlyProperty<?>
|
||||
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
|
||||
String value = comboBox.valueProperty().get();
|
||||
if (value == null)
|
||||
value = "";
|
||||
int idx = value.indexOf('x');
|
||||
if (idx < 0)
|
||||
idx = value.indexOf('*');
|
||||
|
||||
if (idx < 0) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
String widthStr = value.substring(0, idx).trim();
|
||||
String heightStr = value.substring(idx + 1).trim();
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
try {
|
||||
newWidth = Integer.parseInt(widthStr);
|
||||
newHeight = Integer.parseInt(heightStr);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
widthProperty.set(newWidth);
|
||||
heightProperty.set(newHeight);
|
||||
} else {
|
||||
comboBox.setValue(width + "x" + height);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return this.comboBoxRef.get() == null
|
||||
|| this.widthPropertyRef.get() == null
|
||||
|| this.heightPropertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof WindowsSizeBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (WindowsSizeBidirectionalBinding) obj;
|
||||
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
var thatComboBox = that.comboBoxRef.get();
|
||||
var thatWidthProperty = that.widthPropertyRef.get();
|
||||
var thatHeightProperty = that.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null
|
||||
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return comboBox == thatComboBox
|
||||
&& widthProperty == thatWidthProperty
|
||||
&& heightProperty == thatHeightProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().addListener(binding);
|
||||
comboBox.sceneProperty().addListener(binding);
|
||||
widthProperty.addListener(binding);
|
||||
heightProperty.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().removeListener(binding);
|
||||
comboBox.sceneProperty().removeListener(binding);
|
||||
widthProperty.removeListener(binding);
|
||||
heightProperty.removeListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
|
||||
@@ -30,8 +30,7 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
private Duration duration;
|
||||
private Node previousNode, currentNode;
|
||||
|
||||
{
|
||||
currentNode = getChildren().stream().findFirst().orElse(null);
|
||||
public TransitionPane() {
|
||||
FXUtils.setOverflowHidden(this);
|
||||
}
|
||||
|
||||
@@ -91,8 +90,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
private void updateContent(Node newView) {
|
||||
if (getWidth() > 0 && getHeight() > 0) {
|
||||
previousNode = currentNode;
|
||||
if (previousNode == null)
|
||||
previousNode = EMPTY_PANE;
|
||||
if (previousNode == null) {
|
||||
if (getChildren().isEmpty())
|
||||
previousNode = EMPTY_PANE;
|
||||
else
|
||||
previousNode = getChildren().get(0);
|
||||
}
|
||||
} else
|
||||
previousNode = EMPTY_PANE;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -59,16 +58,10 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
lblTitle = new Label();
|
||||
lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
{
|
||||
taskListPane = new TaskListPane();
|
||||
scrollPane.setContent(taskListPane);
|
||||
}
|
||||
taskListPane = new TaskListPane();
|
||||
VBox.setVgrow(taskListPane, Priority.ALWAYS);
|
||||
|
||||
center.getChildren().setAll(lblTitle, scrollPane);
|
||||
center.getChildren().setAll(lblTitle, taskListPane);
|
||||
}
|
||||
|
||||
BorderPane bottom = new BorderPane();
|
||||
|
||||
@@ -17,15 +17,20 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||
@@ -52,6 +57,7 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthInstallTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
@@ -63,54 +69,86 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
|
||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
|
||||
|
||||
private TaskExecutor executor;
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXListView<Node> listView = new JFXListView<>();
|
||||
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
|
||||
private final List<StageNode> stageNodes = new ArrayList<>();
|
||||
private final Map<String, StageNode> stageNodes = new HashMap<>();
|
||||
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
|
||||
private final DoubleProperty cellWidth = new SimpleDoubleProperty();
|
||||
|
||||
private Cell lastCell;
|
||||
|
||||
public TaskListPane() {
|
||||
listBox.setSpacing(0);
|
||||
listView.setPadding(new Insets(12, 0, 0, 0));
|
||||
listView.setCellFactory(l -> new Cell());
|
||||
listView.setSelectionModel(null);
|
||||
FXUtils.onChangeAndOperate(listView.widthProperty(), width -> {
|
||||
double w = width.doubleValue();
|
||||
cellWidth.set(w <= 12.0 ? w : w - 12.0);
|
||||
});
|
||||
|
||||
getChildren().setAll(listBox);
|
||||
getChildren().setAll(listView);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private void addStages(@NotNull Collection<String> stages) {
|
||||
for (String stage : stages) {
|
||||
stageNodes.computeIfAbsent(stage, s -> {
|
||||
StageNode node = new StageNode(stage);
|
||||
listView.getItems().add(node);
|
||||
return node;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private void updateProgressNodePadding() {
|
||||
progressNodePadding.set(stageNodes.isEmpty() ? DEFAULT_PROGRESS_NODE_PADDING : STAGED_PROGRESS_NODE_PADDING);
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
List<String> stages = Lang.removingDuplicates(executor.getStages());
|
||||
this.executor = executor;
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStart() {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.clear();
|
||||
listBox.clear();
|
||||
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
|
||||
stageNodes.forEach(listBox::add);
|
||||
|
||||
if (stages.isEmpty()) progressNodePadding.setValue(new Insets(0, 0, 8, 0));
|
||||
else progressNodePadding.setValue(new Insets(0, 0, 8, 26));
|
||||
listView.getItems().clear();
|
||||
addStages(executor.getStages());
|
||||
updateProgressNodePadding();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady(Task<?> task) {
|
||||
if (task instanceof Task.StagesHintTask) {
|
||||
Platform.runLater(() -> {
|
||||
addStages(((Task<?>.StagesHintTask) task).getStages());
|
||||
updateProgressNodePadding();
|
||||
});
|
||||
}
|
||||
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
|
||||
StageNode node = stageNodes.get(task.getStage());
|
||||
if (node != null)
|
||||
node.begin();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -123,6 +161,8 @@ public final class TaskListPane extends StackPane {
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
task.setName(i18n("assets.download_all"));
|
||||
} else if (task instanceof GameInstallTask) {
|
||||
if (task.getInheritedStage() != null && task.getInheritedStage().startsWith("hmcl.install.game"))
|
||||
return;
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
|
||||
@@ -156,7 +196,7 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
@@ -169,25 +209,25 @@ public final class TaskListPane extends StackPane {
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
|
||||
listBox.add(listBox.indexOf(stageNode) + 1, node);
|
||||
StageNode stageNode = stageNodes.get(task.getInheritedStage());
|
||||
listView.getItems().add(listView.getItems().indexOf(stageNode) + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task<?> task) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (task.getStage() != null) {
|
||||
StageNode stageNode = stageNodes.get(task.getStage());
|
||||
if (stageNode != null)
|
||||
stageNode.succeed();
|
||||
}
|
||||
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
listBox.remove(node);
|
||||
if (node != null) {
|
||||
node.unbind();
|
||||
listView.getItems().remove(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,25 +235,23 @@ public final class TaskListPane extends StackPane {
|
||||
public void onFailed(Task<?> task, Throwable throwable) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
|
||||
StageNode stageNode = stageNodes.get(task.getStage());
|
||||
if (stageNode != null)
|
||||
stageNode.fail();
|
||||
});
|
||||
}
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
Platform.runLater(() -> {
|
||||
node.setThrowable(throwable);
|
||||
});
|
||||
if (node != null)
|
||||
Platform.runLater(() -> node.setThrowable(throwable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPropertiesUpdate(Task<?> task) {
|
||||
if (task instanceof Task.CountTask) {
|
||||
runInFX(() -> {
|
||||
stageNodes.stream()
|
||||
.filter(x -> x.stage.equals(((Task<?>.CountTask) task).getCountStage()))
|
||||
.findAny()
|
||||
.ifPresent(StageNode::count);
|
||||
StageNode stageNode = stageNodes.get(((Task<?>.CountTask) task).getCountStage());
|
||||
if (stageNode != null)
|
||||
stageNode.count();
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -222,121 +260,251 @@ public final class TaskListPane extends StackPane {
|
||||
if (task.getStage() != null) {
|
||||
int total = tryCast(task.getProperties().get("total"), Integer.class).orElse(0);
|
||||
runInFX(() -> {
|
||||
stageNodes.stream()
|
||||
.filter(x -> x.stage.equals(task.getStage()))
|
||||
.findAny()
|
||||
.ifPresent(stageNode -> {
|
||||
stageNode.setTotal(total);
|
||||
});
|
||||
StageNode stageNode = stageNodes.get(task.getStage());
|
||||
if (stageNode != null)
|
||||
stageNode.addTotal(total);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class StageNode extends BorderPane {
|
||||
private final String stage;
|
||||
private final class Cell extends ListCell<Node> {
|
||||
private static final double STATUS_ICON_SIZE = 14;
|
||||
|
||||
private final BorderPane pane = new BorderPane();
|
||||
private final StackPane left = new StackPane();
|
||||
private final Label title = new Label();
|
||||
private final Label message = new Label();
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
|
||||
private WeakReference<StageNode> prevStageNodeRef;
|
||||
private StatusChangeListener statusChangeListener;
|
||||
|
||||
private Cell() {
|
||||
setPadding(new Insets(0, 0, 4, 0));
|
||||
|
||||
prefWidthProperty().bind(cellWidth);
|
||||
|
||||
FXUtils.setLimitHeight(left, STATUS_ICON_SIZE);
|
||||
FXUtils.setLimitWidth(left, STATUS_ICON_SIZE);
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(left, new Insets(0, 12, 0, 0));
|
||||
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
||||
pane.setCenter(title);
|
||||
|
||||
DoubleBinding barWidth = Bindings.createDoubleBinding(() -> {
|
||||
Insets padding = pane.getPadding();
|
||||
Insets insets = pane.getInsets();
|
||||
return pane.getWidth() - padding.getLeft() - padding.getRight() - insets.getLeft() - insets.getRight();
|
||||
}, pane.paddingProperty(), pane.widthProperty());
|
||||
bar.minWidthProperty().bind(barWidth);
|
||||
bar.prefWidthProperty().bind(barWidth);
|
||||
bar.maxWidthProperty().bind(barWidth);
|
||||
|
||||
setGraphic(pane);
|
||||
}
|
||||
|
||||
private void updateLeftIcon(StageNode.Status status) {
|
||||
left.getChildren().setAll(status.svg.createIcon(Theme.blackFill(), STATUS_ICON_SIZE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Node item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell && !isVisible())
|
||||
return;
|
||||
lastCell = this;
|
||||
|
||||
pane.paddingProperty().unbind();
|
||||
title.textProperty().unbind();
|
||||
message.textProperty().unbind();
|
||||
bar.progressProperty().unbind();
|
||||
|
||||
StageNode prevStageNode;
|
||||
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||
prevStageNode.status.removeListener(statusChangeListener);
|
||||
|
||||
if (item instanceof ProgressListNode) {
|
||||
var progressListNode = (ProgressListNode) item;
|
||||
title.setText(progressListNode.title);
|
||||
message.textProperty().bind(progressListNode.message);
|
||||
bar.progressProperty().bind(progressListNode.progress);
|
||||
|
||||
pane.paddingProperty().bind(progressNodePadding);
|
||||
pane.setLeft(null);
|
||||
pane.setRight(message);
|
||||
pane.setBottom(bar);
|
||||
} else if (item instanceof StageNode) {
|
||||
var stageNode = (StageNode) item;
|
||||
title.textProperty().bind(stageNode.title);
|
||||
message.setText("");
|
||||
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
|
||||
pane.setPadding(Insets.EMPTY);
|
||||
pane.setLeft(left);
|
||||
pane.setRight(message);
|
||||
pane.setBottom(null);
|
||||
|
||||
updateLeftIcon(stageNode.status.get());
|
||||
if (statusChangeListener == null)
|
||||
statusChangeListener = new StatusChangeListener(this);
|
||||
stageNode.status.addListener(statusChangeListener);
|
||||
prevStageNodeRef = new WeakReference<>(stageNode);
|
||||
} else { // item == null
|
||||
title.setText("");
|
||||
message.setText("");
|
||||
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
pane.setPadding(Insets.EMPTY);
|
||||
pane.setLeft(null);
|
||||
pane.setRight(null);
|
||||
pane.setBottom(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StatusChangeListener implements ChangeListener<StageNode.Status>, WeakListener {
|
||||
|
||||
private final WeakReference<Cell> cellRef;
|
||||
|
||||
private StatusChangeListener(Cell cell) {
|
||||
this.cellRef = new WeakReference<>(cell);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return cellRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends StageNode.Status> observable,
|
||||
StageNode.Status oldValue,
|
||||
StageNode.Status newValue) {
|
||||
Cell cell = cellRef.get();
|
||||
if (cell == null) {
|
||||
if (observable != null)
|
||||
observable.removeListener(this);
|
||||
return;
|
||||
}
|
||||
cell.updateLeftIcon(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Node {
|
||||
|
||||
}
|
||||
|
||||
private static final class StageNode extends Node {
|
||||
private enum Status {
|
||||
WAITING(SVG.MORE_HORIZ),
|
||||
RUNNING(SVG.ARROW_FORWARD),
|
||||
SUCCESS(SVG.CHECK),
|
||||
FAILED(SVG.CLOSE);
|
||||
|
||||
private final SVG svg;
|
||||
|
||||
Status(SVG svg) {
|
||||
this.svg = svg;
|
||||
}
|
||||
}
|
||||
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.WAITING);
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final String message;
|
||||
private int count = 0;
|
||||
private int total = 0;
|
||||
private boolean started = false;
|
||||
|
||||
public StageNode(String stage) {
|
||||
this.stage = stage;
|
||||
private StageNode(String stage) {
|
||||
String stageKey;
|
||||
String stageValue;
|
||||
|
||||
String stageKey = StringUtils.substringBefore(stage, ':');
|
||||
String stageValue = StringUtils.substringAfter(stage, ':');
|
||||
int idx = stage.indexOf(':');
|
||||
if (idx >= 0) {
|
||||
stageKey = stage.substring(0, idx);
|
||||
stageValue = stage.substring(idx + 1);
|
||||
} else {
|
||||
stageKey = stage;
|
||||
stageValue = null;
|
||||
}
|
||||
|
||||
// CHECKSTYLE:OFF
|
||||
// @formatter:off
|
||||
switch (stageKey) {
|
||||
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
||||
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
||||
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
|
||||
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||
case "hmcl.install.libraries": message = i18n("libraries.download"); break;
|
||||
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
||||
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
||||
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
|
||||
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
|
||||
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
||||
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
|
||||
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
||||
default: message = i18n(stageKey); break;
|
||||
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
||||
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
|
||||
default: message = i18n(stageKey); break;
|
||||
}
|
||||
// @formatter:on
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
title.setText(message);
|
||||
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
|
||||
setPadding(new Insets(0, 0, 8, 4));
|
||||
setCenter(title);
|
||||
setLeft(FXUtils.limitingSize(SVG.MORE_HORIZ.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
title.set(message);
|
||||
}
|
||||
|
||||
public void begin() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
setLeft(FXUtils.limitingSize(SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
private void begin() {
|
||||
if (status.get() == Status.WAITING) {
|
||||
status.set(Status.RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
public void fail() {
|
||||
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
status.set(Status.FAILED);
|
||||
}
|
||||
|
||||
public void succeed() {
|
||||
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
status.set(Status.SUCCESS);
|
||||
}
|
||||
|
||||
public void count() {
|
||||
updateCounter(++count, total);
|
||||
}
|
||||
|
||||
public void setTotal(int total) {
|
||||
this.total = total;
|
||||
public void addTotal(int n) {
|
||||
this.total += n;
|
||||
updateCounter(count, total);
|
||||
}
|
||||
|
||||
public void updateCounter(int count, int total) {
|
||||
if (total > 0)
|
||||
title.setText(String.format("%s - %d/%d", message, count, total));
|
||||
else
|
||||
title.setText(message);
|
||||
title.setValue(total > 0
|
||||
? message + " - " + count + "/" + total
|
||||
: message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressListNode extends BorderPane {
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
|
||||
getWidth() - getPadding().getLeft() - getPadding().getRight() - getInsets().getLeft() - getInsets().getRight(),
|
||||
paddingProperty(), widthProperty());
|
||||
private static final class ProgressListNode extends Node {
|
||||
private final String title;
|
||||
private final StringProperty message = new SimpleStringProperty("");
|
||||
private final DoubleProperty progress = new SimpleDoubleProperty(0.0);
|
||||
|
||||
public ProgressListNode(Task<?> task) {
|
||||
bar.progressProperty().bind(task.progressProperty());
|
||||
title.setText(task.getName());
|
||||
state.textProperty().bind(task.messageProperty());
|
||||
|
||||
setLeft(title);
|
||||
setRight(state);
|
||||
setBottom(bar);
|
||||
|
||||
bar.minWidthProperty().bind(binding);
|
||||
bar.prefWidthProperty().bind(binding);
|
||||
bar.maxWidthProperty().bind(binding);
|
||||
|
||||
paddingProperty().bind(progressNodePadding);
|
||||
private ProgressListNode(Task<?> task) {
|
||||
this.title = task.getName();
|
||||
message.bind(task.messageProperty());
|
||||
progress.bind(task.progressProperty());
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
message.unbind();
|
||||
progress.unbind();
|
||||
}
|
||||
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
message.set(throwable.getLocalizedMessage());
|
||||
progress.set(0.);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
@@ -61,6 +58,7 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -167,6 +165,17 @@ public class DecoratorController {
|
||||
// press ESC to go back
|
||||
onEscPressed(navigator, this::back);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
// press F11 to toggle full screen
|
||||
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// For JavaFX 12+
|
||||
MouseButton button = MouseButton.valueOf("BACK");
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -29,6 +32,7 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -43,6 +47,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane root, parent;
|
||||
@@ -50,6 +55,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
private final InvalidationListener onWindowsStatusChange;
|
||||
|
||||
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
|
||||
|
||||
/**
|
||||
@@ -80,9 +87,34 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
skinnable.getSnackbar().registerSnackbarContainer(parent);
|
||||
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
|
||||
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
|
||||
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
|
||||
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
onWindowsStatusChange = observable -> {
|
||||
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
|
||||
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
} else {
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
};
|
||||
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
|
||||
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
|
||||
onWindowsStatusChange.invalidated(null);
|
||||
} else {
|
||||
onWindowsStatusChange = null;
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
|
||||
shadowContainer.getChildren().setAll(parent);
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
@@ -156,6 +188,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
BorderPane titleBar = new BorderPane();
|
||||
titleContainer.getChildren().add(titleBar);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
primaryStage.setMaximized(!primaryStage.isMaximized());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
|
||||
@@ -85,6 +85,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
|
||||
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
|
||||
if ("game".equals(remoteVersion.getLibraryId())) {
|
||||
stages.add("hmcl.install.libraries");
|
||||
stages.add("hmcl.install.assets");
|
||||
}
|
||||
} else if (value instanceof RemoveVersionAction) {
|
||||
|
||||
@@ -17,24 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
@@ -55,291 +48,67 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final String libraryId;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
private final JFXSpinner spinner;
|
||||
private final StackPane failedPane;
|
||||
private final StackPane emptyPane;
|
||||
private final TransitionPane root;
|
||||
private final JFXCheckBox chkRelease;
|
||||
private final JFXCheckBox chkSnapshot;
|
||||
private final JFXCheckBox chkOld;
|
||||
private final ComponentList centrePane;
|
||||
private final StackPane center;
|
||||
|
||||
private final VersionList<?> versionList;
|
||||
private Task<?> executor;
|
||||
private final Runnable callback;
|
||||
|
||||
private final HBox searchBar;
|
||||
private final StringProperty queryString = new SimpleStringProperty();
|
||||
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.navigation = navigation;
|
||||
|
||||
HintPane hintPane = new HintPane();
|
||||
hintPane.setText(i18n("sponsor.bmclapi"));
|
||||
hintPane.getStyleClass().add("sponsor-pane");
|
||||
FXUtils.onClicked(hintPane, this::onSponsor);
|
||||
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
|
||||
this.setTop(hintPane);
|
||||
|
||||
root = new TransitionPane();
|
||||
BorderPane toolbarPane = new BorderPane();
|
||||
JFXButton btnRefresh;
|
||||
{
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
center = new StackPane();
|
||||
center.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
HBox checkPane = new HBox();
|
||||
checkPane.setSpacing(10);
|
||||
{
|
||||
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
|
||||
chkRelease.setSelected(true);
|
||||
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
|
||||
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkOld = new JFXCheckBox(i18n("version.game.old"));
|
||||
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
|
||||
|
||||
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld);
|
||||
}
|
||||
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
TransitionPane rightToolbarPane = new TransitionPane();
|
||||
{
|
||||
HBox refreshPane = new HBox();
|
||||
refreshPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnRefresh = new JFXButton(i18n("button.refresh"));
|
||||
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRefresh.setOnAction(e -> onRefresh());
|
||||
|
||||
JFXButton btnSearch = new JFXButton(i18n("search"));
|
||||
btnSearch.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
searchBar = new HBox();
|
||||
{
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 0));
|
||||
|
||||
JFXTextField searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
|
||||
JFXButton closeSearchBar = new JFXButton();
|
||||
closeSearchBar.getStyleClass().add("jfx-tool-bar-button");
|
||||
closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1)));
|
||||
closeSearchBar.setOnAction(e -> {
|
||||
searchField.clear();
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE);
|
||||
});
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> queryString.set(searchField.getText()));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
btnSearch.setOnAction(e -> {
|
||||
rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE);
|
||||
searchField.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh));
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE);
|
||||
}
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
toolbarPane.setLeft(checkPane);
|
||||
toolbarPane.setRight(rightToolbarPane);
|
||||
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
}
|
||||
|
||||
center.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, this::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, this::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
}
|
||||
this.setCenter(root);
|
||||
|
||||
versionList = downloadProvider.getVersionListById(libraryId);
|
||||
boolean hasType = versionList.hasType();
|
||||
chkRelease.setManaged(hasType);
|
||||
chkRelease.setVisible(hasType);
|
||||
chkSnapshot.setManaged(hasType);
|
||||
chkSnapshot.setVisible(hasType);
|
||||
chkOld.setManaged(hasType);
|
||||
chkOld.setVisible(hasType);
|
||||
|
||||
if (hasType) {
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
} else {
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
List<RemoteVersion> versions = loadVersions();
|
||||
String query = queryString.get();
|
||||
if (!StringUtils.isBlank(query)) {
|
||||
Predicate<RemoteVersion> predicate;
|
||||
if (query.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(query.substring("regex:".length()));
|
||||
predicate = it -> pattern.matcher(it.getSelfVersion()).find();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression", e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = query.toLowerCase(Locale.ROOT);
|
||||
predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString);
|
||||
}
|
||||
|
||||
versions = versions.stream().filter(predicate).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
list.getItems().setAll(versions);
|
||||
};
|
||||
chkRelease.selectedProperty().addListener(listener);
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
queryString.addListener(listener);
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
|
||||
callback.run();
|
||||
});
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
this.callback = callback;
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private List<RemoteVersion> loadVersions() {
|
||||
return versionList.getVersions(gameVersion).stream()
|
||||
.filter(it -> {
|
||||
switch (it.getVersionType()) {
|
||||
case RELEASE:
|
||||
return chkRelease.isSelected();
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
return chkSnapshot.isSelected();
|
||||
case OLD:
|
||||
return chkOld.isSelected();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.sorted().collect(Collectors.toList());
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new VersionsPageSkin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
VersionList<?> currentVersionList = versionList;
|
||||
root.setContent(spinner, ContainerAnimations.FADE);
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
List<RemoteVersion> items = loadVersions();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
|
||||
root.setContent(emptyPane, ContainerAnimations.FADE);
|
||||
status.set(Status.LOADING);
|
||||
Task<?> task = versionList.refreshAsync(gameVersion)
|
||||
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
|
||||
.whenComplete(Schedulers.javafx(), (items, exception) -> {
|
||||
if (exception == null) {
|
||||
versions.setAll(items);
|
||||
status.set(Status.SUCCESS);
|
||||
} else {
|
||||
if (items.isEmpty()) {
|
||||
chkRelease.setSelected(true);
|
||||
chkSnapshot.setSelected(true);
|
||||
chkOld.setSelected(true);
|
||||
} else {
|
||||
list.getItems().setAll(items);
|
||||
}
|
||||
root.setContent(center, ContainerAnimations.FADE);
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
status.set(Status.FAILED);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
root.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
});
|
||||
executor.start();
|
||||
task.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -350,9 +119,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
settings.remove(libraryId);
|
||||
// fixme
|
||||
// if (executor != null)
|
||||
// executor.cancel(true);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
@@ -367,10 +133,24 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
|
||||
}
|
||||
|
||||
private enum Status {
|
||||
LOADING,
|
||||
FAILED,
|
||||
SUCCESS,
|
||||
}
|
||||
|
||||
private enum VersionTypeFilter {
|
||||
ALL,
|
||||
RELEASE,
|
||||
SNAPSHOTS,
|
||||
APRIL_FOOLS,
|
||||
OLD
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
private final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
private final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
private final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
|
||||
@@ -401,7 +181,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
content.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
} else {
|
||||
@@ -409,22 +189,30 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
String wikiSuffix = getWikiUrlSuffix(remoteVersion.getGameVersion());
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(VersionIconType.GRASS.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
content.getTags().setAll(i18n("version.game.april_fools"));
|
||||
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
content.setExternalLink(null);
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -452,5 +240,285 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
content.setExternalLink(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getWikiUrlSuffix(String gameVersion) {
|
||||
String id = gameVersion.toLowerCase(Locale.ROOT);
|
||||
|
||||
switch (id) {
|
||||
case "0.30-1":
|
||||
case "0.30-2":
|
||||
case "c0.30_01c":
|
||||
return i18n("wiki.version.game.search", "Classic_0.30");
|
||||
case "in-20100206-2103":
|
||||
return i18n("wiki.version.game.search", "Indev_20100206");
|
||||
case "inf-20100630-1":
|
||||
return i18n("wiki.version.game.search", "Infdev_20100630");
|
||||
case "inf-20100630-2":
|
||||
return i18n("wiki.version.game.search", "Alpha_v1.0.0");
|
||||
case "1.19_deep_dark_experimental_snapshot-1":
|
||||
return "1.19-exp1";
|
||||
case "in-20100130":
|
||||
return i18n("wiki.version.game.search", "Indev_0.31_20100130");
|
||||
case "b1.6-tb3":
|
||||
return i18n("wiki.version.game.search", "Beta_1.6_Test_Build_3");
|
||||
case "1.14_combat-212796":
|
||||
return i18n("wiki.version.game.search", "1.14.3_-_Combat_Test");
|
||||
case "1.14_combat-0":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_2");
|
||||
case "1.14_combat-3":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_3");
|
||||
case "1_15_combat-1":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_4");
|
||||
case "1_15_combat-6":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_5");
|
||||
case "1_16_combat-0":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_6");
|
||||
case "1_16_combat-1":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7");
|
||||
case "1_16_combat-2":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7b");
|
||||
case "1_16_combat-3":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7c");
|
||||
case "1_16_combat-4":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8");
|
||||
case "1_16_combat-5":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8b");
|
||||
case "1_16_combat-6":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8c");
|
||||
}
|
||||
|
||||
if (id.startsWith("1.0.0-rc2")) return "RC2";
|
||||
if (id.startsWith("2.0")) return i18n("wiki.version.game.search", "2.0");
|
||||
if (id.startsWith("b1.8-pre1")) return "Beta_1.8-pre1";
|
||||
if (id.startsWith("b1.1-")) return i18n("wiki.version.game.search", "Beta_1.1");
|
||||
if (id.startsWith("a1.1.0")) return "Alpha_v1.1.0";
|
||||
if (id.startsWith("a1.0.14")) return "Alpha_v1.0.14";
|
||||
if (id.startsWith("a1.0.13_01")) return "Alpha_v1.0.13_01";
|
||||
if (id.startsWith("in-20100214")) return i18n("wiki.version.game.search", "Indev_20100214");
|
||||
|
||||
if (id.contains("experimental-snapshot")) {
|
||||
return id.replace("_experimental-snapshot-", "-exp");
|
||||
}
|
||||
|
||||
if (id.startsWith("inf-")) return id.replace("inf-", "Infdev_");
|
||||
if (id.startsWith("in-")) return id.replace("in-", "Indev_");
|
||||
if (id.startsWith("rd-")) return "pre-Classic_" + id;
|
||||
if (id.startsWith("b")) return id.replace("b", "Beta_");
|
||||
if (id.startsWith("a")) return id.replace("a", "Alpha_v");
|
||||
if (id.startsWith("c")) return id.replace("c", "Classic_").replace("st", "SURVIVAL_TEST");
|
||||
|
||||
return i18n("wiki.version.game.search", id);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
|
||||
private final TransitionPane transitionPane;
|
||||
private final JFXSpinner spinner;
|
||||
|
||||
private final JFXTextField nameField;
|
||||
private final JFXComboBox<VersionTypeFilter> categoryField = new JFXComboBox<>();
|
||||
|
||||
VersionsPageSkin(VersionsPage control) {
|
||||
super(control);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
root.setTop(searchPane);
|
||||
searchPane.getStyleClass().addAll("card");
|
||||
BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10));
|
||||
|
||||
ColumnConstraints nameColumn = new ColumnConstraints();
|
||||
nameColumn.setMinWidth(USE_PREF_SIZE);
|
||||
ColumnConstraints column1 = new ColumnConstraints();
|
||||
column1.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints column2 = new ColumnConstraints();
|
||||
column2.setMaxWidth(150);
|
||||
ColumnConstraints column3 = new ColumnConstraints();
|
||||
|
||||
if (control.versionList.hasType())
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
|
||||
else
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
|
||||
|
||||
searchPane.setHgap(16);
|
||||
searchPane.setVgap(10);
|
||||
|
||||
{
|
||||
int rowIndex = 0;
|
||||
|
||||
{
|
||||
nameField = new JFXTextField();
|
||||
nameField.setPromptText(i18n("version.search.prompt"));
|
||||
nameField.textProperty().addListener(o -> updateList());
|
||||
|
||||
if ("game".equals(control.libraryId)) {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS,
|
||||
VersionTypeFilter.APRIL_FOOLS,
|
||||
VersionTypeFilter.OLD
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
|
||||
} else {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
|
||||
}
|
||||
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
|
||||
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
|
||||
|
||||
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
refreshButton.setOnAction(event -> control.onRefresh());
|
||||
|
||||
if (control.versionList.hasType()) {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
new Label(i18n("version.game.type")), categoryField,
|
||||
refreshButton
|
||||
);
|
||||
} else {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
refreshButton
|
||||
);
|
||||
}
|
||||
}
|
||||
// {
|
||||
// HBox actionsBox = new HBox(8);
|
||||
// GridPane.setColumnSpan(actionsBox, 4);
|
||||
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
//
|
||||
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
// refreshButton.setOnAction(event -> control.onRefresh());
|
||||
//
|
||||
// actionsBox.getChildren().setAll(refreshButton);
|
||||
//
|
||||
// searchPane.addRow(rowIndex++, actionsBox);
|
||||
// }
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
root.setCenter(spinnerPane);
|
||||
|
||||
transitionPane = new TransitionPane();
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
StackPane centerWrapper = new StackPane();
|
||||
centerWrapper.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
ComponentList centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
control.versions.addListener((InvalidationListener) o -> updateList());
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem());
|
||||
control.callback.run();
|
||||
});
|
||||
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
|
||||
centerWrapper.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
StackPane failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, control::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
StackPane emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, control::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(control.status, status -> {
|
||||
if (status == Status.LOADING)
|
||||
transitionPane.setContent(spinner, ContainerAnimations.FADE);
|
||||
else if (status == Status.SUCCESS)
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
else // if (status == Status.FAILED)
|
||||
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
root.setCenter(transitionPane);
|
||||
}
|
||||
|
||||
this.getChildren().setAll(root);
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
|
||||
|
||||
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
switch (filter) {
|
||||
case RELEASE:
|
||||
return versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD:
|
||||
return versionType == RemoteVersion.Type.OLD;
|
||||
case ALL:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
String nameQuery = nameField.getText();
|
||||
if (!StringUtils.isBlank(nameQuery)) {
|
||||
if (nameQuery.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length()));
|
||||
versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find());
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression: " + nameQuery, e);
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT);
|
||||
versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString));
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
list.getItems().setAll(versions.collect(Collectors.toList()));
|
||||
list.scrollTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
File tempModpack;
|
||||
Task<?> exportTask;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
@@ -165,6 +169,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency = null;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
@@ -181,6 +189,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
@@ -225,6 +237,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
@@ -241,6 +257,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ModrinthModpackExportTask(
|
||||
|
||||
@@ -31,6 +31,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
@@ -42,10 +43,16 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -61,16 +68,24 @@ import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.download.RemoteVersion.Type.RELEASE;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private static final String ANNOUNCEMENT = "announcement";
|
||||
@@ -211,7 +226,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchButton.setPrefWidth(230);
|
||||
launchButton.setPrefHeight(55);
|
||||
//launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.setOnAction(e -> launch());
|
||||
launchButton.setDefaultButton(true);
|
||||
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
{
|
||||
@@ -219,18 +233,34 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
graphic.setAlignment(Pos.CENTER);
|
||||
graphic.setTranslateX(-7);
|
||||
graphic.setMaxWidth(200);
|
||||
Label launchLabel = new Label(i18n("version.launch"));
|
||||
Label launchLabel = new Label();
|
||||
launchLabel.setStyle("-fx-font-size: 16px;");
|
||||
Label currentLabel = new Label();
|
||||
currentLabel.setStyle("-fx-font-size: 12px;");
|
||||
currentLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (getCurrentGame() == null) {
|
||||
return i18n("version.empty");
|
||||
} else {
|
||||
return getCurrentGame();
|
||||
|
||||
FXUtils.onChangeAndOperate(currentGameProperty(), new Consumer<>() {
|
||||
private Tooltip tooltip;
|
||||
|
||||
@Override
|
||||
public void accept(String currentGame) {
|
||||
if (currentGame == null) {
|
||||
launchLabel.setText(i18n("version.launch.empty"));
|
||||
currentLabel.setText(null);
|
||||
graphic.getChildren().setAll(launchLabel);
|
||||
launchButton.setOnAction(e -> MainPage.this.launchNoGame());
|
||||
if (tooltip == null)
|
||||
tooltip = new Tooltip(i18n("version.launch.empty.tooltip"));
|
||||
FXUtils.installFastTooltip(launchButton, tooltip);
|
||||
} else {
|
||||
launchLabel.setText(i18n("version.launch"));
|
||||
currentLabel.setText(currentGame);
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
launchButton.setOnAction(e -> MainPage.this.launch());
|
||||
if (tooltip != null)
|
||||
Tooltip.uninstall(launchButton, tooltip);
|
||||
}
|
||||
}
|
||||
}, currentGameProperty()));
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
});
|
||||
|
||||
launchButton.setGraphic(graphic);
|
||||
}
|
||||
@@ -319,7 +349,46 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void launch() {
|
||||
Versions.launch(Profiles.getSelectedProfile());
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), null);
|
||||
}
|
||||
|
||||
private void launchNoGame() {
|
||||
DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
VersionList<?> versionList = downloadProvider.getVersionListById("game");
|
||||
|
||||
Holder<String> gameVersionHolder = new Holder<>();
|
||||
Task<?> task = versionList.refreshAsync("")
|
||||
.thenSupplyAsync(() -> versionList.getVersions("").stream()
|
||||
.filter(it -> it.getVersionType() == RELEASE)
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("No versions found")))
|
||||
.thenComposeAsync(version -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
DefaultDependencyManager dependency = profile.getDependency();
|
||||
String gameVersion = gameVersionHolder.value = version.getGameVersion();
|
||||
|
||||
return dependency.gameBuilder()
|
||||
.name(gameVersion)
|
||||
.gameVersion(gameVersion)
|
||||
.buildAsync();
|
||||
})
|
||||
.whenComplete(any -> profile.getRepository().refreshVersions())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
profile.setSelectedVersion(gameVersionHolder.value);
|
||||
launch();
|
||||
} else if (exception instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
LOG.warning("Failed to install game", exception);
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
});
|
||||
Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
private void onMenu() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -34,22 +35,30 @@ import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class SettingsPage extends SettingsView {
|
||||
|
||||
@@ -61,6 +70,8 @@ public final class SettingsPage extends SettingsView {
|
||||
// ==== Languages ====
|
||||
cboLanguage.getItems().setAll(Locales.LOCALES);
|
||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
// ====
|
||||
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
@@ -123,22 +134,73 @@ public final class SettingsPage extends SettingsView {
|
||||
|
||||
@Override
|
||||
protected void onExportLogs() {
|
||||
// We cannot determine which file is JUL using.
|
||||
// So we write all the logs to a new file.
|
||||
thread(() -> {
|
||||
Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
|
||||
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
|
||||
|
||||
LOG.info("Exporting logs to " + logFile);
|
||||
try (OutputStream output = Files.newOutputStream(logFile)) {
|
||||
LOG.exportLogs(output);
|
||||
Path outputFile;
|
||||
try {
|
||||
if (recentLogFiles.isEmpty()) {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (OutputStream output = Files.newOutputStream(outputFile)) {
|
||||
LOG.exportLogs(output);
|
||||
}
|
||||
} else {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
|
||||
Path tempFile = Files.createTempFile("hmcl-decompress-log-", ".txt");
|
||||
try (var tempChannel = FileChannel.open(tempFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
|
||||
for (Path path : recentLogFiles) {
|
||||
String extension = FileUtils.getExtension(path);
|
||||
decompress:
|
||||
if ("gz".equalsIgnoreCase(extension) || "xz".equalsIgnoreCase(extension)) {
|
||||
try (InputStream fis = Files.newInputStream(path);
|
||||
InputStream uncompressed = "gz".equalsIgnoreCase(extension)
|
||||
? new GZIPInputStream(fis)
|
||||
: new XZInputStream(fis)) {
|
||||
uncompressed.transferTo(Channels.newOutputStream(tempChannel));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to decompress log: " + path, e);
|
||||
break decompress;
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(StringUtils.substringBeforeLast(FileUtils.getName(path), '.')));
|
||||
Channels.newInputStream(tempChannel).transferTo(zos);
|
||||
zos.closeEntry();
|
||||
tempChannel.truncate(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(FileUtils.getName(path)));
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry("hmcl-latest.log"));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
LOG.warning("Failed to export logs", e);
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
|
||||
FXUtils.showFileInExplorer(outputFile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -49,6 +51,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public abstract class SettingsView extends StackPane {
|
||||
protected final JFXComboBox<SupportedLocale> cboLanguage;
|
||||
protected final OptionToggleButton disableAutoGameOptionsPane;
|
||||
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
|
||||
protected final ComponentSublist fileCommonLocationSublist;
|
||||
protected final Label lblUpdate;
|
||||
@@ -177,14 +180,28 @@ public abstract class SettingsView extends StackPane {
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
languagePane.setLeft(left);
|
||||
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
cboLanguage = new JFXComboBox<>();
|
||||
cboLanguage.setConverter(stringConverter(I18n::getName));
|
||||
cboLanguage.setConverter(stringConverter(locale -> {
|
||||
if (locale.isSameLanguage(currentLocale) || locale == Locales.DEFAULT)
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
}));
|
||||
|
||||
FXUtils.setLimitWidth(cboLanguage, 300);
|
||||
languagePane.setRight(cboLanguage);
|
||||
|
||||
settingsPane.getContent().add(languagePane);
|
||||
}
|
||||
|
||||
{
|
||||
disableAutoGameOptionsPane = new OptionToggleButton();
|
||||
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
|
||||
|
||||
settingsPane.getContent().add(disableAutoGameOptionsPane);
|
||||
}
|
||||
|
||||
{
|
||||
BorderPane debugPane = new BorderPane();
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -68,7 +70,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
|
||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
@@ -90,6 +91,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private int searchID = 0;
|
||||
protected RemoteModRepository repository;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
private Runnable retrySearch;
|
||||
|
||||
@@ -101,6 +103,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.repository = repository;
|
||||
this.callback = callback;
|
||||
this.versionSelection = versionSelection;
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
}
|
||||
|
||||
public ObservableList<Node> getActions() {
|
||||
@@ -171,7 +174,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
: "";
|
||||
}
|
||||
}).thenApplyAsync(
|
||||
gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
|
||||
gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (searchID != currentSearchID) {
|
||||
return;
|
||||
@@ -231,6 +234,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
@@ -543,7 +547,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
|
||||
LOG.debug("Icon: " + dataItem.getIconUrl());
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class VersionIconDialog extends DialogPane {
|
||||
createIcon(VersionIconType.CHEST),
|
||||
createIcon(VersionIconType.CHICKEN),
|
||||
createIcon(VersionIconType.COMMAND),
|
||||
createIcon(VersionIconType.APRIL_FOOLS),
|
||||
createIcon(VersionIconType.OPTIFINE),
|
||||
createIcon(VersionIconType.CRAFT_TABLE),
|
||||
createIcon(VersionIconType.FABRIC),
|
||||
|
||||
@@ -26,12 +26,14 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
@@ -93,8 +95,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private String versionId;
|
||||
|
||||
private final VBox rootPane;
|
||||
private final JFXTextField txtWidth;
|
||||
private final JFXTextField txtHeight;
|
||||
private final JFXComboBox<String> cboWindowsSize;
|
||||
private final JFXTextField txtServerIP;
|
||||
private final ComponentList componentList;
|
||||
private final JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||
@@ -389,29 +390,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
BorderPane right = new BorderPane();
|
||||
dimensionPane.setRight(right);
|
||||
{
|
||||
HBox hbox = new HBox();
|
||||
right.setLeft(hbox);
|
||||
hbox.setPrefWidth(210);
|
||||
hbox.setSpacing(3);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
BorderPane.setAlignment(hbox, Pos.CENTER);
|
||||
{
|
||||
txtWidth = new JFXTextField();
|
||||
txtWidth.setPromptText("800");
|
||||
txtWidth.setPrefWidth(100);
|
||||
FXUtils.setValidateWhileTextChanged(txtWidth, true);
|
||||
txtWidth.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
|
||||
|
||||
Label x = new Label("x");
|
||||
|
||||
txtHeight = new JFXTextField();
|
||||
txtHeight.setPromptText("480");
|
||||
txtHeight.setPrefWidth(100);
|
||||
FXUtils.setValidateWhileTextChanged(txtHeight, true);
|
||||
txtHeight.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
|
||||
|
||||
hbox.getChildren().setAll(txtWidth, x, txtHeight);
|
||||
}
|
||||
cboWindowsSize = new JFXComboBox<>();
|
||||
cboWindowsSize.setPrefWidth(150);
|
||||
right.setLeft(cboWindowsSize);
|
||||
cboWindowsSize.setEditable(true);
|
||||
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
|
||||
cboWindowsSize.setPromptText("854x480");
|
||||
cboWindowsSize.getItems().setAll(getSupportedResolutions());
|
||||
|
||||
chkFullscreen = new JFXCheckBox();
|
||||
right.setRight(chkFullscreen);
|
||||
@@ -419,6 +404,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
chkFullscreen.setAlignment(Pos.CENTER);
|
||||
BorderPane.setAlignment(chkFullscreen, Pos.CENTER);
|
||||
BorderPane.setMargin(chkFullscreen, new Insets(0, 0, 0, 7));
|
||||
|
||||
cboWindowsSize.disableProperty().bind(chkFullscreen.selectedProperty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,8 +546,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
// unbind data fields
|
||||
if (lastVersionSetting != null) {
|
||||
FXUtils.unbind(txtWidth, lastVersionSetting.widthProperty());
|
||||
FXUtils.unbind(txtHeight, lastVersionSetting.heightProperty());
|
||||
FXUtils.unbindWindowsSize(cboWindowsSize, lastVersionSetting.widthProperty(), lastVersionSetting.heightProperty());
|
||||
maxMemory.unbindBidirectional(lastVersionSetting.maxMemoryProperty());
|
||||
javaCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.javaDirProperty());
|
||||
gameDirCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.gameDirProperty());
|
||||
@@ -593,8 +579,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
javaVersionOption.valueProperty().unbind();
|
||||
|
||||
// bind new data fields
|
||||
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
|
||||
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
|
||||
FXUtils.bindWindowsSize(cboWindowsSize, versionSetting.widthProperty(), versionSetting.heightProperty());
|
||||
maxMemory.bindBidirectional(versionSetting.maxMemoryProperty());
|
||||
|
||||
javaCustomOption.bindBidirectional(versionSetting.javaDirProperty());
|
||||
@@ -794,6 +779,28 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
|
||||
}
|
||||
|
||||
private static List<String> getSupportedResolutions() {
|
||||
int maxScreenWidth = 0;
|
||||
int maxScreenHeight = 0;
|
||||
|
||||
for (Screen screen : Screen.getScreens()) {
|
||||
Rectangle2D bounds = screen.getBounds();
|
||||
int screenWidth = (int) (bounds.getWidth() * screen.getOutputScaleX());
|
||||
int screenHeight = (int) (bounds.getHeight() * screen.getOutputScaleY());
|
||||
|
||||
maxScreenWidth = Math.max(maxScreenWidth, screenWidth);
|
||||
maxScreenHeight = Math.max(maxScreenHeight, screenHeight);
|
||||
}
|
||||
|
||||
List<String> resolutions = new ArrayList<>(List.of("854x480", "1280x720", "1600x900"));
|
||||
|
||||
if (maxScreenWidth >= 1920 && maxScreenHeight >= 1080) resolutions.add("1920x1080");
|
||||
if (maxScreenWidth >= 2560 && maxScreenHeight >= 1440) resolutions.add("2560x1440");
|
||||
if (maxScreenWidth >= 3840 && maxScreenHeight >= 2160) resolutions.add("3840x2160");
|
||||
|
||||
return resolutions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
|
||||
@@ -205,14 +205,6 @@ public final class Versions {
|
||||
});
|
||||
}
|
||||
|
||||
public static void launch(Profile profile) {
|
||||
launch(profile, profile.getSelectedVersion());
|
||||
}
|
||||
|
||||
public static void launch(Profile profile, String id) {
|
||||
launch(profile, id, null);
|
||||
}
|
||||
|
||||
public static void launch(Profile profile, String id, Consumer<LauncherHelper> injecter) {
|
||||
if (!checkVersionForLaunching(profile, id))
|
||||
return;
|
||||
|
||||
@@ -361,14 +361,27 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
playerGameTypePane.setRight(gameTypeBox);
|
||||
|
||||
Tag tag = player.get("playerGameType");
|
||||
Tag hardcoreTag = dataTag.get("hardcore");
|
||||
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
||||
|
||||
if (tag instanceof IntTag) {
|
||||
IntTag intTag = (IntTag) tag;
|
||||
GameType gameType = GameType.of(intTag.getValue());
|
||||
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
||||
if (gameType != null) {
|
||||
gameTypeBox.setValue(gameType);
|
||||
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
intTag.setValue(newValue.ordinal());
|
||||
if (newValue == GameType.HARDCORE) {
|
||||
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
||||
if (hardcoreTag instanceof ByteTag) {
|
||||
((ByteTag) hardcoreTag).setValue((byte) 1);
|
||||
}
|
||||
} else {
|
||||
intTag.setValue(newValue.ordinal());
|
||||
if (hardcoreTag instanceof ByteTag) {
|
||||
((ByteTag) hardcoreTag).setValue((byte) 0);
|
||||
}
|
||||
}
|
||||
saveLevelDat();
|
||||
}
|
||||
});
|
||||
@@ -606,12 +619,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
|
||||
private enum GameType {
|
||||
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR;
|
||||
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR, HARDCORE;
|
||||
|
||||
static final ObservableList<GameType> items = FXCollections.observableList(Arrays.asList(values()));
|
||||
|
||||
static GameType of(int d) {
|
||||
return d >= 0 && d <= items.size() ? items.get(d) : null;
|
||||
static GameType of(int d, boolean hardcore) {
|
||||
if (hardcore && d == 0) return HARDCORE; // hardcore + survival
|
||||
return d >= 0 && d < 4 ? items.get(d) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,14 +18,9 @@
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
final class HMCLDownloadTask extends FileDownloadTask {
|
||||
|
||||
@@ -45,15 +40,6 @@ final class HMCLDownloadTask extends FileDownloadTask {
|
||||
switch (archiveFormat) {
|
||||
case JAR:
|
||||
break;
|
||||
|
||||
case PACK_XZ:
|
||||
byte[] raw = Files.readAllBytes(target);
|
||||
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
|
||||
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
|
||||
Pack200Utils.unpack(in, out);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class RemoteVersion {
|
||||
public final class RemoteVersion {
|
||||
|
||||
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
|
||||
try {
|
||||
@@ -36,12 +35,8 @@ public class RemoteVersion {
|
||||
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
|
||||
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
|
||||
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
|
||||
return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash), force);
|
||||
} else if (jarUrl != null && jarHash != null) {
|
||||
if (jarUrl != null && jarHash != null) {
|
||||
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
|
||||
} else {
|
||||
throw new IOException("No download url is available");
|
||||
@@ -97,7 +92,6 @@ public class RemoteVersion {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
PACK_XZ,
|
||||
JAR
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,9 @@ public final class ChunkBaseApp {
|
||||
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
|
||||
|
||||
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
||||
"1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3", "1.19",
|
||||
"1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12", "1.11",
|
||||
"1.10", "1.9", "1.8", "1.7"
|
||||
"1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3",
|
||||
"1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12",
|
||||
"1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
};
|
||||
|
||||
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
|
||||
|
||||
@@ -26,7 +26,10 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WindowsVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -178,7 +181,19 @@ public final class NativePatcher {
|
||||
return version.setLibraries(newLibraries);
|
||||
}
|
||||
|
||||
public static Library getMesaLoader(JavaRuntime javaVersion) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull WindowsVersion windowsVersion) {
|
||||
if (renderer == Renderer.DEFAULT)
|
||||
return null;
|
||||
|
||||
if (windowsVersion.compareTo(WindowsVersion.WINDOWS_10) >= 0) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
} else if (windowsVersion.compareTo(WindowsVersion.WINDOWS_7) >= 0) {
|
||||
if (renderer == Renderer.LLVMPIPE)
|
||||
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
|
||||
else
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
@@ -63,6 +62,7 @@ import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
@@ -261,9 +261,15 @@ public final class SelfDependencyPatcher {
|
||||
.map(DependencyDescriptor::localPath)
|
||||
.toArray(Path[]::new);
|
||||
|
||||
String[] addOpens = JarUtils.getManifestAttribute("Add-Opens", "").split(" ");
|
||||
String addOpens = null;
|
||||
try (InputStream input = SelfDependencyPatcher.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
|
||||
if (input != null)
|
||||
addOpens = new Manifest(input).getMainAttributes().getValue("Add-Opens");
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read MANIFEST.MF file", e);
|
||||
}
|
||||
|
||||
JavaFXPatcher.patch(modules, jars, addOpens);
|
||||
JavaFXPatcher.patch(modules, jars, addOpens != null ? addOpens.split(" ") : new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,10 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.*;
|
||||
|
||||
@@ -33,26 +32,24 @@ public final class I18n {
|
||||
|
||||
private static volatile SupportedLocale locale = Locales.DEFAULT;
|
||||
private static volatile ResourceBundle resourceBundle = locale.getResourceBundle();
|
||||
private static volatile DateTimeFormatter dateTimeFormatter;
|
||||
|
||||
public static void setLocale(SupportedLocale locale) {
|
||||
I18n.locale = locale;
|
||||
resourceBundle = locale.getResourceBundle();
|
||||
dateTimeFormatter = null;
|
||||
}
|
||||
|
||||
public static SupportedLocale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public static boolean isUseChinese() {
|
||||
return locale.getLocale() == Locale.CHINA;
|
||||
return locale.getLocale().getLanguage().equals("zh");
|
||||
}
|
||||
|
||||
public static ResourceBundle getResourceBundle() {
|
||||
return resourceBundle;
|
||||
}
|
||||
|
||||
public static String getName(SupportedLocale locale) {
|
||||
return locale == Locales.DEFAULT ? resourceBundle.getString("lang.default") : locale.getResourceBundle().getString("lang");
|
||||
}
|
||||
|
||||
public static String i18n(String key, Object... formatArgs) {
|
||||
try {
|
||||
return String.format(getResourceBundle().getString(key), formatArgs);
|
||||
@@ -75,12 +72,11 @@ public final class I18n {
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
DateTimeFormatter formatter = dateTimeFormatter;
|
||||
if (formatter == null) {
|
||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format")).withZone(ZoneId.systemDefault());
|
||||
}
|
||||
return locale.formatDateTime(time);
|
||||
}
|
||||
|
||||
return formatter.format(time);
|
||||
public static String getDisplaySelfVersion(RemoteVersion version) {
|
||||
return locale.getDisplaySelfVersion(version);
|
||||
}
|
||||
|
||||
public static boolean hasKey(String key) {
|
||||
|
||||
@@ -20,115 +20,224 @@ package org.jackhuang.hmcl.util.i18n;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class Locales {
|
||||
private Locales() {
|
||||
}
|
||||
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale(Locale.getDefault());
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale("def", Locale.getDefault()) {
|
||||
@Override
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
try {
|
||||
return inLocale.getResourceBundle().getString("lang.default");
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get localized name for default locale", e);
|
||||
return "Default";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* English
|
||||
*/
|
||||
public static final SupportedLocale EN = new SupportedLocale(Locale.ROOT);
|
||||
public static final SupportedLocale EN = new SupportedLocale("en", Locale.ENGLISH);
|
||||
|
||||
/**
|
||||
* Spanish
|
||||
*/
|
||||
public static final SupportedLocale ES = new SupportedLocale(Locale.forLanguageTag("es"));
|
||||
public static final SupportedLocale ES = new SupportedLocale("es", Locale.forLanguageTag("es"));
|
||||
|
||||
/**
|
||||
* Russian
|
||||
*/
|
||||
public static final SupportedLocale RU = new SupportedLocale(Locale.forLanguageTag("ru"));
|
||||
public static final SupportedLocale RU = new SupportedLocale("ru", Locale.forLanguageTag("ru"));
|
||||
|
||||
/**
|
||||
* Ukrainian
|
||||
*/
|
||||
public static final SupportedLocale UK = new SupportedLocale("uk", Locale.forLanguageTag("uk"));
|
||||
|
||||
/**
|
||||
* Japanese
|
||||
*/
|
||||
public static final SupportedLocale JA = new SupportedLocale(Locale.JAPANESE);
|
||||
public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE);
|
||||
|
||||
/**
|
||||
* Traditional Chinese
|
||||
* Chinese (Simplified)
|
||||
*/
|
||||
public static final SupportedLocale ZH = new SupportedLocale(Locale.TRADITIONAL_CHINESE);
|
||||
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", Locale.forLanguageTag("zh-Hans"));
|
||||
|
||||
/**
|
||||
* Simplified Chinese
|
||||
* Chinese (Traditional)
|
||||
*/
|
||||
public static final SupportedLocale ZH_CN = new SupportedLocale(Locale.SIMPLIFIED_CHINESE);
|
||||
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant"));
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ES, JA, RU, ZH_CN, ZH);
|
||||
/**
|
||||
* Wenyan (Classical Chinese)
|
||||
*/
|
||||
public static final SupportedLocale WENYAN = new SupportedLocale("lzh", Locale.forLanguageTag("lzh")) {
|
||||
|
||||
@Override
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (isChinese(inLocale.locale))
|
||||
return "文言";
|
||||
|
||||
String name = super.getDisplayName(inLocale);
|
||||
return name.equals("lzh") || name.equals("Literary Chinese")
|
||||
? "Classical Chinese"
|
||||
: name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
return WenyanUtils.formatDateTime(time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplaySelfVersion(RemoteVersion version) {
|
||||
if (version instanceof GameRemoteVersion)
|
||||
return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion()));
|
||||
else
|
||||
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
|
||||
}
|
||||
};
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
|
||||
|
||||
public static SupportedLocale getLocaleByName(String name) {
|
||||
if (name == null) return DEFAULT;
|
||||
switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "en":
|
||||
return EN;
|
||||
case "es":
|
||||
return ES;
|
||||
case "ja":
|
||||
return JA;
|
||||
case "ru":
|
||||
return RU;
|
||||
|
||||
for (SupportedLocale locale : LOCALES) {
|
||||
if (locale.getName().equalsIgnoreCase(name))
|
||||
return locale;
|
||||
}
|
||||
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
public static boolean isEnglish(Locale locale) {
|
||||
return locale.getLanguage().equals("en") || locale.getLanguage().isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isChinese(Locale locale) {
|
||||
switch (locale.getLanguage()) {
|
||||
case "zh":
|
||||
return ZH;
|
||||
case "zh_cn":
|
||||
return ZH_CN;
|
||||
case "lzh":
|
||||
case "cmn":
|
||||
return true;
|
||||
default:
|
||||
return DEFAULT;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getNameByLocale(SupportedLocale locale) {
|
||||
if (locale == EN) return "en";
|
||||
else if (locale == ES) return "es";
|
||||
else if (locale == RU) return "ru";
|
||||
else if (locale == JA) return "ja";
|
||||
else if (locale == ZH) return "zh";
|
||||
else if (locale == ZH_CN) return "zh_CN";
|
||||
else if (locale == DEFAULT) return "def";
|
||||
else throw new IllegalArgumentException("Unknown locale: " + locale);
|
||||
public static boolean isSimplifiedChinese(Locale locale) {
|
||||
if (locale.getLanguage().equals("zh") || locale.getLanguage().equals("cmn")) {
|
||||
String script = locale.getScript();
|
||||
if (script.isEmpty()) {
|
||||
String region = locale.getCountry();
|
||||
return region.isEmpty() || region.equals("CN") || region.equals("SG") || region.equals("MY");
|
||||
} else
|
||||
return script.equals("Hans");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
||||
public static final class SupportedLocale {
|
||||
public static class SupportedLocale {
|
||||
private final String name;
|
||||
private final Locale locale;
|
||||
private ResourceBundle resourceBundle;
|
||||
private DateTimeFormatter dateTimeFormatter;
|
||||
|
||||
SupportedLocale(Locale locale) {
|
||||
SupportedLocale(String name, Locale locale) {
|
||||
this.name = name;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (inLocale.locale.getLanguage().equals("lzh"))
|
||||
inLocale = ZH_HANT;
|
||||
return locale.getDisplayName(inLocale.getLocale());
|
||||
}
|
||||
|
||||
public ResourceBundle getResourceBundle() {
|
||||
ResourceBundle bundle = resourceBundle;
|
||||
|
||||
if (resourceBundle == null) {
|
||||
if (this != DEFAULT && this.locale == DEFAULT.locale) {
|
||||
bundle = DEFAULT.getResourceBundle();
|
||||
} else {
|
||||
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale);
|
||||
}
|
||||
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() {
|
||||
@Override
|
||||
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
||||
if (isSimplifiedChinese(locale)) {
|
||||
return List.of(
|
||||
Locale.SIMPLIFIED_CHINESE,
|
||||
Locale.CHINESE,
|
||||
Locale.ROOT
|
||||
);
|
||||
}
|
||||
|
||||
resourceBundle = bundle;
|
||||
if (locale.getLanguage().equals("lzh")) {
|
||||
return List.of(
|
||||
locale,
|
||||
Locale.CHINESE,
|
||||
Locale.ROOT
|
||||
);
|
||||
}
|
||||
|
||||
if (locale.getLanguage().equals("en")) {
|
||||
return List.of(Locale.ROOT);
|
||||
}
|
||||
|
||||
return super.getCandidateLocales(baseName, locale);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
DateTimeFormatter formatter = dateTimeFormatter;
|
||||
if (formatter == null)
|
||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
|
||||
.withZone(ZoneId.systemDefault());
|
||||
return formatter.format(time);
|
||||
}
|
||||
|
||||
public String getDisplaySelfVersion(RemoteVersion version) {
|
||||
return version.getSelfVersion();
|
||||
}
|
||||
|
||||
public boolean isSameLanguage(SupportedLocale other) {
|
||||
return this.getLocale().getLanguage().equals(other.getLocale().getLanguage())
|
||||
|| isChinese(this.getLocale()) && isChinese(other.getLocale());
|
||||
}
|
||||
|
||||
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
||||
@Override
|
||||
public void write(JsonWriter out, SupportedLocale value) throws IOException {
|
||||
out.value(getNameByLocale(value));
|
||||
out.value(value.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WenyanUtils {
|
||||
private static final String DOT = "點";
|
||||
|
||||
private static final String[] NUMBERS = {
|
||||
"〇", "一", "二", "三", "四", "五", "六", "七", "八", "九",
|
||||
"十", "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九",
|
||||
"廿", "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九",
|
||||
"卅", "卅一", "卅二", "卅三", "卅四", "卅五", "卅六", "卅七", "卅八", "卅九",
|
||||
"卌", "卌一", "卌二", "卌三", "卌四", "卌五", "卌六", "卌七", "卌八", "卌九",
|
||||
"五十", "五十一", "五十二", "五十三", "五十四", "五十五", "五十六", "五十七", "五十八", "五十九",
|
||||
"六十", "六十一", "六十二", "六十三", "六十四", "六十五", "六十六", "六十七", "六十八", "六十九",
|
||||
};
|
||||
|
||||
private static final char[] TIAN_GAN = {'甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'};
|
||||
private static final char[] DI_ZHI = {'子', '醜', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'};
|
||||
|
||||
private static String digitToString(char digit) {
|
||||
return digit >= '0' && digit <= '9'
|
||||
? NUMBERS[digit - '0']
|
||||
: String.valueOf(digit);
|
||||
}
|
||||
|
||||
private static String numberToString(int number) {
|
||||
return number >= 0 && number < NUMBERS.length ? NUMBERS[number] : String.valueOf(number);
|
||||
}
|
||||
|
||||
private static void appendDigitByDigit(StringBuilder builder, String number) {
|
||||
for (int i = 0; i < number.length(); i++) {
|
||||
builder.append(digitToString(number.charAt(i)));
|
||||
}
|
||||
}
|
||||
|
||||
private static int mod(int a, int b) {
|
||||
int r = a % b;
|
||||
return r >= 0 ? r : r + b;
|
||||
}
|
||||
|
||||
static void appendYear(StringBuilder builder, int year) {
|
||||
int yearOffset = year - 1984;
|
||||
|
||||
builder.append(TIAN_GAN[mod(yearOffset, TIAN_GAN.length)]);
|
||||
builder.append(DI_ZHI[mod(yearOffset, DI_ZHI.length)]);
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
LocalDateTime localDateTime;
|
||||
if (time instanceof Instant)
|
||||
localDateTime = ((Instant) time).atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
else
|
||||
localDateTime = LocalDateTime.from(time);
|
||||
|
||||
StringBuilder builder = new StringBuilder(16);
|
||||
|
||||
appendYear(builder, localDateTime.getYear());
|
||||
builder.append('年');
|
||||
builder.append(numberToString(localDateTime.getMonthValue()));
|
||||
builder.append('月');
|
||||
builder.append(numberToString(localDateTime.getDayOfMonth()));
|
||||
builder.append('日');
|
||||
|
||||
builder.append(' ');
|
||||
|
||||
builder.append(numberToString(localDateTime.getHour()));
|
||||
builder.append('时');
|
||||
builder.append(numberToString(localDateTime.getMinute()));
|
||||
builder.append('分');
|
||||
builder.append(numberToString(localDateTime.getSecond()));
|
||||
builder.append('秒');
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String translateGameVersion(GameVersionNumber gameVersion) {
|
||||
if (gameVersion instanceof GameVersionNumber.Release) {
|
||||
var release = (GameVersionNumber.Release) gameVersion;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendDigitByDigit(builder, String.valueOf(release.getMajor()));
|
||||
builder.append(DOT);
|
||||
appendDigitByDigit(builder, String.valueOf(release.getMinor()));
|
||||
|
||||
if (release.getPatch() != 0) {
|
||||
builder.append(DOT);
|
||||
appendDigitByDigit(builder, String.valueOf(release.getPatch()));
|
||||
}
|
||||
|
||||
if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) {
|
||||
// do nothing
|
||||
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) {
|
||||
builder.append("之預");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_RC) {
|
||||
builder.append("之候");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
} else {
|
||||
// Unsupported
|
||||
return gameVersion.toString();
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
} else if (gameVersion instanceof GameVersionNumber.Snapshot) {
|
||||
var snapshot = (GameVersionNumber.Snapshot) gameVersion;
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
appendDigitByDigit(builder, String.valueOf(snapshot.getYear()));
|
||||
builder.append('週');
|
||||
appendDigitByDigit(builder, String.valueOf(snapshot.getWeek()));
|
||||
|
||||
char suffix = snapshot.getSuffix();
|
||||
if (suffix >= 'a' && (suffix - 'a') < TIAN_GAN.length)
|
||||
builder.append(TIAN_GAN[suffix - 'a']);
|
||||
else
|
||||
builder.append(suffix);
|
||||
|
||||
return builder.toString();
|
||||
} else if (gameVersion instanceof GameVersionNumber.Special) {
|
||||
String version = gameVersion.toString();
|
||||
switch (version.toLowerCase(Locale.ROOT)) {
|
||||
case "2.0":
|
||||
return "二點〇";
|
||||
case "2.0_blue":
|
||||
return "二點〇藍";
|
||||
case "2.0_red":
|
||||
return "二點〇赤";
|
||||
case "2.0_purple":
|
||||
return "二點〇紫";
|
||||
case "1.rv-pre1":
|
||||
return "一點真視之預一";
|
||||
case "3d shareware v1.34":
|
||||
return "躍然享件一點三四";
|
||||
case "20w14infinite":
|
||||
case "20w14~":
|
||||
case "20w14∞":
|
||||
return "二〇週一四宇";
|
||||
case "22w13oneblockatatime":
|
||||
return "二二週一三典";
|
||||
case "23w13a_or_b":
|
||||
return "二三週一三暨";
|
||||
case "24w14potato":
|
||||
return "二四週一四芋";
|
||||
case "25w14craftmine":
|
||||
return "二五週一四礦";
|
||||
default:
|
||||
return version;
|
||||
}
|
||||
|
||||
} else {
|
||||
return gameVersion.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern GENERIC_VERSION_PATTERN =
|
||||
Pattern.compile("^[0-9]+(\\.[0-9]+)*");
|
||||
|
||||
public static String translateGenericVersion(String version) {
|
||||
Matcher matcher = GENERIC_VERSION_PATTERN.matcher(version);
|
||||
if (matcher.find()) {
|
||||
String prefix = matcher.group();
|
||||
StringBuilder builder = new StringBuilder(version.length());
|
||||
|
||||
for (int i = 0; i < prefix.length(); i++) {
|
||||
char ch = prefix.charAt(i);
|
||||
if (ch >= '0' && ch <= '9')
|
||||
builder.append(digitToString(ch));
|
||||
else if (ch == '.')
|
||||
builder.append(DOT);
|
||||
else
|
||||
builder.append(ch);
|
||||
}
|
||||
builder.append(version, prefix.length(), version.length());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private WenyanUtils() {
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,12 @@
|
||||
"titleLocalized" : "about.thanks_to.mcbbs",
|
||||
"subtitleLocalized" : "about.thanks_to.mcbbs.statement"
|
||||
},
|
||||
{
|
||||
"image" : "/assets/img/mcim.png",
|
||||
"titleLocalized" : "about.thanks_to.mcim",
|
||||
"subtitleLocalized" : "about.thanks_to.mcim.statement",
|
||||
"externalLink" : "https://github.com/mcmod-info-mirror"
|
||||
},
|
||||
{
|
||||
"image" : "/assets/img/github.png",
|
||||
"titleLocalized" : "about.thanks_to.contributors",
|
||||
|
||||
@@ -52,43 +52,43 @@
|
||||
/*
|
||||
Colors are picked from bootstrap
|
||||
|
||||
https://getbootstrap.com/docs/4.1/components/alerts/
|
||||
https://getbootstrap.com/docs/5.3/components/alerts/
|
||||
*/
|
||||
|
||||
.hint.info {
|
||||
-fx-background-color: #cce5ff;
|
||||
-fx-border-color: #b8daff;
|
||||
-fx-background-color: #cfe2ff;
|
||||
-fx-border-color: #9ec5fe;
|
||||
}
|
||||
|
||||
.hint.info Text, .hint.info .svg {
|
||||
-fx-fill: #004085;
|
||||
-fx-fill: #052c65;
|
||||
}
|
||||
|
||||
.hint.success {
|
||||
-fx-background-color: #d4edda;
|
||||
-fx-border-color: #c3e6cb;
|
||||
-fx-background-color: #d1e7dd;
|
||||
-fx-border-color: #a3cfbb;
|
||||
}
|
||||
|
||||
.hint.success Text, .hint.success .svg {
|
||||
-fx-fill: #155724;
|
||||
-fx-fill: #0a3622;
|
||||
}
|
||||
|
||||
.hint.error {
|
||||
-fx-background-color: #f8d7da;
|
||||
-fx-border-color: #f5c6cb;
|
||||
-fx-border-color: #f1aeb5;
|
||||
}
|
||||
|
||||
.hint.error Text, .hint.error .svg {
|
||||
-fx-fill: #721c24;
|
||||
-fx-fill: #58151c;
|
||||
}
|
||||
|
||||
.hint.warning {
|
||||
-fx-background-color: #fff3cd;
|
||||
-fx-border-color: #ffeeba;
|
||||
-fx-border-color: #ffe69c;
|
||||
}
|
||||
|
||||
.hint.warning Text, .hint.warning .svg {
|
||||
-fx-fill: #856404;
|
||||
-fx-fill: #664d03;
|
||||
}
|
||||
|
||||
.skin-pane .jfx-text-field {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 503 B |
@@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Contribute a lot of technical support to
|
||||
about.thanks_to.zkitefly.statement=Responsible for maintaining the documentation of HMCL.
|
||||
about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
|
||||
about.thanks_to.mcbbs.statement=For providing the mcbbs.net download mirror for Chinese Mainland users. (No longer available)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=For providing the mod info cache acceleration service for Chinese Mainland users.
|
||||
about.thanks_to.mcmod=MCMod (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=For providing the Simplified Chinese translations and wiki for various mods.
|
||||
about.thanks_to.red_lnn.statement=For providing the default background image.
|
||||
@@ -398,6 +400,16 @@ fatal.apply_update_failure=We are sorry, but Hello Minecraft! Launcher is unable
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher cannot automatically update on Windows XP/Vista.\n\
|
||||
\n\
|
||||
You can update manually by downloading a newer launcher version from %s.
|
||||
fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the future, but will still support launching games with Java 6~16.\n\
|
||||
\n\
|
||||
It is recommended to install the latest version of Java to ensure that HMCL works properly.\n\
|
||||
\n\
|
||||
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.update=HMCL will require Java 17 or later to run in the future. Please install the latest version of Java to ensure that HMCL can complete the upgrade.\n\
|
||||
\n\
|
||||
You can continue to keep the old version of Java.\
|
||||
HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.download_link=Download Java %d
|
||||
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
|
||||
@@ -435,7 +447,7 @@ folder.screenshots=Screenshots
|
||||
folder.world=World Directory
|
||||
|
||||
game=Games
|
||||
game.crash.feedback=<b>Please do not share screenshots of this window with others!</b> If you ask for help from others, please click <b>"Export Crash Logs"</b> and send the exported file to others for analysis.
|
||||
game.crash.feedback=<b>Please do not share screenshots or photos of this interface with others!</b> If you ask for help from others, please click <b>"Export Crash Logs"</b> and send the exported file to others for analysis.
|
||||
game.crash.info=Crash Info
|
||||
game.crash.reason=Crash Cause
|
||||
game.crash.reason.analyzing=Analyzing...
|
||||
@@ -754,7 +766,6 @@ java.installing=Installing Java
|
||||
java.uninstall=Uninstall Java
|
||||
java.uninstall.confirm=Are you sure you want to uninstall this Java? This action cannot be undone!
|
||||
|
||||
lang=English
|
||||
lang.default=Use System Locales
|
||||
|
||||
launch.advice=%s Do you still want to continue to launch?
|
||||
@@ -825,6 +836,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a f
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher!
|
||||
launcher.update_java=Please update your Java version.
|
||||
|
||||
libraries.download=Downloading Libraries
|
||||
|
||||
login.empty_username=You have not set your username yet!
|
||||
login.enter_password=Please enter your password.
|
||||
|
||||
@@ -859,7 +872,7 @@ modpack.choose.local.detail=You can drag the modpack file here.
|
||||
modpack.choose.remote=Download from URL
|
||||
modpack.choose.remote.detail=A direct download link to the remote modpack file is required.
|
||||
modpack.choose.repository=Download 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.repository.detail=You can choose the desired modpack on the next page.
|
||||
modpack.choose.remote.tooltip=Please enter your modpack URL
|
||||
modpack.completion=Downloading dependencies
|
||||
modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported.
|
||||
@@ -1136,6 +1149,7 @@ world.info.player.food_level=Hunger Level
|
||||
world.info.player.game_type=Game Mode
|
||||
world.info.player.game_type.adventure=Adventure
|
||||
world.info.player.game_type.creative=Creative
|
||||
world.info.player.game_type.hardcore=Hardcore
|
||||
world.info.player.game_type.spectator=Spectator
|
||||
world.info.player.game_type.survival=Survival
|
||||
world.info.player.health=Health
|
||||
@@ -1324,6 +1338,7 @@ settings.launcher=Launcher Settings
|
||||
settings.launcher.appearance=Appearance
|
||||
settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first.
|
||||
settings.launcher.debug=Debug
|
||||
settings.launcher.disable_auto_game_options=Do not switch game language
|
||||
settings.launcher.download=Download
|
||||
settings.launcher.download.threads=Threads
|
||||
settings.launcher.download.threads.auto=Automatically Determine
|
||||
@@ -1426,12 +1441,18 @@ version.empty=No Instances
|
||||
version.empty.add=Add new instance
|
||||
version.empty.launch=No available instances. Clicking "OK" will take you to the "Download" page.\n\nYou can also download the game or switch game directories via the "Download" or "All Instances" buttons on the HMCL homepage.
|
||||
version.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to download one.
|
||||
version.game.all=All
|
||||
version.game.april_fools=April Fools
|
||||
version.game.old=Historical
|
||||
version.game.release=Release
|
||||
version.game.releases=Releases
|
||||
version.game.snapshot=Snapshot
|
||||
version.game.snapshots=Snapshots
|
||||
version.game.type=Type
|
||||
version.launch=Launch Game
|
||||
version.launch.empty=Start Game
|
||||
version.launch.empty.installing=Installing Game
|
||||
version.launch.empty.tooltip=Install and launch the latest official release
|
||||
version.launch.test=Test Launch
|
||||
version.switch=Switch Instance
|
||||
version.launch_script=Export Launch Script
|
||||
@@ -1456,12 +1477,14 @@ version.manage.remove_libraries=Delete All Libraries
|
||||
version.manage.rename=Rename Instance
|
||||
version.manage.rename.message=Enter New Instance Name
|
||||
version.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character.
|
||||
version.search=Name
|
||||
version.search.prompt=Enter the version name to search
|
||||
version.settings=Settings
|
||||
version.update=Update Modpack
|
||||
|
||||
wiki.tooltip=Minecraft Wiki Page
|
||||
wiki.version.game.release=https://minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game.snapshot=https://minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game=https://minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=Java_Edition_%s
|
||||
|
||||
wizard.prev=< Prev
|
||||
wizard.failed=Failed
|
||||
|
||||
@@ -39,6 +39,8 @@ about.thanks_to.zekerzhayard.statement=Aportar mucho apoyo técnico a HMCL.
|
||||
about.thanks_to.zkitefly.statement=Responsable del mantenimiento de la documentación de HMCL.
|
||||
about.thanks_to.mcbbs=MCBBS (Foro Chino Minecraft)
|
||||
about.thanks_to.mcbbs.statement=Por proporcionar el espejo de descarga de mcbbs.net para los usuarios de China continental. (Ya no disponible)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=Por proporcionar el servicio de aceleración de caché de información de mods para los usuarios de China continental.
|
||||
about.thanks_to.mcmod=MCMod (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=Por proporcionar las traducciones al chino simplificado y la wiki de varios mods.
|
||||
about.thanks_to.red_lnn.statement=Por proporcionar la imagen de fondo por defecto.
|
||||
@@ -400,6 +402,17 @@ fatal.apply_update_failure=Lo sentimos, pero Hello Minecraft! Launcher no puede
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher no puede actualizarse automáticamente en Windows XP/Vista.\n\
|
||||
\n\
|
||||
Puedes actualizar manualmente descargando una versión más reciente del launcher desde %s.
|
||||
fatal.deprecated_java_version=HMCL requerirá Java 17 o posterior para funcionar en el futuro, pero seguirá siendo compatible con el lanzamiento de juegos con Java 6~16.\n\
|
||||
\n\
|
||||
Se recomienda instalar la última versión de Java para garantizar el correcto funcionamiento de HMCL.\n\
|
||||
\n\
|
||||
Puedes conservar la versión antigua de Java. HMCL puede reconocer y gestionar varias versiones de Java y elegirá automáticamente la adecuada para usted en función de la versión del juego.
|
||||
fatal.deprecated_java_version.update=HMCL requerirá Java 17 o posterior para funcionar en el futuro. Por favor, instalar la última versión de Java para asegurarse de que el lanzador puede completar la actualización.\n\
|
||||
\n\
|
||||
Puedes conservar la versión antigua de Java.\
|
||||
HMCL puede reconocer y gestionar varias versiones de Java \
|
||||
y elegirá automáticamente la adecuada para usted en función de la versión del juego.
|
||||
fatal.deprecated_java_version.download_link=Descargar Java %d
|
||||
fatal.samba=Si ha abierto Hello Minecraft! Launcher desde una unidad de red Samba, es posible que algunas funciones no funcionen. Intenta actualizar tu Java o mover el launcher a otro directorio.
|
||||
fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal «=», por lo que algunas características podrían no funcionar correctamente.\n\
|
||||
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline.
|
||||
@@ -438,7 +451,7 @@ folder.screenshots=Capturas de pantalla
|
||||
folder.world=Directorio del mundo
|
||||
|
||||
game=Juegos
|
||||
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla de esta interfaz con otras personas!</b> Si pide ayuda a otras personas, haga clic en <b>Exportar registros de colgado</b> y envíe el archivo exportado a otras personas para que lo analicen.
|
||||
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla o fotos de esta interfaz con otras personas!</b> Si pide ayuda a otras personas, haga clic en <b>Exportar registros de colgado</b> y envíe el archivo exportado a otras personas para que lo analicen.
|
||||
game.crash.info=Información de colgado
|
||||
game.crash.reason=Motivo del colgado
|
||||
game.crash.reason.analyzing=Analizando...
|
||||
@@ -719,7 +732,7 @@ install.installer.external_version=%s (Instalado por un proceso externo, que no
|
||||
install.installing=Instalando
|
||||
install.modpack=Añadir modpack
|
||||
install.modpack.installation=Instalación de modpack
|
||||
install.name.invalid=El nombre contiene caracteres no ASCII (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
|
||||
install.name.invalid=El nombre contiene caracteres especiales (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
|
||||
install.new_game=Añadir instancia
|
||||
install.new_game.already_exists=Esta instancia ya existe. Por favor, utilice otro nombre.
|
||||
install.new_game.current_game_version=Versión actual de la instancia
|
||||
@@ -759,7 +772,6 @@ java.installing=Instalando Java
|
||||
java.uninstall=Desinstalar Java
|
||||
java.uninstall.confirm=¿Está seguro de que desea desinstalar este Java? ¡Esta acción no se puede deshacer!
|
||||
|
||||
lang=Español
|
||||
lang.default=Usar idioma del sistema
|
||||
|
||||
launch.advice=%s ¿Todavía quieres continuar?
|
||||
@@ -830,6 +842,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher ha encontrado un er
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher ha encontrado un error fatal. Su launcher está desactualizado. Por favor, ¡actualícelo!
|
||||
launcher.update_java=Por favor, actualice su versión de Java.
|
||||
|
||||
libraries.download=Descargando bibliotecas
|
||||
|
||||
login.empty_username=¡Todavía no has puesto tu nombre de usuario!
|
||||
login.enter_password=Por favor, introduzca su contraseña.
|
||||
|
||||
@@ -865,7 +879,7 @@ modpack.choose.remote=Descargar desde Internet
|
||||
modpack.choose.remote.detail=Se requiere un enlace de descarga directa al archivo modpack remoto
|
||||
modpack.choose.remote.tooltip=Por favor, introduzca la URL de su modpack
|
||||
modpack.choose.repository=Descargar Modpack de CurseForge o Modrinth
|
||||
modpack.choose.repository.detail=Recuerda volver a esta página y soltar aquí el archivo del modpack una vez descargado.
|
||||
modpack.choose.repository.detail=Puedes elegir el modpack que desees en la página siguiente.
|
||||
modpack.completion=Descargando dependencias
|
||||
modpack.desc=Describa su modpack, incluyendo una introducción y probablemente algún registro de cambios. Actualmente se admiten Markdown e imágenes desde URL.
|
||||
modpack.description=Descripción
|
||||
@@ -1426,12 +1440,18 @@ version.empty=No hay instancias
|
||||
version.empty.add=Añadir una instancia
|
||||
version.empty.launch=No hay instancias disponibles. Si haces clic en «Aceptar», accederás a la página «Descargar». También puedes descargar el juego o cambiar de directorio de juego con los botones «Descargar» o «Todas las instancias» de la página principal de HMCL.
|
||||
version.empty.hint=No hay instancias de Minecraft aquí.\nPuedes intentar cambiar a otro directorio del juego o hacer clic aquí para descargar una.
|
||||
version.game.all=Todos
|
||||
version.game.april_fools=Día de los Inocentes
|
||||
version.game.old=Histórico
|
||||
version.game.release=Lanzamiento
|
||||
version.game.releases=Lanzamientos
|
||||
version.game.snapshot=Snapshot
|
||||
version.game.snapshots=Snapshots
|
||||
version.game.type=Tipo
|
||||
version.launch=Iniciar Minecraft
|
||||
version.launch.empty=Iniciar Minecraft
|
||||
version.launch.empty.installing=Instalando el juego
|
||||
version.launch.empty.tooltip=Instala y ejecuta la última versión oficial del juego.
|
||||
version.launch.test=Probar instalación
|
||||
version.switch=Cambiar Instancia
|
||||
version.launch_script=Exportar script de ejecución
|
||||
@@ -1456,12 +1476,14 @@ version.manage.remove_libraries=Borrar todas las bibliotecas
|
||||
version.manage.rename=Renombrar instancia
|
||||
version.manage.rename.message=Introduzca el nombre de la nueva instancia
|
||||
version.manage.rename.fail=No se ha podido renombrar la instancia, algunos archivos pueden estar en uso o el nombre contiene un carácter no válido.
|
||||
version.search=Nombre
|
||||
version.search.prompt=Introduzca el nombre de la versión para buscar.
|
||||
version.settings=Configuración
|
||||
version.update=Actualizar Modpack
|
||||
|
||||
wiki.tooltip=Página de Minecraft Wiki
|
||||
wiki.version.game.release=https://es.minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game.snapshot=https://es.minecraft.wiki/w/Java_Edition_%s
|
||||
wiki.version.game=https://es.minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=Java_Edition_%s
|
||||
|
||||
wizard.prev=< Previo
|
||||
wizard.failed=Falló
|
||||
|
||||
@@ -347,7 +347,7 @@ folder.saves=マップ
|
||||
folder.screenshots=スクリーンショット
|
||||
|
||||
game=ゲーム
|
||||
game.crash.feedback=<b>このインターフェースのスクリーンショットを他の人と共有しないでください。 </b> 他の人に助けを求める場合は、左下隅にあるゲーム クラッシュ情報をクリックしてエクスポートし、エクスポートされたファイルを分析のために他の人に送信してください。
|
||||
game.crash.feedback=<b>このインターフェースのスクリーンショットや写真を他の人と共有しないでください。</b> 他の人に助けを求める場合は、左下隅にあるゲーム クラッシュ情報をクリックしてエクスポートし、エクスポートされたファイルを分析のために他の人に送信してください。
|
||||
game.crash.info=ゲームステータス
|
||||
game.crash.reason=クラッシュアナライザー
|
||||
game.crash.reason.analyzing=分析中..
|
||||
@@ -492,7 +492,6 @@ java.install.warning.invalid_character=名前に不正な文字が含まれて
|
||||
java.uninstall=このJavaをアンインストールする
|
||||
java.uninstall.confirm=本当にこのJavaをアンインストールしますか?この操作は元に戻せません!
|
||||
|
||||
lang=日本語
|
||||
lang.default=システム言語を使用する
|
||||
|
||||
launch.advice.java.auto=現在選択されているJavaVMは、ゲームの要件を満たしていません。適切なJavaVMを選択することを許可しますか?または、ゲーム設定で適切なJavaVMを選択することもできます。
|
||||
@@ -963,6 +962,9 @@ version.manage.rename.fail=バージョンの名前を変更できませんで
|
||||
version.settings=設定
|
||||
version.update=modpackを更新する
|
||||
|
||||
wiki.version.game=https://ja.minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=Java_Edition_%s
|
||||
|
||||
wizard.prev=< 前へ
|
||||
wizard.failed=失敗
|
||||
wizard.finish=終了
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=Оказал большую технич
|
||||
about.thanks_to.zkitefly.statement=Отвечает за поддержание документации HMCL.
|
||||
about.thanks_to.mcbbs=MCBBS (Китайский форум Minecraft)
|
||||
about.thanks_to.mcbbs.statement=За предоставление зеркала загрузки mcbbs.net для пользователей материкового Китая. (Больше недоступно)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=За предоставление услуги ускорения кэширования информации о модах для пользователей из материкового Китая.
|
||||
about.thanks_to.mcmod=MCMod (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=За предоставление упрощенного китайского перевода и вики для различных модов.
|
||||
about.thanks_to.red_lnn.statement=За предоставление фонового изображения.
|
||||
@@ -398,6 +400,17 @@ fatal.apply_update_failure=Мы сожалеем, лаунчер не смог
|
||||
Вы можете обновить программу вручную, скачав более новую версию с %s.
|
||||
fatal.apply_update_need_win7=Лаунчер не может автоматически обновляться на Windows XP/Vista.\n\
|
||||
Вы можете обновить программу вручную, скачав более новую версию с %s.
|
||||
fatal.deprecated_java_version=В будущем для работы HMCL потребуется Java 17 или более поздняя версия, однако запуск игр будет поддерживаться и на Java 6~16.\n\
|
||||
\n\
|
||||
Рекомендуется установить последнюю версию Java, чтобы обеспечить правильную работу HMCL.\n\
|
||||
\n\
|
||||
Вы можете продолжать использовать старую версию Java. HMCL может распознавать и управлять несколькими версиями Java и автоматически выбирать подходящую Java в зависимости от версии игры.
|
||||
fatal.deprecated_java_version.update=В будущем для работы HMCL потребуется Java 17 или более поздняя версия. Пожалуйста, установить последнюю версию Java, чтобы убедиться, что приложение сможет завершить обновление.\n\
|
||||
\n\
|
||||
Вы можете продолжать использовать старую версию Java.\
|
||||
HMCL может распознавать и управлять несколькими версиями Java \
|
||||
и автоматически выбирать подходящую Java в зависимости от версии игры.
|
||||
fatal.deprecated_java_version.download_link=Скачать Java %d
|
||||
fatal.samba=Если вы пытаетесь открыть лаунчер в общей папке Samba, он может не работать, попробуйте обновить Java или запустить лаунчер из локальной папки.
|
||||
fatal.illegal_char=Недопустимый символ «=» в пути к папке пользователя. Лаунчер может работать, но некоторые функции не будут работать.\n\
|
||||
Вы не сможете использовать аккаунт authlib-injector или изменить скин для аккаунта в режиме офлайн.
|
||||
@@ -436,7 +449,7 @@ folder.screenshots=Снимки экрана
|
||||
folder.world=Папка мира
|
||||
|
||||
game=Игры
|
||||
game.crash.feedback=<b>Пожалуйста, не делитесь скриншотами этого интерфейса с другими!</b> Если вы просите помощи у других, нажмите <b>«Экспорт журналов с ошибками»</b> и отправьте экспортированный файл другим для анализа.
|
||||
game.crash.feedback=<b>Пожалуйста, не делитесь с другими скриншотами или фотографиями этого интерфейса.</b> Если вы просите помощи у других, нажмите <b>«Экспорт журналов с ошибками»</b> и отправьте экспортированный файл другим для анализа.
|
||||
game.crash.info=Сведения об отказе
|
||||
game.crash.reason=Анализатор сбоев
|
||||
game.crash.reason.analyzing=Анализирование...
|
||||
@@ -715,7 +728,7 @@ install.installer.external_version=%s (Устанавливается внешн
|
||||
install.installing=Установка
|
||||
install.modpack=Установить модпак
|
||||
install.modpack.installation=Установка модпак
|
||||
install.name.invalid=Имя содержит символы, отличные от ASCII (например, эмодзи или символы CJK).\nРекомендуется изменить имя, включив в него только английские буквы, цифры и подчеркивание, чтобы избежать возможных проблем при запуске игры.\nВы хотите продолжить процесс?
|
||||
install.name.invalid=Имя содержит специальные символы (например, эмодзи или символы CJK).\nРекомендуется изменить имя, включив в него только английские буквы, цифры и подчеркивание, чтобы избежать возможных проблем при запуске игры.\nВы хотите продолжить процесс?
|
||||
install.new_game=Установить сборку
|
||||
install.new_game.already_exists=Это имя сборки уже существует. Пожалуйста, используйте другое имя.
|
||||
install.new_game.current_game_version=Текущая версия сборки
|
||||
@@ -755,7 +768,6 @@ java.installing=Установка Java
|
||||
java.uninstall=Удалить Java
|
||||
java.uninstall.confirm=Вы уверены, что хотите удалить эту Java? Это действие нельзя отменить!
|
||||
|
||||
lang=Русский
|
||||
lang.default=Использовать язык системы
|
||||
|
||||
launch.advice=%s Вы все еще хотите продолжить запуск?
|
||||
@@ -829,6 +841,8 @@ launcher.crash.java_internal_error=Лаунчер столкнулся с фат
|
||||
launcher.crash.hmcl_out_dated=Лаунчер столкнулся с фатальной ошибкой! Ваш лаунчер устарел. Обновите его!
|
||||
launcher.update_java=Пожалуйста, обновите версию Java.
|
||||
|
||||
libraries.download=Скачивание библиотек
|
||||
|
||||
login.empty_username=Имя пользователя не установлено!
|
||||
login.enter_password=Введите пароль.
|
||||
|
||||
@@ -864,7 +878,7 @@ modpack.choose.remote=Скачать
|
||||
modpack.choose.remote.detail=Необходима прямая ссылка на скачивание модпака
|
||||
modpack.choose.remote.tooltip=Введите URL-адрес модпака
|
||||
modpack.choose.repository=Скачать модпак с CurseForge или Modrinth
|
||||
modpack.choose.repository.detail=Не забудьте вернуться на эту страницу и перетащить файл модпака сюда после того, как модпак будет скачан.
|
||||
modpack.choose.repository.detail=Вы сможете выбрать нужный вам модпак на следующей странице.
|
||||
modpack.completion=Скачивание зависимостей
|
||||
modpack.desc=Опишите модпак, включая введение и журнал изменений. Поддерживаются Markdown и изображения по URL-адресу.
|
||||
modpack.description=Описание
|
||||
@@ -1426,12 +1440,18 @@ version.empty=Нет сборок
|
||||
version.empty.add=Установить новую сборок
|
||||
version.empty.launch=Нет доступных сборок. Нажав «ОК», вы перейдете на страницу «Скачать»\n\nВы также можете скачать игру или переключить директорию игры с помощью кнопок «Скачать» или «Все сборок» на главной странице HMCL.
|
||||
version.empty.hint=Нет сборок.\nВы можете попробовать перейти в другую папку с игрой или нажать здесь, чтобы скачать игру.
|
||||
version.game.all=Все
|
||||
version.game.april_fools=День смеха
|
||||
version.game.old=Исторические
|
||||
version.game.release=Релиз
|
||||
version.game.releases=Релизы
|
||||
version.game.snapshot=Снапшот
|
||||
version.game.snapshots=Снапшоты
|
||||
version.game.type=Тип
|
||||
version.launch=Играть
|
||||
version.launch.empty=Играть
|
||||
version.launch.empty.installing=Установка игры
|
||||
version.launch.empty.tooltip=Установить и запустить последнюю официальную версию игры.
|
||||
version.launch.test=Тестовая игра
|
||||
version.switch=Сменить сборку
|
||||
version.launch_script=Создать сценарий запуска
|
||||
@@ -1456,12 +1476,14 @@ version.manage.remove_libraries=Удалить файлы библиотек
|
||||
version.manage.rename=Переименовать
|
||||
version.manage.rename.message=Введите новое название для этой сборки
|
||||
version.manage.rename.fail=Не удалось переименовать сборку. Возможно, некоторые файлы уже используются или в их имени содержится недопустимый символ.
|
||||
version.search=Название
|
||||
version.search.prompt=Введите название версии для поиска
|
||||
version.settings=Настройки
|
||||
version.update=Обновить модпак
|
||||
|
||||
wiki.tooltip=Страница Minecraft Wiki
|
||||
wiki.version.game.release=https://ru.minecraft.wiki/w/%s_(Java_Edition)
|
||||
wiki.version.game.snapshot=https://ru.minecraft.wiki/w/%s_(Java_Edition)
|
||||
wiki.version.game=https://ru.minecraft.wiki/w/Special:Search?search=%s
|
||||
wiki.version.game.search=%s_(Java_Edition)
|
||||
|
||||
wizard.prev=< Пред.
|
||||
wizard.failed=Не удалось
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=為 HMCL 貢獻許多技術支援
|
||||
about.thanks_to.zkitefly.statement=負責維護 HMCL 的文件
|
||||
about.thanks_to.mcbbs=MCBBS (我的世界中文論壇)
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 (現已停止服務)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=為中國大陸使用者提供模組資訊快取加速服務
|
||||
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=提供模組簡體中文名映射表與模組百科
|
||||
about.thanks_to.red_lnn.statement=提供預設背景圖
|
||||
@@ -380,6 +382,9 @@ fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Laun
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。建議安裝最新版本的 Java 以確保 HMCL 正常執行。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能執行,請安裝最新版本的 Java 以確保 HMCL 能夠完成升級。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
fatal.deprecated_java_version.download_link=下載 Java %d
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.illegal_char=由於您的使用者目錄路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未你您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「軟繪製器」,以獲得更好的相容性。
|
||||
@@ -410,7 +415,7 @@ folder.screenshots=截圖目錄
|
||||
folder.world=世界目錄
|
||||
|
||||
game=遊戲
|
||||
game.crash.feedback=<b>請不要將本介面截圖給他人!</b>如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。
|
||||
game.crash.feedback=<b>請不要將本介面截圖或拍照給他人!</b>如果你要求助他人,請你點擊左下角「匯出遊戲崩潰資訊」後將匯出的檔案發送給他人以供分析。\n你可以點擊下方的「幫助」前往社群尋求幫助。
|
||||
game.crash.info=遊戲訊息
|
||||
game.crash.reason=崩潰原因
|
||||
game.crash.reason.analyzing=分析中……
|
||||
@@ -570,7 +575,6 @@ java.installing=安裝 Java
|
||||
java.uninstall=移除此 Java
|
||||
java.uninstall.confirm=你確定要移除此 Java 嗎?此操作無法復原!
|
||||
|
||||
lang=繁體中文
|
||||
lang.default=使用系統語言
|
||||
|
||||
launch.advice=%s 是否繼續啟動?
|
||||
@@ -641,6 +645,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher 由於目前 Java
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
|
||||
launcher.update_java=請更新你的 Java
|
||||
|
||||
libraries.download=下載依賴庫
|
||||
|
||||
login.empty_username=你還未設定使用者名稱!
|
||||
login.enter_password=請輸入你的密碼
|
||||
|
||||
@@ -675,7 +681,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以
|
||||
modpack.choose.remote=從網路下載模組包
|
||||
modpack.choose.remote.detail=需要提供模組包的下載連結
|
||||
modpack.choose.repository=從 CurseForge/Modrinth 下載模組包
|
||||
modpack.choose.repository.detail=下載後記得回到這個介面,把模組包拖進來哦
|
||||
modpack.choose.repository.detail=可直接在跳轉到的頁面安裝模組包
|
||||
modpack.choose.remote.tooltip=要下載的模組包的連結
|
||||
modpack.completion=下載模組包相關檔案
|
||||
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新紀錄。支援 Markdown (圖片請上傳至網路)。
|
||||
@@ -941,6 +947,7 @@ world.info.player.food_level=饑餓值
|
||||
world.info.player.game_type=遊戲模式
|
||||
world.info.player.game_type.adventure=冒險
|
||||
world.info.player.game_type.creative=創造
|
||||
world.info.player.game_type.hardcore=極限
|
||||
world.info.player.game_type.spectator=旁觀者
|
||||
world.info.player.game_type.survival=生存
|
||||
world.info.player.health=生命值
|
||||
@@ -1121,6 +1128,7 @@ settings.launcher=啟動器設定
|
||||
settings.launcher.appearance=外觀
|
||||
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元件庫檔案放於此集中管理。如果遊戲目錄內有現成的將不會使用公共庫檔案。
|
||||
settings.launcher.debug=除錯
|
||||
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
|
||||
settings.launcher.download=下載
|
||||
settings.launcher.download.threads=執行緒數
|
||||
settings.launcher.download.threads.auto=自動選取執行緒數
|
||||
@@ -1219,12 +1227,18 @@ version.empty=沒有遊戲實例
|
||||
version.empty.add=進入下載頁安裝遊戲
|
||||
version.empty.launch=沒有可啟動的遊戲。點擊「確定」將進入「下載」頁面。\n你也可以點擊 HMCL 主介面左側的「下載」按鈕安裝遊戲,或在「實例清單」切換遊戲目錄。
|
||||
version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處進入遊戲下載頁面。
|
||||
version.game.all=全部
|
||||
version.game.april_fools=愚人節
|
||||
version.game.old=遠古版
|
||||
version.game.release=正式版
|
||||
version.game.releases=正式版
|
||||
version.game.snapshot=快照
|
||||
version.game.snapshots=快照
|
||||
version.game.type=版本類型
|
||||
version.launch=啟動遊戲
|
||||
version.launch.empty=開始遊戲
|
||||
version.launch.empty.installing=安裝遊戲
|
||||
version.launch.empty.tooltip=安裝並啟動最新正式版遊戲
|
||||
version.launch.test=測試遊戲
|
||||
version.switch=切換實例
|
||||
version.launch_script=生成啟動指令碼
|
||||
@@ -1249,12 +1263,14 @@ version.manage.remove_libraries=刪除所有支援庫檔案
|
||||
version.manage.rename=重新命名該實例
|
||||
version.manage.rename.message=請輸入新名稱
|
||||
version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。
|
||||
version.search=名稱
|
||||
version.search.prompt=輸入版本名稱進行搜尋
|
||||
version.settings=遊戲設定
|
||||
version.update=更新模組包
|
||||
|
||||
wiki.tooltip=Minecraft Wiki 頁面
|
||||
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-tw
|
||||
wiki.version.game.snapshot=https://zh.minecraft.wiki/w/%s?variant=zh-tw
|
||||
wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-tw
|
||||
wiki.version.game.search=Java版%s
|
||||
|
||||
wizard.prev=< 上一步
|
||||
wizard.failed=失敗
|
||||
|
||||
@@ -38,6 +38,8 @@ about.thanks_to.zekerzhayard.statement=为 HMCL 贡献许多技术支持
|
||||
about.thanks_to.zkitefly.statement=负责维护 HMCL 的文档
|
||||
about.thanks_to.mcbbs=MCBBS (我的世界中文论坛)
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下载源 (现已停止服务)
|
||||
about.thanks_to.mcim=mcmod-info-mirror
|
||||
about.thanks_to.mcim.statement=为中国大陆用户提供模组信息缓存加速服务
|
||||
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
|
||||
about.thanks_to.mcmod.statement=提供模组简体中文名映射表与模组百科
|
||||
about.thanks_to.red_lnn.statement=提供默认背景图
|
||||
@@ -389,6 +391,9 @@ fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Laun
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
|
||||
fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。建议安装最新版本的 Java 以确保 HMCL 能正常工作。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能运行,请安装最新版本的 Java 以确保 HMCL 能够完成升级。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。
|
||||
fatal.deprecated_java_version.download_link=下载 Java %d
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/版本特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“软渲染器”,以获得更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
@@ -419,7 +424,7 @@ folder.screenshots=截图文件夹
|
||||
folder.world=世界文件夹
|
||||
|
||||
game=游戏
|
||||
game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要向他人求助,请你点击左下角<b>“导出游戏崩溃信息”</b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b>“帮助”</b>前往交流群寻求帮助。
|
||||
game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如果你要向他人求助,请你点击左下角<b>“导出游戏崩溃信息”</b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b>“帮助”</b>前往交流群寻求帮助。
|
||||
game.crash.info=游戏信息
|
||||
game.crash.reason=崩溃原因
|
||||
game.crash.reason.analyzing=分析中……
|
||||
@@ -580,7 +585,6 @@ java.installing=安装 Java
|
||||
java.uninstall=卸载此 Java
|
||||
java.uninstall.confirm=你确定要卸载此 Java 吗?此操作无法撤销!
|
||||
|
||||
lang=简体中文
|
||||
lang.default=跟随系统语言
|
||||
|
||||
launch.advice=%s 是否继续启动?
|
||||
@@ -651,6 +655,8 @@ launcher.crash.java_internal_error=Hello Minecraft! Launcher 由于当前 Java
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
|
||||
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
|
||||
libraries.download=下载依赖库
|
||||
|
||||
login.empty_username=你还未设置用户名!
|
||||
login.enter_password=请输入你的密码
|
||||
|
||||
@@ -685,7 +691,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
|
||||
modpack.choose.remote=从互联网下载整合包
|
||||
modpack.choose.remote.detail=需要提供整合包的下载链接
|
||||
modpack.choose.repository=从 CurseForge/Modrinth 下载整合包
|
||||
modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦
|
||||
modpack.choose.repository.detail=可直接在跳转到的页面安装整合包
|
||||
modpack.choose.remote.tooltip=要下载的整合包的链接
|
||||
modpack.completion=下载整合包相关文件
|
||||
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录。支持 Markdown (图片请用网络链接)。
|
||||
@@ -952,6 +958,7 @@ world.info.player.food_level=饥饿值
|
||||
world.info.player.game_type=游戏模式
|
||||
world.info.player.game_type.adventure=冒险
|
||||
world.info.player.game_type.creative=创造
|
||||
world.info.player.game_type.hardcore=极限
|
||||
world.info.player.game_type.spectator=旁观者
|
||||
world.info.player.game_type.survival=生存
|
||||
world.info.player.health=生命值
|
||||
@@ -1131,6 +1138,7 @@ settings.launcher=启动器设置
|
||||
settings.launcher.appearance=外观
|
||||
settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件存放于此集中管理。如果游戏文件夹内有现成的将不会使用公共库文件。
|
||||
settings.launcher.debug=调试
|
||||
settings.launcher.disable_auto_game_options=不自动切换游戏语言
|
||||
settings.launcher.download=下载
|
||||
settings.launcher.download.threads=线程数
|
||||
settings.launcher.download.threads.auto=自动选择线程数
|
||||
@@ -1229,12 +1237,18 @@ version.empty=没有游戏版本
|
||||
version.empty.add=进入下载页安装游戏
|
||||
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。
|
||||
version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。
|
||||
version.game.all=全部
|
||||
version.game.april_fools=愚人节
|
||||
version.game.old=远古版
|
||||
version.game.release=正式版
|
||||
version.game.releases=正式版
|
||||
version.game.snapshot=快照
|
||||
version.game.snapshots=快照
|
||||
version.game.type=版本类型
|
||||
version.launch=启动游戏
|
||||
version.launch.empty=开始游戏
|
||||
version.launch.empty.installing=安装游戏
|
||||
version.launch.empty.tooltip=安装并启动最新正式版游戏
|
||||
version.launch.test=测试游戏
|
||||
version.switch=切换版本
|
||||
version.launch_script=生成启动脚本
|
||||
@@ -1259,12 +1273,14 @@ version.manage.remove_libraries=删除所有库文件
|
||||
version.manage.rename=重命名该版本
|
||||
version.manage.rename.message=请输入要修改的名称
|
||||
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
|
||||
version.search=名称
|
||||
version.search.prompt=输入版本名称进行搜索
|
||||
version.settings=游戏设置
|
||||
version.update=更新整合包
|
||||
|
||||
wiki.tooltip=Minecraft Wiki 页面
|
||||
wiki.version.game.release=https://zh.minecraft.wiki/w/Java%%E7%%89%%88%s?variant=zh-cn
|
||||
wiki.version.game.snapshot=https://zh.minecraft.wiki/w/%s?variant=zh-cn
|
||||
wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-cn
|
||||
wiki.version.game.search=Java版%s
|
||||
|
||||
wizard.prev=< 上一步
|
||||
wizard.failed=失败
|
||||
|
||||
@@ -4007,6 +4007,17 @@
|
||||
"size": 27971214
|
||||
}
|
||||
}
|
||||
},
|
||||
"software-renderer-loader": {
|
||||
"name": "org.glavo:llvmpipe-loader:1.0",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
|
||||
"sha1": "ff255415e5c4b2a18970da0a8e552b557ca013ae",
|
||||
"size": 12964773
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"windows-x86": {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.util.i18n;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WenyanUtilsTest {
|
||||
|
||||
private static void assertYearToString(String value, int year) {
|
||||
StringBuilder builder = new StringBuilder(2);
|
||||
WenyanUtils.appendYear(builder, year);
|
||||
assertEquals(value, builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testYearToString() {
|
||||
assertYearToString("甲子", 1984);
|
||||
assertYearToString("乙巳", 2025);
|
||||
assertYearToString("甲子", -2996);
|
||||
assertYearToString("庚子", 1000);
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.auth.authlibinjector;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class AuthlibInjectorExtractor implements AuthlibInjectorArtifactProvider {
|
||||
private final URL source;
|
||||
private final Path artifactLocation;
|
||||
|
||||
public AuthlibInjectorExtractor(URL source, Path artifactLocation) {
|
||||
if (source == null)
|
||||
throw new IllegalArgumentException("Missing authlib injector");
|
||||
this.source = source;
|
||||
this.artifactLocation = artifactLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
|
||||
Optional<AuthlibInjectorArtifactInfo> cached = getArtifactInfoImmediately();
|
||||
if (cached.isPresent())
|
||||
return cached.get();
|
||||
|
||||
synchronized (this) {
|
||||
cached = getArtifactInfoImmediately();
|
||||
if (cached.isPresent())
|
||||
return cached.get();
|
||||
|
||||
LOG.info("No local authlib-injector found, extracting");
|
||||
Files.createDirectories(artifactLocation.getParent());
|
||||
try (InputStream inputStream = source.openStream()) {
|
||||
FileUtils.saveSafely(artifactLocation, inputStream::transferTo);
|
||||
}
|
||||
return getArtifactInfoImmediately().orElseThrow(() ->
|
||||
new IOException("Failed to extract authlib-injector artifact"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
if (!Files.isRegularFile(artifactLocation))
|
||||
return Optional.empty();
|
||||
|
||||
try {
|
||||
return Optional.of(AuthlibInjectorArtifactInfo.from(artifactLocation));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Bad authlib-injector artifact", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,12 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
pair("https://authlib-injector.yushi.moe", apiRoot + "/mirrors/authlib-injector"),
|
||||
pair("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto")
|
||||
// // https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
// pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"),
|
||||
// pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"),
|
||||
// pair("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge"),
|
||||
// pair("https://edge.forgecdn.net", "https://mod.mcimirror.top"),
|
||||
// pair("https://mediafilez.forgecdn.net", "https://mod.mcimirror.top")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
} else {
|
||||
index = new Index();
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to read index file", e);
|
||||
index = new Index();
|
||||
} finally {
|
||||
|
||||
@@ -85,7 +85,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
else
|
||||
return null;
|
||||
}).thenComposeAsync(checkPatchCompletionAsync(version, integrityCheck)),
|
||||
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck),
|
||||
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck)
|
||||
.setSignificance(Task.TaskSignificance.MODERATE),
|
||||
new GameLibrariesTask(this, version, integrityCheck)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
Task<Version> libraryTask = Task.supplyAsync(() -> new Version(name));
|
||||
libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, "game", gameVersion));
|
||||
stages.add("hmcl.install.game:" + gameVersion);
|
||||
stages.add("hmcl.install.libraries");
|
||||
stages.add("hmcl.install.assets");
|
||||
|
||||
for (Map.Entry<String, String> entry : toolVersions.entrySet()) {
|
||||
|
||||
@@ -75,6 +75,7 @@ public final class GameLibrariesTask extends Task<Void> {
|
||||
this.integrityCheck = integrityCheck;
|
||||
this.libraries = libraries;
|
||||
|
||||
setStage("hmcl.install.libraries");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@@ -115,6 +116,7 @@ public final class GameLibrariesTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void execute() throws IOException {
|
||||
int progress = 0;
|
||||
GameRepository gameRepository = dependencyManager.getGameRepository();
|
||||
for (Library library : libraries) {
|
||||
if (!library.appliesToCurrentEnvironment()) {
|
||||
@@ -135,10 +137,17 @@ public final class GameLibrariesTask extends Task<Void> {
|
||||
}
|
||||
}
|
||||
if (shouldDownloadLibrary(gameRepository, version, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))) {
|
||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library).withCounter("hmcl.install.libraries"));
|
||||
} else {
|
||||
dependencyManager.getCacheRepository().tryCacheLibrary(library, file.toPath());
|
||||
}
|
||||
|
||||
updateProgress(++progress, libraries.size());
|
||||
}
|
||||
|
||||
if (!dependencies.isEmpty()) {
|
||||
getProperties().put("total", dependencies.size());
|
||||
notifyPropertiesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,15 +54,16 @@ public final class GameRemoteVersion extends RemoteVersion {
|
||||
|
||||
@Override
|
||||
public int compareTo(RemoteVersion o) {
|
||||
if (!(o instanceof GameRemoteVersion))
|
||||
if (!(o instanceof GameRemoteVersion)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dateCompare = o.getReleaseDate().compareTo(getReleaseDate());
|
||||
if (dateCompare != 0) {
|
||||
return dateCompare;
|
||||
}
|
||||
|
||||
return GameVersionNumber.compare(getSelfVersion(), o.getSelfVersion());
|
||||
return GameVersionNumber.compare(o.getSelfVersion(), getSelfVersion());
|
||||
}
|
||||
|
||||
private static Type getReleaseType(ReleaseType type) {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final String OLD_URL = "https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge";
|
||||
|
||||
+5
-1
@@ -10,7 +10,7 @@ import java.util.List;
|
||||
|
||||
public class NeoForgeRemoteVersion extends RemoteVersion {
|
||||
public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List<String> urls) {
|
||||
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, urls);
|
||||
super(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), gameVersion, selfVersion, null, getType(selfVersion), urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -18,6 +18,10 @@ public class NeoForgeRemoteVersion extends RemoteVersion {
|
||||
return new NeoForgeInstallTask(dependencyManager, baseVersion, this);
|
||||
}
|
||||
|
||||
private static Type getType(String version) {
|
||||
return version.contains("beta") ? Type.SNAPSHOT : Type.RELEASE;
|
||||
}
|
||||
|
||||
public static String normalize(String version) {
|
||||
if (version.startsWith("1.20.1-")) {
|
||||
if (version.startsWith("forge-", "1.20.1-".length())) {
|
||||
|
||||
@@ -168,7 +168,7 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
|
||||
res.addDefault("-Duser.home=", options.getGameDir().getParent());
|
||||
res.addDefault("-Duser.home=", options.getGameDir().getAbsoluteFile().getParent());
|
||||
|
||||
Proxy.Type proxyType = options.getProxyType();
|
||||
if (proxyType == null) {
|
||||
@@ -442,7 +442,7 @@ public class DefaultLauncher extends Launcher {
|
||||
pair("${library_directory}", repository.getLibrariesDirectory(version).getAbsolutePath()),
|
||||
pair("${classpath_separator}", File.pathSeparator),
|
||||
pair("${primary_jar}", repository.getVersionJar(version).getAbsolutePath()),
|
||||
pair("${language}", Locale.getDefault().toString()),
|
||||
pair("${language}", Locale.getDefault().toLanguageTag()),
|
||||
|
||||
// defined by HMCL
|
||||
// libraries_directory stands for historical reasons here. We don't know the official launcher
|
||||
|
||||
@@ -300,11 +300,13 @@ public final class RemoteMod {
|
||||
|
||||
public FileDownloadTask.IntegrityCheck getIntegrityCheck() {
|
||||
if (hashes.containsKey("md5")) {
|
||||
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("sha1"));
|
||||
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("md5"));
|
||||
} else if (hashes.containsKey("sha1")) {
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1"));
|
||||
} else if (hashes.containsKey("sha256")) {
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha256"));
|
||||
} else if (hashes.containsKey("sha512")) {
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha1"));
|
||||
return new FileDownloadTask.IntegrityCheck("SHA-512", hashes.get("sha512"));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -83,7 +84,7 @@ public interface RemoteModRepository {
|
||||
}
|
||||
}
|
||||
|
||||
SearchResult search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
throws IOException;
|
||||
|
||||
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
||||
|
||||
+28
-25
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.mod.curse;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -26,6 +27,7 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -43,10 +45,17 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
|
||||
|
||||
private static final String PREFIX = "https://api.curseforge.com";
|
||||
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getManifestAttribute("CurseForge-Api-Key", ""));
|
||||
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
|
||||
|
||||
private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
|
||||
|
||||
private static <R extends HttpRequest> R withApiKey(R request) {
|
||||
if (request.getUrl().startsWith(PREFIX) && !apiKey.isEmpty()) {
|
||||
request.header("X-API-KEY", apiKey);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public static boolean isAvailable() {
|
||||
return !apiKey.isEmpty();
|
||||
}
|
||||
@@ -100,29 +109,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
int categoryId = 0;
|
||||
if (category != null && category.getSelf() instanceof CurseAddon.Category) {
|
||||
categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
||||
}
|
||||
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
||||
pair("gameId", "432"),
|
||||
pair("classId", Integer.toString(section)),
|
||||
pair("categoryId", Integer.toString(categoryId)),
|
||||
pair("gameVersion", gameVersion),
|
||||
pair("searchFilter", searchFilter),
|
||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||
pair("sortOrder", toSortOrder(sortOrder)),
|
||||
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||
pair("pageSize", Integer.toString(pageSize)))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon>> response = withApiKey(HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", mapOf(
|
||||
pair("gameId", "432"),
|
||||
pair("classId", Integer.toString(section)),
|
||||
pair("categoryId", Integer.toString(categoryId)),
|
||||
pair("gameVersion", gameVersion),
|
||||
pair("searchFilter", searchFilter),
|
||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||
pair("sortOrder", toSortOrder(sortOrder)),
|
||||
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||
pair("pageSize", Integer.toString(pageSize)))))))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
|
||||
if (searchFilter.isEmpty()) {
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/1549
|
||||
String lowerCaseSearchFilter = searchFilter.toLowerCase();
|
||||
String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT);
|
||||
Map<String, Integer> searchFilterWords = new HashMap<>();
|
||||
for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) {
|
||||
searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1);
|
||||
@@ -162,9 +170,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
|
||||
|
||||
Response<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints/432")
|
||||
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
|
||||
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
|
||||
.header("X-API-KEY", apiKey)
|
||||
.getJson(Response.typeOf(FingerprintMatchesResult.class));
|
||||
|
||||
if (response.getData().getExactMatches() == null || response.getData().getExactMatches().isEmpty()) {
|
||||
@@ -176,32 +183,28 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
Response<CurseAddon> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id)
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
|
||||
.getJson(Response.typeOf(CurseAddon.class));
|
||||
return response.data.toMod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
|
||||
Response<CurseAddon.LatestFile> response = HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, modId, fileId)))
|
||||
.getJson(Response.typeOf(CurseAddon.LatestFile.class));
|
||||
return response.getData().toVersion().getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
Response<List<CurseAddon.LatestFile>> response = HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000"))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000")))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class)));
|
||||
return response.getData().stream().map(CurseAddon.LatestFile::toVersion);
|
||||
}
|
||||
|
||||
public List<CurseAddon.Category> getCategoriesImpl() throws IOException {
|
||||
Response<List<CurseAddon.Category>> categories = HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432"))
|
||||
.header("X-API-KEY", apiKey)
|
||||
Response<List<CurseAddon.Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.Category.class)));
|
||||
return reorganizeCategories(categories.getData(), section);
|
||||
}
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod.modrinth;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
@@ -79,7 +80,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
List<List<String>> facets = new ArrayList<>();
|
||||
facets.add(Collections.singletonList("project_type:" + projectType));
|
||||
if (StringUtils.isNotBlank(gameVersion)) {
|
||||
@@ -95,7 +96,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
pair("limit", Integer.toString(pageSize)),
|
||||
pair("index", convertSortType(sort))
|
||||
);
|
||||
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
||||
Response<ProjectSearchResult> response = HttpRequest.GET(downloadProvider.injectURL(NetworkUtils.withQuery(PREFIX + "/v2/search", query)))
|
||||
.getJson(Response.typeOf(ProjectSearchResult.class));
|
||||
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
|
||||
}
|
||||
|
||||
+16
-3
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
@@ -120,7 +121,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
dependencies.add(builder.buildAsync());
|
||||
}
|
||||
|
||||
Path rootPath = repository.getVersionRoot(version).toPath();
|
||||
Path rootPath = repository.getVersionRoot(version).toPath().toAbsolutePath().normalize();
|
||||
Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
|
||||
.collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
|
||||
Function.identity()));
|
||||
@@ -128,12 +129,24 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
Set<String> remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Path runDirectory = repository.getRunDirectory(version).toPath().toAbsolutePath().normalize();
|
||||
Path modsDirectory = runDirectory.resolve("mods");
|
||||
|
||||
int total = 0;
|
||||
// for files in new modpack
|
||||
for (ModpackConfiguration.FileInformation file : remoteManifest.getFiles()) {
|
||||
Path actualPath = rootPath.resolve(file.getPath());
|
||||
Path actualPath = rootPath.resolve(file.getPath()).toAbsolutePath().normalize();
|
||||
String fileName = actualPath.getFileName().toString();
|
||||
|
||||
if (!actualPath.startsWith(rootPath)) {
|
||||
throw new IOException("Unsecure path: " + file.getPath());
|
||||
}
|
||||
|
||||
boolean download;
|
||||
if (!files.containsKey(file.getPath())) {
|
||||
if (!files.containsKey(file.getPath()) ||
|
||||
modsDirectory.equals(actualPath.getParent())
|
||||
&& Files.notExists(actualPath.resolveSibling(fileName + ModManager.DISABLED_EXTENSION))
|
||||
&& Files.notExists(actualPath.resolveSibling(fileName + ModManager.OLD_EXTENSION))) {
|
||||
// If old modpack does not have this entry, download it
|
||||
download = true;
|
||||
} else if (!Files.exists(actualPath)) {
|
||||
|
||||
@@ -38,11 +38,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public final class CacheFileTask extends FetchTask<Path> {
|
||||
|
||||
public CacheFileTask(@NotNull String uri) {
|
||||
super(List.of(NetworkUtils.toURI(uri)));
|
||||
this(NetworkUtils.toURI(uri));
|
||||
}
|
||||
|
||||
public CacheFileTask(@NotNull URI uri) {
|
||||
super(List.of(uri));
|
||||
setName(uri.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,7 +24,6 @@ import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.ReflectionHelper;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
@@ -35,10 +34,9 @@ import java.util.*;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -1009,8 +1007,19 @@ public abstract class Task<T> {
|
||||
void execute(T result, Exception exception) throws Exception;
|
||||
}
|
||||
|
||||
private static final String PACKAGE_PREFIX = Task.class.getPackageName() + ".";
|
||||
private static final Predicate<StackWalker.StackFrame> PREDICATE = stackFrame -> !stackFrame.getClassName().startsWith(PACKAGE_PREFIX);
|
||||
private static final Function<Stream<StackWalker.StackFrame>, Optional<StackWalker.StackFrame>> FUNCTION = stream -> stream.filter(PREDICATE).findFirst();
|
||||
private static final Function<StackWalker.StackFrame, String> FRAME_MAPPING = frame -> {
|
||||
String fileName = frame.getFileName();
|
||||
if (fileName != null)
|
||||
return frame.getClassName() + '.' + frame.getMethodName() + '(' + fileName + ':' + frame.getLineNumber() + ')';
|
||||
else
|
||||
return frame.getClassName() + '.' + frame.getMethodName();
|
||||
};
|
||||
|
||||
private static String getCaller() {
|
||||
return ReflectionHelper.getCaller(packageName -> !"org.jackhuang.hmcl.task".equals(packageName)).toString();
|
||||
return StackWalker.getInstance().walk(FUNCTION).map(FRAME_MAPPING).orElse("Unknown");
|
||||
}
|
||||
|
||||
private static final class SimpleTask<T> extends Task<T> {
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
@@ -75,7 +74,7 @@ public class CacheRepository {
|
||||
index = new LinkedHashMap<>();
|
||||
indexFileLastModified = null;
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to read index file", e);
|
||||
index = new LinkedHashMap<>();
|
||||
indexFileLastModified = null;
|
||||
@@ -354,7 +353,7 @@ public class CacheRepository {
|
||||
}
|
||||
|
||||
public int compareTo(ETagItem other) {
|
||||
if (!url.equals(other.url))
|
||||
if (!url.equals(other.url) && !NetworkUtils.toURI(url).equals(NetworkUtils.toURI(other.url)))
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
ZonedDateTime thisTime = Lang.ignoringException(() -> ZonedDateTime.parse(remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class Pack200Utils {
|
||||
private Pack200Utils() {
|
||||
}
|
||||
|
||||
private static final String[] IMPL_NAMES = {
|
||||
"java.util.jar.Pack200",
|
||||
"org.glavo.pack200.Pack200",
|
||||
"io.pack200.Pack200"
|
||||
};
|
||||
|
||||
private static final MethodHandle newUnpackerHandle;
|
||||
private static final MethodHandle unpackHandle;
|
||||
private static final MethodHandle unpackFileHandle;
|
||||
|
||||
static {
|
||||
Class<?> pack200Class = null;
|
||||
Class<?> unpackerClass = null;
|
||||
|
||||
for (String implName : IMPL_NAMES) {
|
||||
try {
|
||||
pack200Class = Class.forName(implName);
|
||||
unpackerClass = Class.forName(implName + "$Unpacker");
|
||||
break;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (pack200Class == null) {
|
||||
LOG.warning("Pack200 not found");
|
||||
newUnpackerHandle = null;
|
||||
unpackHandle = null;
|
||||
unpackFileHandle = null;
|
||||
} else {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
MethodHandle newUnpacker = null;
|
||||
MethodHandle unpack = null;
|
||||
MethodHandle unpackFile = null;
|
||||
try {
|
||||
newUnpacker = lookup.findStatic(pack200Class, "newUnpacker", MethodType.methodType(unpackerClass));
|
||||
unpack = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, InputStream.class, JarOutputStream.class));
|
||||
unpackFile = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, File.class, JarOutputStream.class));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to find pack200 methods", e);
|
||||
}
|
||||
|
||||
if (newUnpacker != null) {
|
||||
newUnpackerHandle = newUnpacker;
|
||||
unpackHandle = unpack;
|
||||
unpackFileHandle = unpackFile;
|
||||
} else {
|
||||
newUnpackerHandle = null;
|
||||
unpackHandle = null;
|
||||
unpackFileHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return newUnpackerHandle != null;
|
||||
}
|
||||
|
||||
public static void unpack(InputStream in, JarOutputStream out) throws IOException {
|
||||
if (newUnpackerHandle == null) {
|
||||
throw new UnsupportedOperationException("Pack200");
|
||||
}
|
||||
|
||||
try {
|
||||
unpackHandle.invoke(newUnpackerHandle.invoke(), in, out);
|
||||
} catch (IOException | RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unpack(File in, JarOutputStream out) throws IOException {
|
||||
if (newUnpackerHandle == null) {
|
||||
throw new UnsupportedOperationException("Pack200");
|
||||
}
|
||||
|
||||
try {
|
||||
unpackFileHandle.invoke(newUnpackerHandle.invoke(), in, out);
|
||||
} catch (IOException | RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class ReflectionHelper {
|
||||
private ReflectionHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get caller, this method is caller sensitive.
|
||||
*
|
||||
* @param packageFilter returns false if we consider the given package is internal calls, not the caller
|
||||
* @return the caller, method name, source file, line number
|
||||
*/
|
||||
public static StackTraceElement getCaller(Predicate<String> packageFilter) {
|
||||
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
|
||||
// element[0] is Thread.currentThread().getStackTrace()
|
||||
// element[1] is ReflectionHelper.getCaller(packageFilter)
|
||||
// so element[2] is caller of this method.
|
||||
StackTraceElement caller = elements[2];
|
||||
for (int i = 3; i < elements.length; ++i) {
|
||||
if (packageFilter.test(StringUtils.substringBeforeLast(elements[i].getClassName(), '.')) &&
|
||||
!caller.getClassName().equals(elements[i].getClassName()))
|
||||
return elements[i];
|
||||
}
|
||||
return caller;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ public abstract class HttpRequest {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public HttpRequest accept(String contentType) {
|
||||
return header("Accept", contentType);
|
||||
}
|
||||
|
||||
@@ -20,47 +20,51 @@ package org.jackhuang.hmcl.util.io;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.CodeSource;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.Properties;
|
||||
|
||||
public final class JarUtils {
|
||||
private JarUtils() {
|
||||
}
|
||||
|
||||
private static final Path THIS_JAR;
|
||||
|
||||
private static final Manifest manifest;
|
||||
private static final Properties properties = new Properties();
|
||||
|
||||
static {
|
||||
CodeSource cs = JarUtils.class.getProtectionDomain().getCodeSource();
|
||||
Class<?> entryPointClass = null;
|
||||
CodeSource cs = null;
|
||||
try {
|
||||
entryPointClass = Class.forName("org.jackhuang.hmcl.EntryPoint");
|
||||
cs = entryPointClass.getProtectionDomain().getCodeSource();
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
|
||||
if (cs == null) {
|
||||
THIS_JAR = null;
|
||||
manifest = new Manifest();
|
||||
} else {
|
||||
Path path;
|
||||
try {
|
||||
path = Paths.get(cs.getLocation().toURI()).toAbsolutePath();
|
||||
path = Path.of(cs.getLocation().toURI()).toAbsolutePath();
|
||||
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
|
||||
path = null;
|
||||
}
|
||||
if (path == null || !Files.isRegularFile(path)) {
|
||||
THIS_JAR = null;
|
||||
manifest = new Manifest();
|
||||
} else {
|
||||
THIS_JAR = path;
|
||||
Manifest mn;
|
||||
try (JarFile file = new JarFile(path.toFile())) {
|
||||
mn = file.getManifest();
|
||||
} catch (IOException e) {
|
||||
mn = new Manifest();
|
||||
THIS_JAR = path != null && Files.isRegularFile(path) ? path : null;
|
||||
}
|
||||
|
||||
if (entryPointClass != null) {
|
||||
InputStream input = entryPointClass.getResourceAsStream("/assets/hmcl.properties");
|
||||
if (input != null) {
|
||||
try (var reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
|
||||
properties.load(reader);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
manifest = mn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,8 +74,7 @@ public final class JarUtils {
|
||||
return THIS_JAR;
|
||||
}
|
||||
|
||||
public static String getManifestAttribute(String name, String defaultValue) {
|
||||
String value = manifest.getMainAttributes().getValue(name);
|
||||
return value != null ? value : defaultValue;
|
||||
public static String getAttribute(String name, String defaultValue) {
|
||||
return properties.getProperty(name, defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
@@ -45,6 +46,19 @@ public final class NetworkUtils {
|
||||
private NetworkUtils() {
|
||||
}
|
||||
|
||||
public static boolean isLoopbackAddress(URI uri) {
|
||||
String host = uri.getHost();
|
||||
if (StringUtils.isBlank(host))
|
||||
return false;
|
||||
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(host);
|
||||
return addr.isLoopbackAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isHttpUri(URI uri) {
|
||||
return "http".equals(uri.getScheme()) || "https".equals(uri.getScheme());
|
||||
}
|
||||
@@ -146,7 +160,10 @@ public final class NetworkUtils {
|
||||
boolean left = true;
|
||||
while (i < location.length()) {
|
||||
char ch = location.charAt(i);
|
||||
if (ch == ' ' || ch >= 0x80)
|
||||
if (ch == ' '
|
||||
|| ch == '[' || ch == ']'
|
||||
|| ch == '{' || ch == '}'
|
||||
|| ch >= 0x80)
|
||||
break;
|
||||
else if (ch == '?')
|
||||
left = false;
|
||||
@@ -163,22 +180,6 @@ public final class NetworkUtils {
|
||||
|
||||
for (; i < location.length(); i++) {
|
||||
char ch = location.charAt(i);
|
||||
if (Character.isSurrogate(ch)) {
|
||||
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
|
||||
char ch2 = location.charAt(i + 1);
|
||||
if (Character.isLowSurrogate(ch2)) {
|
||||
int codePoint = Character.toCodePoint(ch, ch2);
|
||||
encodeCodePoint(builder, codePoint);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid surrogate pair, encode as '?'
|
||||
builder.append("%3F");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == ' ') {
|
||||
if (left)
|
||||
builder.append("%20");
|
||||
@@ -187,7 +188,23 @@ public final class NetworkUtils {
|
||||
} else if (ch == '?') {
|
||||
left = false;
|
||||
builder.append('?');
|
||||
} else if (ch >= 0x80) {
|
||||
} else if (ch >= 0x80 || (left && (ch == '[' || ch == ']' || ch == '{' || ch == '}'))) {
|
||||
if (Character.isSurrogate(ch)) {
|
||||
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
|
||||
char ch2 = location.charAt(i + 1);
|
||||
if (Character.isLowSurrogate(ch2)) {
|
||||
int codePoint = Character.toCodePoint(ch, ch2);
|
||||
encodeCodePoint(builder, codePoint);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid surrogate pair, encode as U+FFFD (replacement character)
|
||||
encodeCodePoint(builder, 0xfffd);
|
||||
continue;
|
||||
}
|
||||
|
||||
encodeCodePoint(builder, ch);
|
||||
} else {
|
||||
builder.append(ch);
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.util.logging;
|
||||
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.tukaani.xz.LZMA2Options;
|
||||
import org.tukaani.xz.XZOutputStream;
|
||||
|
||||
@@ -127,47 +145,10 @@ public final class Logger {
|
||||
String caller = CLASS_NAME + ".onExit";
|
||||
|
||||
if (logRetention > 0 && logFile != null) {
|
||||
List<Pair<Path, int[]>> list = new ArrayList<>();
|
||||
Pattern fileNamePattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
|
||||
Path dir = logFile.getParent();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
|
||||
for (Path path : stream) {
|
||||
Matcher matcher = fileNamePattern.matcher(path.getFileName().toString());
|
||||
if (matcher.matches() && Files.isRegularFile(path)) {
|
||||
int year = Integer.parseInt(matcher.group("year"));
|
||||
int month = Integer.parseInt(matcher.group("month"));
|
||||
int day = Integer.parseInt(matcher.group("day"));
|
||||
int hour = Integer.parseInt(matcher.group("hour"));
|
||||
int minute = Integer.parseInt(matcher.group("minute"));
|
||||
int second = Integer.parseInt(matcher.group("second"));
|
||||
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
|
||||
|
||||
list.add(Pair.pair(path, new int[]{year, month, day, hour, minute, second, n}));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.WARNING, caller, "Failed to list log files in " + dir, e);
|
||||
}
|
||||
|
||||
var list = findRecentLogFiles(Integer.MAX_VALUE);
|
||||
if (list.size() > logRetention) {
|
||||
list.sort((a, b) -> {
|
||||
int[] v1 = a.getValue();
|
||||
int[] v2 = b.getValue();
|
||||
|
||||
assert v1.length == v2.length;
|
||||
|
||||
for (int i = 0; i < v1.length; i++) {
|
||||
int c = Integer.compare(v1[i], v2[i]);
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (int i = 0, end = list.size() - logRetention; i < end; i++) {
|
||||
Path file = list.get(i).getKey();
|
||||
|
||||
Path file = list.get(i);
|
||||
try {
|
||||
if (!Files.isSameFile(file, logFile)) {
|
||||
log(Level.INFO, caller, "Delete old log file " + file, null);
|
||||
@@ -276,6 +257,40 @@ public final class Logger {
|
||||
return logFile;
|
||||
}
|
||||
|
||||
public @NotNull List<Path> findRecentLogFiles(int n) {
|
||||
if (n <= 0 || logFile == null)
|
||||
return List.of();
|
||||
|
||||
var currentLogFile = LogFile.ofFile(logFile);
|
||||
|
||||
Path logDir = logFile.getParent();
|
||||
if (logDir == null || !Files.isDirectory(logDir))
|
||||
return List.of();
|
||||
|
||||
var logFiles = new ArrayList<LogFile>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(logDir)) {
|
||||
for (Path path : stream) {
|
||||
LogFile item = LogFile.ofFile(path);
|
||||
if (item != null && (currentLogFile == null || item.compareTo(currentLogFile) < 0)) {
|
||||
logFiles.add(item);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.WARNING, CLASS_NAME + ".findRecentLogFiles", "Failed to list log files in " + logDir, e);
|
||||
return List.of();
|
||||
}
|
||||
logFiles.sort(Comparator.naturalOrder());
|
||||
|
||||
final int resultLength = Math.min(n, logFiles.size());
|
||||
final int offset = logFiles.size() - resultLength;
|
||||
|
||||
var result = new Path[resultLength];
|
||||
for (int i = 0; i < resultLength; i++) {
|
||||
result[i] = logFiles.get(i + offset).file;
|
||||
}
|
||||
return List.of(result);
|
||||
}
|
||||
|
||||
public void exportLogs(OutputStream output) throws IOException {
|
||||
Objects.requireNonNull(output);
|
||||
LogEvent.ExportLog event = new LogEvent.ExportLog(output);
|
||||
@@ -352,4 +367,69 @@ public final class Logger {
|
||||
public void trace(String msg, Throwable exception) {
|
||||
log(Level.TRACE, CallerFinder.getCaller(), msg, exception);
|
||||
}
|
||||
|
||||
private static final class LogFile implements Comparable<LogFile> {
|
||||
private static final Pattern FILE_NAME_PATTERN = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
|
||||
|
||||
private static @Nullable LogFile ofFile(Path file) {
|
||||
if (!Files.isRegularFile(file))
|
||||
return null;
|
||||
|
||||
Matcher matcher = FILE_NAME_PATTERN.matcher(file.getFileName().toString());
|
||||
if (!matcher.matches())
|
||||
return null;
|
||||
|
||||
int year = Integer.parseInt(matcher.group("year"));
|
||||
int month = Integer.parseInt(matcher.group("month"));
|
||||
int day = Integer.parseInt(matcher.group("day"));
|
||||
int hour = Integer.parseInt(matcher.group("hour"));
|
||||
int minute = Integer.parseInt(matcher.group("minute"));
|
||||
int second = Integer.parseInt(matcher.group("second"));
|
||||
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
|
||||
|
||||
return new LogFile(file, year, month, day, hour, minute, second, n);
|
||||
}
|
||||
|
||||
private final Path file;
|
||||
private final int year;
|
||||
private final int month;
|
||||
private final int day;
|
||||
private final int hour;
|
||||
private final int minute;
|
||||
private final int second;
|
||||
private final int n;
|
||||
|
||||
private LogFile(Path file, int year, int month, int day, int hour, int minute, int second, int n) {
|
||||
this.file = file;
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
this.second = second;
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Logger.LogFile that) {
|
||||
if (this.year != that.year) return Integer.compare(this.year, that.year);
|
||||
if (this.month != that.month) return Integer.compare(this.month, that.month);
|
||||
if (this.day != that.day) return Integer.compare(this.day, that.day);
|
||||
if (this.hour != that.hour) return Integer.compare(this.hour, that.hour);
|
||||
if (this.minute != that.minute) return Integer.compare(this.minute, that.minute);
|
||||
if (this.second != that.second) return Integer.compare(this.second, that.second);
|
||||
if (this.n != that.n) return Integer.compare(this.n, that.n);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(year, month, day, hour, minute, second, n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof LogFile && compareTo((LogFile) obj) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,12 +306,39 @@ public enum OperatingSystem {
|
||||
if (name.isEmpty())
|
||||
return false;
|
||||
// . and .. have special meaning on all platforms
|
||||
if (name.equals("."))
|
||||
return false;
|
||||
// \0 and / are forbidden on all platforms
|
||||
if (name.indexOf('/') != -1 || name.indexOf('\0') != -1)
|
||||
if (name.equals(".") || name.equals(".."))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char ch = name.charAt(i);
|
||||
int codePoint;
|
||||
|
||||
if (Character.isSurrogate(ch)) {
|
||||
if (!Character.isHighSurrogate(ch))
|
||||
return false;
|
||||
|
||||
if (i == name.length() - 1)
|
||||
return false;
|
||||
|
||||
char ch2 = name.charAt(++i);
|
||||
if (!Character.isLowSurrogate(ch2))
|
||||
return false;
|
||||
|
||||
codePoint = Character.toCodePoint(ch, ch2);
|
||||
} else {
|
||||
codePoint = ch;
|
||||
}
|
||||
|
||||
if (!Character.isValidCodePoint(codePoint)
|
||||
|| Character.isISOControl(codePoint)
|
||||
|| codePoint == '/' || codePoint == '\0'
|
||||
// Unicode replacement character
|
||||
|| codePoint == 0xfffd
|
||||
// Not Unicode character
|
||||
|| codePoint == 0xfffe || codePoint == 0xffff)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CURRENT_OS == WINDOWS) { // Windows only
|
||||
char lastChar = name.charAt(name.length() - 1);
|
||||
// filenames ending in dot are not valid
|
||||
|
||||
+4
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -93,6 +94,9 @@ final class WindowsGPUDetector {
|
||||
return null;
|
||||
} else if (object instanceof String[]) {
|
||||
return String.join(" ", (String[]) object);
|
||||
} else if (object instanceof byte[]) {
|
||||
return new String((byte[]) object, StandardCharsets.UTF_16LE)
|
||||
.replace("\0", "");
|
||||
} else {
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.versioning;
|
||||
|
||||
import org.intellij.lang.annotations.MagicConstant;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -98,8 +99,20 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean isAprilFools() {
|
||||
if (this instanceof Special)
|
||||
return true;
|
||||
|
||||
if (this instanceof Snapshot) {
|
||||
Snapshot snapshot = (Snapshot) this;
|
||||
return snapshot.intValue == Snapshot.toInt(15, 14, 'a');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
enum Type {
|
||||
PRE_CLASSIC, CLASSIC, INFDEV, ALPHA, BETA, NEW
|
||||
PRE_CLASSIC, CLASSIC, INDEV, INFDEV, ALPHA, BETA, NEW
|
||||
}
|
||||
|
||||
abstract Type getType();
|
||||
@@ -123,7 +136,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
return value;
|
||||
}
|
||||
|
||||
static final class Old extends GameVersionNumber {
|
||||
public static final class Old extends GameVersionNumber {
|
||||
static Old parse(String value) {
|
||||
Type type;
|
||||
int prefixLength = 1;
|
||||
@@ -137,11 +150,15 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
prefixLength = "rd-".length();
|
||||
break;
|
||||
case 'i':
|
||||
if (!value.startsWith("inf-")) {
|
||||
if (value.startsWith("inf-")) {
|
||||
type = Type.INFDEV;
|
||||
prefixLength = "inf-".length();
|
||||
} else if (value.startsWith("in-")) {
|
||||
type = Type.INDEV;
|
||||
prefixLength = "in-".length();
|
||||
} else {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
type = Type.INFDEV;
|
||||
prefixLength = "inf-".length();
|
||||
break;
|
||||
case 'a':
|
||||
type = Type.ALPHA;
|
||||
@@ -195,16 +212,16 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
}
|
||||
|
||||
static final class Release extends GameVersionNumber {
|
||||
public static final class Release extends GameVersionNumber {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("1\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?((?<eaType>(-[a-zA-Z]+| Pre-Release ))(?<eaVersion>.+))?");
|
||||
|
||||
static final int TYPE_GA = Integer.MAX_VALUE;
|
||||
public static final int TYPE_GA = Integer.MAX_VALUE;
|
||||
|
||||
static final int TYPE_UNKNOWN = 0;
|
||||
static final int TYPE_EXP = 1;
|
||||
static final int TYPE_PRE = 2;
|
||||
static final int TYPE_RC = 3;
|
||||
public static final int TYPE_UNKNOWN = 0;
|
||||
public static final int TYPE_EXP = 1;
|
||||
public static final int TYPE_PRE = 2;
|
||||
public static final int TYPE_RC = 3;
|
||||
|
||||
static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, VersionNumber.ZERO);
|
||||
|
||||
@@ -243,6 +260,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
private final int minor;
|
||||
private final int patch;
|
||||
|
||||
@MagicConstant(intValues = {TYPE_GA, TYPE_UNKNOWN, TYPE_EXP, TYPE_PRE, TYPE_RC})
|
||||
private final int eaType;
|
||||
private final VersionNumber eaVersion;
|
||||
|
||||
@@ -310,6 +328,26 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
throw new AssertionError(other.getClass());
|
||||
}
|
||||
|
||||
public int getMajor() {
|
||||
return major;
|
||||
}
|
||||
|
||||
public int getMinor() {
|
||||
return minor;
|
||||
}
|
||||
|
||||
public int getPatch() {
|
||||
return patch;
|
||||
}
|
||||
|
||||
public int getEaType() {
|
||||
return eaType;
|
||||
}
|
||||
|
||||
public VersionNumber getEaVersion() {
|
||||
return eaVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(major, minor, patch, eaType, eaVersion);
|
||||
@@ -324,7 +362,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
}
|
||||
|
||||
static final class Snapshot extends GameVersionNumber {
|
||||
public static final class Snapshot extends GameVersionNumber {
|
||||
static Snapshot parse(String value) {
|
||||
if (value.length() != 6 || value.charAt(2) != 'w')
|
||||
throw new IllegalArgumentException(value);
|
||||
@@ -375,6 +413,18 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
throw new AssertionError(other.getClass());
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return (intValue >> 16) & 0xff;
|
||||
}
|
||||
|
||||
public int getWeek() {
|
||||
return (intValue >> 8) & 0xff;
|
||||
}
|
||||
|
||||
public char getSuffix() {
|
||||
return (char) (intValue & 0xff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@@ -389,7 +439,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
}
|
||||
|
||||
static final class Special extends GameVersionNumber {
|
||||
public static final class Special extends GameVersionNumber {
|
||||
private VersionNumber versionNumber;
|
||||
|
||||
private GameVersionNumber prev;
|
||||
|
||||
@@ -18,17 +18,14 @@
|
||||
package org.jackhuang.hmcl.util.versioning;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Copied from org.apache.maven.artifact.versioning.ComparableVersion
|
||||
* Apache License 2.0
|
||||
*
|
||||
* <p>
|
||||
* Maybe we can migrate to org.jenkins-ci:version-number:1.7?
|
||||
*
|
||||
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
|
||||
*/
|
||||
public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
@@ -220,9 +217,17 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
*/
|
||||
private static final class StringItem implements Item {
|
||||
private final String value;
|
||||
private final boolean pre;
|
||||
|
||||
StringItem(String value) {
|
||||
this.value = value;
|
||||
|
||||
String lower = value.trim().toLowerCase(Locale.ROOT);
|
||||
this.pre = lower.startsWith("alpha")
|
||||
|| lower.startsWith("beta")
|
||||
|| lower.startsWith("pre")
|
||||
|| lower.startsWith("rc")
|
||||
|| lower.startsWith("experimental");
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
@@ -235,8 +240,8 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
|
||||
public int compareTo(Item item) {
|
||||
if (item == null) {
|
||||
// 1-string > 1
|
||||
return 1;
|
||||
// 1-beta < 1 < 1-string
|
||||
return pre ? -1 : 1;
|
||||
}
|
||||
switch (item.getType()) {
|
||||
case LONG_ITEM:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# https://gitlab.freedesktop.org/mesa/drm/-/blob/b65d6ede3ebe64dbde5805007ddef22f390f8050/data/amdgpu.ids
|
||||
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/7d43d9b6fe5df4b234937fce9e51adb1b327ded1/data/amdgpu.ids
|
||||
#
|
||||
# List of AMDGPU IDs
|
||||
#
|
||||
@@ -591,7 +591,9 @@
|
||||
74B6, 00, AMD Instinct MI308X
|
||||
74BD, 00, AMD Instinct MI300X HF
|
||||
7550, C0, AMD Radeon RX 9070 XT
|
||||
7550, C2, AMD Radeon RX 9070 GRE
|
||||
7550, C3, AMD Radeon RX 9070
|
||||
7551, C0, AMD Radeon AI PRO R9700
|
||||
9830, 00, AMD Radeon HD 8400 / R3 Series
|
||||
9831, 00, AMD Radeon HD 8400E
|
||||
9832, 00, AMD Radeon HD 8330
|
||||
|
||||
@@ -19,33 +19,50 @@ package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.io.NetworkUtils.encodeLocation;
|
||||
import static org.jackhuang.hmcl.util.io.NetworkUtils.getCharsetFromContentType;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.jackhuang.hmcl.util.io.NetworkUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public class NetworkUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testIsLoopbackAddress() {
|
||||
assertTrue(isLoopbackAddress(URI.create("https://127.0.0.1/test")));
|
||||
assertTrue(isLoopbackAddress(URI.create("https://127.0.0.1:8080/test")));
|
||||
assertTrue(isLoopbackAddress(URI.create("https://localhost/test")));
|
||||
assertTrue(isLoopbackAddress(URI.create("https://localhost:8080/test")));
|
||||
assertTrue(isLoopbackAddress(URI.create("https://[::1]/test")));
|
||||
assertTrue(isLoopbackAddress(URI.create("https://[::1]:8080/test")));
|
||||
|
||||
assertFalse(isLoopbackAddress(URI.create("https://www.example.com/test")));
|
||||
assertFalse(isLoopbackAddress(URI.create("https://www.example.com:8080/test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeLocation() {
|
||||
assertEquals("https://github.com", encodeLocation("https://github.com"));
|
||||
assertEquals("https://github.com/HMCL-dev/HMCL/commits?author=Glavo", encodeLocation("https://github.com/HMCL-dev/HMCL/commits?author=Glavo"));
|
||||
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file with space"));
|
||||
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file%20with%20space"));
|
||||
assertEquals("https://www.example.com/%5Bfile%5D", encodeLocation("https://www.example.com/[file]"));
|
||||
assertEquals("https://www.example.com/%7Bfile%7D", encodeLocation("https://www.example.com/{file}"));
|
||||
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95", encodeLocation("https://www.example.com/测试"));
|
||||
assertEquals("https://www.example.com/%F0%9F%98%87", encodeLocation("https://www.example.com/\uD83D\uDE07"));
|
||||
assertEquals("https://www.example.com/test?a=10+20", encodeLocation("https://www.example.com/test?a=10 20"));
|
||||
assertEquals("https://www.example.com/test?a=10+20&b=[30]&c={40}", encodeLocation("https://www.example.com/test?a=10 20&b=[30]&c={40}"));
|
||||
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95?a=10+20", encodeLocation("https://www.example.com/测试?a=10 20"));
|
||||
|
||||
// Invalid surrogate pair
|
||||
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uD83D"));
|
||||
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uDE07"));
|
||||
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uD83Dtest"));
|
||||
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uDE07test"));
|
||||
assertEquals("https://www.example.com/%EF%BF%BD", encodeLocation("https://www.example.com/\uD83D"));
|
||||
assertEquals("https://www.example.com/%EF%BF%BD", encodeLocation("https://www.example.com/\uDE07"));
|
||||
assertEquals("https://www.example.com/%EF%BF%BDtest", encodeLocation("https://www.example.com/\uD83Dtest"));
|
||||
assertEquals("https://www.example.com/%EF%BF%BDtest", encodeLocation("https://www.example.com/\uDE07test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.util.platform;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.jackhuang.hmcl.util.platform.OperatingSystem.isNameValid;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class OperatingSystemTest {
|
||||
@Test
|
||||
public void testIsNameValid() {
|
||||
assertTrue(isNameValid("example"));
|
||||
assertTrue(isNameValid("example.zip"));
|
||||
assertTrue(isNameValid("example.tar.gz"));
|
||||
assertTrue(isNameValid("\uD83D\uDE00"));
|
||||
assertTrue(isNameValid("a\uD83D\uDE00b"));
|
||||
|
||||
assertFalse(isNameValid("."));
|
||||
assertFalse(isNameValid(".."));
|
||||
assertFalse(isNameValid("exam\0ple"));
|
||||
assertFalse(isNameValid("example/0"));
|
||||
|
||||
// Test for invalid surrogate pair
|
||||
assertFalse(isNameValid("\uD83D"));
|
||||
assertFalse(isNameValid("\uDE00"));
|
||||
assertFalse(isNameValid("\uDE00\uD83D"));
|
||||
assertFalse(isNameValid("\uD83D\uD83D"));
|
||||
assertFalse(isNameValid("a\uD83D"));
|
||||
assertFalse(isNameValid("a\uDE00"));
|
||||
assertFalse(isNameValid("a\uDE00\uD83D"));
|
||||
assertFalse(isNameValid("a\uD83Db"));
|
||||
assertFalse(isNameValid("a\uDE00b"));
|
||||
assertFalse(isNameValid("a\uDE00\uD83Db"));
|
||||
}
|
||||
}
|
||||
@@ -99,9 +99,34 @@ public final class GameVersionNumberTest {
|
||||
assertEquals(VersionNumber.asVersion(versionNumber), old.versionNumber);
|
||||
}
|
||||
|
||||
private static boolean isAprilFools(String version) {
|
||||
return GameVersionNumber.asGameVersion(version).isAprilFools();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAprilFools() {
|
||||
assertTrue(isAprilFools("15w14a"));
|
||||
assertTrue(isAprilFools("1.RV-Pre1"));
|
||||
assertTrue(isAprilFools("3D Shareware v1.34"));
|
||||
assertTrue(isAprilFools("2.0"));
|
||||
assertTrue(isAprilFools("20w14infinite"));
|
||||
assertTrue(isAprilFools("22w13oneBlockAtATime"));
|
||||
assertTrue(isAprilFools("23w13a_or_b"));
|
||||
assertTrue(isAprilFools("24w14potato"));
|
||||
assertTrue(isAprilFools("25w14craftmine"));
|
||||
|
||||
assertFalse(isAprilFools("1.21.8"));
|
||||
assertFalse(isAprilFools("1.21.8-rc1"));
|
||||
assertFalse(isAprilFools("25w21a"));
|
||||
assertFalse(isAprilFools("13w12~"));
|
||||
assertFalse(isAprilFools("15w14b"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseOld() {
|
||||
assertOldVersion("rd-132211", GameVersionNumber.Type.PRE_CLASSIC, "132211");
|
||||
assertOldVersion("in-20100223", GameVersionNumber.Type.INDEV, "20100223");
|
||||
assertOldVersion("in-20100212-2", GameVersionNumber.Type.INDEV, "20100212-2");
|
||||
assertOldVersion("inf-20100618", GameVersionNumber.Type.INFDEV, "20100618");
|
||||
assertOldVersion("inf-20100330-1", GameVersionNumber.Type.INFDEV, "20100330-1");
|
||||
assertOldVersion("a1.0.6", GameVersionNumber.Type.ALPHA, "1.0.6");
|
||||
|
||||
@@ -90,12 +90,17 @@ public class VersionNumberTest {
|
||||
assertLessThan("1.99999999999999999999", "1.199999999999999999999");
|
||||
assertLessThan("1.99999999999999999999", "2");
|
||||
assertLessThan("1.99999999999999999999", "2.0");
|
||||
assertLessThan("1.0", "1.0-zzz");
|
||||
assertLessThan("1.0-beta.1", "1.0");
|
||||
assertLessThan("1.0-alpha.1", "1.0-beta.1");
|
||||
assertLessThan("3.6.15", "3.6.15.289");
|
||||
assertLessThan("3.6.15.289", "3.6.16");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSorting() {
|
||||
final Comparator<String> comparator = ((Comparator<String>) VersionNumber::compare).thenComparing(String::compareTo);
|
||||
final List<String> input = Collections.unmodifiableList(Arrays.asList(
|
||||
final List<String> input = List.of(
|
||||
"0",
|
||||
"0.10.0",
|
||||
"1.6.4",
|
||||
@@ -120,6 +125,9 @@ public class VersionNumberTest {
|
||||
"1.8.9",
|
||||
"1.8.9-forge1902",
|
||||
"1.9",
|
||||
"1.10-alpha.2",
|
||||
"1.10-beta.1",
|
||||
"1.10-beta.2",
|
||||
"1.10",
|
||||
"1.10.2",
|
||||
"1.10.2-AOE",
|
||||
@@ -140,7 +148,8 @@ public class VersionNumberTest {
|
||||
"1.99999999999999999999",
|
||||
"2",
|
||||
"2.0",
|
||||
"2.1"));
|
||||
"2.1"
|
||||
);
|
||||
|
||||
List<String> output = new ArrayList<>(input);
|
||||
output.sort(comparator);
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.7) |
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (Old World) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.7) |
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.7) |
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.7) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.7) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.7) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
|
||||
@@ -52,7 +52,7 @@ Simply execute the following command in the project root directory:
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Make sure you have Java 11 or later installed.
|
||||
Make sure you have Java 17 or later installed.
|
||||
|
||||
## JVM Options (for debugging)
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ HMCL 有着强大的跨平台能力。它不仅支持 Windows、Linux、macOS
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
请确保你至少安装了 JDK 11 或更高版本。
|
||||
请确保你至少安装了 JDK 17 或更高版本。
|
||||
|
||||
## JVM 选项 (用于调试)
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ HMCL 有著強大的跨平臺能力。它不僅支援 Windows、Linux、macOS
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
請確保你至少安裝了 JDK 11 或更高版本。
|
||||
請確保你至少安裝了 JDK 17 或更高版本。
|
||||
|
||||
## JVM 選項 (用於除錯)
|
||||
|
||||
|
||||
@@ -16,12 +16,13 @@ twelvemonkeys = "3.12.0"
|
||||
jna = "5.17.0"
|
||||
pci-ids = "0.4.0"
|
||||
java-info = "1.0"
|
||||
authlib-injector = "1.2.5"
|
||||
|
||||
# testing
|
||||
junit = "5.13.4"
|
||||
|
||||
# plugins
|
||||
shadow = "8.3.6"
|
||||
shadow = "9.0.1"
|
||||
|
||||
[libraries]
|
||||
hmclauncher = { module = "org.glavo.hmcl:HMCLauncher", version.ref = "hmclauncher" }
|
||||
@@ -43,6 +44,7 @@ jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
|
||||
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }
|
||||
java-info = { module = "org.glavo:java-info", version.ref = "java-info" }
|
||||
authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" }
|
||||
|
||||
# testing
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
|
||||
Reference in new issue
Block a user