Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef6cfa785f | ||
|
|
2b792e73c9 | ||
|
|
0f2922dab4 | ||
|
|
45b55e2853 | ||
|
|
9c7795687f | ||
|
|
b273c80a02 | ||
|
|
8cdab4710b | ||
|
|
ea5b31d537 | ||
|
|
bb0c11fa12 | ||
|
|
37186439cb | ||
|
|
ab9f93d68e | ||
|
|
ca6920e82a | ||
|
|
d9db108a31 | ||
|
|
34b7e1d582 | ||
|
|
4d73187376 | ||
|
|
dcd0a42705 | ||
|
|
1b40916046 | ||
|
|
a4e4782f2f | ||
|
|
875b0c27ef | ||
|
|
a38a1775a0 | ||
|
|
fd6ae986c3 | ||
|
|
74a87e65c8 | ||
|
|
a285d2c27d | ||
|
|
44ac3bc900 | ||
|
|
1c346c97a8 | ||
|
|
d890b41380 | ||
|
|
cbcc89b48f | ||
|
|
521bd00a70 | ||
|
|
b4693423ef | ||
|
|
594a2741d7 | ||
|
|
a1d9b52b9d | ||
|
|
c9d825b6c1 | ||
|
|
7dcd0eb616 | ||
|
|
e79a95b885 | ||
|
|
5d232c3f5e | ||
|
|
ee1c0d362d | ||
|
|
aafcd57c82 | ||
|
|
66a4566dc5 | ||
|
|
4c317f5b03 | ||
|
|
12d706a812 | ||
|
|
c006068580 | ||
|
|
f8a838506d | ||
|
|
270844bbb1 | ||
|
|
c9466cf8ef | ||
|
|
7d6f1f98f1 | ||
|
|
fb9418dead | ||
|
|
4a1b06b80c | ||
|
|
8905f2e855 | ||
|
|
6c74364fa5 | ||
|
|
f0720c1755 | ||
|
|
f1acc1d2c1 | ||
|
|
82bddbfa6e | ||
|
|
841755fe56 | ||
|
|
340ee751e8 | ||
|
|
cf70f72f9c | ||
|
|
16ea88a659 | ||
|
|
eaa6790013 | ||
|
|
af571d533d | ||
|
|
004c3c588a | ||
|
|
2a44167c22 | ||
|
|
cf6b735026 | ||
|
|
5918b04b48 | ||
|
|
8004cb3a64 | ||
|
|
9981423927 | ||
|
|
0e5250dfb3 | ||
|
|
2dc956b357 | ||
|
|
b8bd0a08e0 | ||
|
|
fb31eebcf2 | ||
|
|
3c8689a3f1 | ||
|
|
2957d66dd6 | ||
|
|
6f6efe0874 | ||
|
|
355fc04069 | ||
|
|
b4738f383b | ||
|
|
b12711969c | ||
|
|
94727353c4 | ||
|
|
5eb7449b74 |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
# 公告
|
||||
|
||||
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
|
||||
|
||||
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
|
||||
|
||||
|
||||
|
||||
我们发现,对于不经常检查邮箱的人来说,GitHub issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
|
||||
|
||||
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
|
||||
|
||||
|
||||
|
||||
# 问题提交
|
||||
|
||||
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
|
||||
|
||||
* 启动器版本:
|
||||
* 操作系统:
|
||||
* Java 版本:
|
||||
* 错误截图(最好请将整个电脑屏幕的截图发上来):
|
||||
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
|
||||
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
|
||||
* 游戏崩溃报告(如果有):
|
||||
* 启动器崩溃报告(如果有):
|
||||
* 启动器日志文件(在启动器设置中打开日志文件夹并将文件夹内所有文件打包发在这里):
|
||||
* 问题描述(如何触发问题):
|
||||
+1
-1
@@ -33,4 +33,4 @@ NVIDIA
|
||||
.settings
|
||||
|
||||
# netbeans
|
||||
.nb-gradle
|
||||
.nb-gradle
|
||||
@@ -110,6 +110,24 @@ def createExecutable(String suffix, String header) {
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
processResources {
|
||||
ext.convertToBSS = { String resource ->
|
||||
exclude resource
|
||||
doFirst {
|
||||
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
|
||||
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
|
||||
bssFile.parentFile.mkdirs()
|
||||
exec {
|
||||
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
|
||||
}
|
||||
}
|
||||
}
|
||||
from "build/compiled-resources"
|
||||
|
||||
convertToBSS "assets/css/root.css"
|
||||
convertToBSS "assets/css/blue.css"
|
||||
}
|
||||
|
||||
task makePack(dependsOn: jar) {
|
||||
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
|
||||
doLast {
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
@@ -50,22 +49,25 @@ public final class Launcher extends Application {
|
||||
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (IOException e) {
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
}
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (IOException e) {
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
}
|
||||
|
||||
try {
|
||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.setScene(Controllers.getScene());
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
Platform.runLater(() -> {
|
||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.setScene(Controllers.getScene());
|
||||
|
||||
UpdateChecker.init();
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
primaryStage.show();
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
@@ -74,19 +76,14 @@ public final class Launcher extends Application {
|
||||
public static void main(String[] args) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
|
||||
if (!FileUtils.makeDirectory(LOG_DIRECTORY))
|
||||
System.out.println("Unable to create log directory " + LOG_DIRECTORY + ", log files cannot be generated.");
|
||||
|
||||
try {
|
||||
Logging.start(LOG_DIRECTORY);
|
||||
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
|
||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
|
||||
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
|
||||
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
@@ -143,9 +140,5 @@ public final class Launcher extends Application {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final File MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs");
|
||||
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
@@ -29,6 +31,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -37,10 +40,15 @@ public final class Main {
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
|
||||
checkJavaFX();
|
||||
checkDirectoryPath();
|
||||
checkDSTRootCAX3();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(() -> checkDSTRootCAX3(), "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
@@ -33,4 +36,7 @@ public final class Metadata {
|
||||
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
|
||||
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
@@ -41,7 +41,7 @@ public final class AccountHelper {
|
||||
|
||||
private AccountHelper() {}
|
||||
|
||||
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
|
||||
public static final File SKIN_DIR = Metadata.HMCL_DIRECTORY.resolve("skins").toFile();
|
||||
|
||||
public static void loadSkins() {
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
@@ -140,12 +140,12 @@ public final class AccountHelper {
|
||||
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh) throws Exception {
|
||||
account.clearCache();
|
||||
|
||||
Optional<Texture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
File file = getSkinFile(profile.getId());
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
Optional<Texture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
@@ -153,12 +153,12 @@ public final class AccountHelper {
|
||||
account.clearCache();
|
||||
|
||||
if (account.getCharacter() == null) return;
|
||||
Optional<Texture> texture = account.getSkin();
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
File file = getSkinFile(account.getUUID());
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
Optional<Texture> texture = account.getSkin();
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -93,6 +92,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
refreshVersionsAsync().start();
|
||||
}
|
||||
|
||||
private void clean(File directory) throws IOException {
|
||||
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
|
||||
FileUtils.deleteDirectory(new File(directory, "logs"));
|
||||
}
|
||||
|
||||
public void clean(String id) throws IOException {
|
||||
clean(getBaseDirectory());
|
||||
clean(getRunDirectory(id));
|
||||
}
|
||||
|
||||
private File getVersionSettingFile(String id) {
|
||||
return new File(getVersionRoot(id), "hmclversion.cfg");
|
||||
}
|
||||
@@ -217,8 +226,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
beingModpackVersions.remove(id);
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
|
||||
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
private static final HashSet<String> FORBIDDEN = new HashSet<>(Arrays.asList("modpack", "minecraftinstance", "manifest"));
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
@@ -62,6 +63,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -144,8 +146,10 @@ public final class LauncherHelper {
|
||||
try {
|
||||
variables.set("account", account.logIn());
|
||||
} catch (CredentialExpiredException e) {
|
||||
LOG.info("Credential has expired: " + e);
|
||||
variables.set("account", DialogController.logIn(account));
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.warning("Authentication failed, try playing offline: " + e);
|
||||
variables.set("account",
|
||||
account.playOffline().orElseThrow(() -> e));
|
||||
}
|
||||
@@ -216,7 +220,10 @@ public final class LauncherHelper {
|
||||
if (ex != null) {
|
||||
String message;
|
||||
if (ex instanceof CurseCompletionException) {
|
||||
message = i18n("modpack.type.curse.error");
|
||||
if (ex.getCause() instanceof FileNotFoundException)
|
||||
message = i18n("modpack.type.curse.not_found");
|
||||
else
|
||||
message = i18n("modpack.type.curse.error");
|
||||
} else if (ex instanceof PermissionException) {
|
||||
message = i18n("launch.failed.executable_permission");
|
||||
} else if (ex instanceof ProcessCreationException) {
|
||||
@@ -243,6 +250,11 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
|
||||
if (setting.isNotCheckJVM()) {
|
||||
onAccept.run();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean flag = false;
|
||||
boolean java8required = false;
|
||||
boolean newJavaRequired = false;
|
||||
@@ -290,8 +302,10 @@ public final class LauncherHelper {
|
||||
if (java8.isPresent()) {
|
||||
java8required = true;
|
||||
setting.setJavaVersion(java8.get());
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageBox.INFORMATION_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
@@ -343,6 +357,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
@@ -350,12 +365,26 @@ public final class LauncherHelper {
|
||||
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// Forge 2760~2773 will crash game with LiteLoader.
|
||||
if (!flag) {
|
||||
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
0 <= VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) &&
|
||||
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!flag)
|
||||
onAccept.run();
|
||||
}
|
||||
@@ -454,6 +483,39 @@ public final class LauncherHelper {
|
||||
});
|
||||
}
|
||||
|
||||
private void finishLaunch() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
Platform.runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().hide();
|
||||
emitStatus(LoadingState.DONE);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case CLOSE:
|
||||
// Never come to here.
|
||||
break;
|
||||
case KEEP:
|
||||
Platform.runLater(() -> {
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case HIDE:
|
||||
Platform.runLater(() -> {
|
||||
// If application was stopped and execution services did not finish termination,
|
||||
// these codes will be executed.
|
||||
if (Controllers.getStage() != null) {
|
||||
Controllers.getStage().close();
|
||||
emitStatus(LoadingState.DONE);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onLog(String log, Log4jLevel level) {
|
||||
String newLog = log;
|
||||
@@ -483,28 +545,7 @@ public final class LauncherHelper {
|
||||
|
||||
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
|
||||
lwjgl = true;
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
Platform.runLater(() -> {
|
||||
Controllers.getStage().hide();
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case CLOSE:
|
||||
// Never come to here.
|
||||
break;
|
||||
case KEEP:
|
||||
Platform.runLater(() -> {
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case HIDE:
|
||||
Platform.runLater(() -> {
|
||||
Controllers.getStage().close();
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
}
|
||||
finishLaunch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +554,9 @@ public final class LauncherHelper {
|
||||
if (exitType == ExitType.INTERRUPTED)
|
||||
return;
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) finishLaunch();
|
||||
|
||||
if (exitType != ExitType.NORMAL && logWindow == null)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
|
||||
@@ -23,14 +23,16 @@ import javafx.beans.property.ReadOnlyListProperty;
|
||||
import javafx.beans.property.ReadOnlyListWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
|
||||
@@ -39,6 +41,8 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
@@ -60,36 +64,39 @@ public final class Accounts {
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
|
||||
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), DownloadProviders::getDownloadProvider)::getArtifactInfo,
|
||||
Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
|
||||
|
||||
private static final String TYPE_OFFLINE = "offline";
|
||||
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
|
||||
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||
static {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("yggdrasil", FACTORY_YGGDRASIL);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
private static Map<String, AccountFactory<?>> type2factory = mapOf(
|
||||
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
|
||||
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
|
||||
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
|
||||
|
||||
private static String accountType(Account account) {
|
||||
if (account instanceof OfflineAccount)
|
||||
return TYPE_OFFLINE;
|
||||
else if (account instanceof AuthlibInjectorAccount)
|
||||
return TYPE_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return TYPE_YGGDRASIL_ACCOUNT;
|
||||
else
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
||||
}
|
||||
|
||||
public static String getLoginType(AccountFactory<?> factory) {
|
||||
return Optional.ofNullable(factory2type.get(factory))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
|
||||
}
|
||||
|
||||
public static AccountFactory<?> getAccountFactory(String loginType) {
|
||||
return Optional.ofNullable(type2factory.get(loginType))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
|
||||
}
|
||||
// ====
|
||||
|
||||
public static AccountFactory<?> getAccountFactory(Account account) {
|
||||
return type2factory.get(accountType(account));
|
||||
}
|
||||
|
||||
static String accountId(Account account) {
|
||||
return account.getUsername() + ":" + account.getCharacter();
|
||||
if (account instanceof OfflineAccount)
|
||||
return FACTORY_OFFLINE;
|
||||
else if (account instanceof AuthlibInjectorAccount)
|
||||
return FACTORY_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return FACTORY_YGGDRASIL;
|
||||
else
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
}
|
||||
|
||||
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||
@@ -126,7 +133,7 @@ public final class Accounts {
|
||||
// selection is valid, store it
|
||||
if (!initialized)
|
||||
return;
|
||||
config().setSelectedAccount(selected == null ? "" : accountId(selected));
|
||||
updateAccountStorages();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,9 +146,12 @@ public final class Accounts {
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
}
|
||||
|
||||
static Map<Object, Object> getAccountStorage(Account account) {
|
||||
private static Map<Object, Object> getAccountStorage(Account account) {
|
||||
Map<Object, Object> storage = account.toStorage();
|
||||
storage.put("type", accountType(account));
|
||||
storage.put("type", getLoginType(getAccountFactory(account)));
|
||||
if (account == selectedAccount.get()) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
@@ -176,27 +186,38 @@ public final class Accounts {
|
||||
return;
|
||||
}
|
||||
accounts.add(account);
|
||||
|
||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||
selectedAccount.set(account);
|
||||
}
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
|
||||
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
||||
|
||||
// load selected account
|
||||
Account selected = accounts.stream()
|
||||
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
selectedAccount.set(selected);
|
||||
|
||||
Schedulers.io().schedule(() -> {
|
||||
if (selected != null)
|
||||
Account selected = selectedAccount.get();
|
||||
if (selected != null) {
|
||||
Schedulers.io().schedule(() -> {
|
||||
try {
|
||||
selected.logIn();
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
Schedulers.io().schedule(() -> {
|
||||
try {
|
||||
server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static ObservableList<Account> getAccounts() {
|
||||
@@ -220,21 +241,22 @@ public final class Accounts {
|
||||
}
|
||||
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(Metadata.HMCL_DIRECTORY, DownloadProviders::getDownloadProvider);
|
||||
} else {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
return config().getAuthlibInjectorServers().stream()
|
||||
.filter(server -> url.equals(server.getUrl()))
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
// this usually happens when migrating data from an older version
|
||||
AuthlibInjectorServer server;
|
||||
try {
|
||||
server = AuthlibInjectorServer.fetchServerInfo(url);
|
||||
LOG.info("Migrated authlib injector server " + server);
|
||||
} catch (IOException e) {
|
||||
server = new AuthlibInjectorServer(url, url);
|
||||
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
|
||||
}
|
||||
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
config().getAuthlibInjectorServers().add(server);
|
||||
return server;
|
||||
});
|
||||
@@ -255,18 +277,14 @@ public final class Accounts {
|
||||
// ====
|
||||
|
||||
// ==== Login type name i18n ===
|
||||
private static Map<AccountFactory<?>, String> loginType2name = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, i18n("account.methods.offline")),
|
||||
pair(Accounts.FACTORY_YGGDRASIL, i18n("account.methods.yggdrasil")),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, i18n("account.methods.authlib_injector")));
|
||||
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
||||
pair(Accounts.FACTORY_YGGDRASIL, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
|
||||
|
||||
public static String getAccountTypeName(AccountFactory<?> factory) {
|
||||
return Optional.ofNullable(loginType2name.get(factory))
|
||||
.orElseThrow(() -> new IllegalArgumentException("No corresponding login type name"));
|
||||
}
|
||||
|
||||
public static String getAccountTypeName(Account account) {
|
||||
return getAccountTypeName(getAccountFactory(account));
|
||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")));
|
||||
}
|
||||
// ====
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import org.hildan.fxgson.creators.ObservableListCreator;
|
||||
import org.hildan.fxgson.creators.ObservableMapCreator;
|
||||
import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
@@ -40,36 +40,32 @@ import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.Proxy;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Config implements Cloneable, Observable {
|
||||
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
|
||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
||||
.registerTypeAdapter(Theme.class, new Theme.TypeAdapter())
|
||||
.registerTypeAdapter(SupportedLocale.class, new SupportedLocale.TypeAdapter())
|
||||
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
|
||||
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
public static Config fromJson(String json) throws JsonParseException {
|
||||
Config instance = CONFIG_GSON.fromJson(json, Config.class);
|
||||
// Gson will replace the property fields (even they are final!)
|
||||
// So we have to add the listeners again after deserialization
|
||||
instance.addListenerToProperties();
|
||||
Config loaded = CONFIG_GSON.fromJson(json, Config.class);
|
||||
Config instance = new Config();
|
||||
PropertyUtils.copyProperties(loaded, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -86,7 +82,7 @@ public final class Config implements Cloneable, Observable {
|
||||
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
|
||||
@SerializedName("commonpath")
|
||||
private StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
|
||||
private StringProperty commonDirectory = new SimpleStringProperty(Metadata.MINECRAFT_DIRECTORY.toString());
|
||||
|
||||
@SerializedName("hasProxy")
|
||||
private BooleanProperty hasProxy = new SimpleBooleanProperty();
|
||||
@@ -124,9 +120,6 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("accounts")
|
||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||
|
||||
@SerializedName("selectedAccount")
|
||||
private StringProperty selectedAccount = new SimpleStringProperty("");
|
||||
|
||||
@SerializedName("fontFamily")
|
||||
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
|
||||
|
||||
@@ -137,7 +130,7 @@ public final class Config implements Cloneable, Observable {
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(100);
|
||||
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
|
||||
|
||||
@SerializedName("updateChannel")
|
||||
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
|
||||
@@ -145,27 +138,26 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
/**
|
||||
* The version of UI that the user have last used.
|
||||
* If there is a major change in UI, {@link Config#CURRENT_UI_VERSION} should be increased.
|
||||
* When {@link #CURRENT_UI_VERSION} is higher than the property, the user guide should be shown,
|
||||
* then this property is set to the same value as {@link #CURRENT_UI_VERSION}.
|
||||
* In particular, the property is default to 0, so that whoever open the application for the first time will see the guide.
|
||||
*/
|
||||
@SerializedName("uiVersion")
|
||||
private IntegerProperty uiVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
/**
|
||||
* The preferred login type to use when the user wants to add an account.
|
||||
*/
|
||||
@SerializedName("preferredLoginType")
|
||||
private StringProperty preferredLoginType = new SimpleStringProperty();
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public Config() {
|
||||
addListenerToProperties();
|
||||
}
|
||||
|
||||
private void addListenerToProperties() {
|
||||
Stream.of(getClass().getDeclaredFields())
|
||||
.filter(it -> {
|
||||
int modifiers = it.getModifiers();
|
||||
return !Modifier.isTransient(modifiers) && !Modifier.isStatic(modifiers);
|
||||
})
|
||||
.filter(it -> Observable.class.isAssignableFrom(it.getType()))
|
||||
.map(it -> {
|
||||
try {
|
||||
return (Observable) it.get(this);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Failed to get my own properties");
|
||||
}
|
||||
})
|
||||
.forEach(helper::receiveUpdatesFrom);
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -376,18 +368,6 @@ public final class Config implements Cloneable, Observable {
|
||||
return accountStorages;
|
||||
}
|
||||
|
||||
public String getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
|
||||
public void setSelectedAccount(String selectedAccount) {
|
||||
this.selectedAccount.set(selectedAccount);
|
||||
}
|
||||
|
||||
public StringProperty selectedAccountProperty() {
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
public String getFontFamily() {
|
||||
return fontFamily.get();
|
||||
}
|
||||
@@ -440,4 +420,39 @@ public final class Config implements Cloneable, Observable {
|
||||
this.updateChannel.set(updateChannel);
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
return configVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty configVersionProperty() {
|
||||
return configVersion;
|
||||
}
|
||||
|
||||
public void setConfigVersion(int configVersion) {
|
||||
this.configVersion.set(configVersion);
|
||||
}
|
||||
|
||||
public int getUiVersion() {
|
||||
return uiVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty uiVersionProperty() {
|
||||
return uiVersion;
|
||||
}
|
||||
|
||||
public void setUiVersion(int uiVersion) {
|
||||
this.uiVersion.set(uiVersion);
|
||||
}
|
||||
|
||||
public String getPreferredLoginType() {
|
||||
return preferredLoginType.get();
|
||||
}
|
||||
|
||||
public void setPreferredLoginType(String preferredLoginType) {
|
||||
this.preferredLoginType.set(preferredLoginType);
|
||||
}
|
||||
|
||||
public StringProperty preferredLoginTypeProperty() {
|
||||
return preferredLoginType;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -27,10 +31,6 @@ import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
@@ -52,13 +52,6 @@ public final class ConfigHolder {
|
||||
return configInstance;
|
||||
}
|
||||
|
||||
public static boolean isNewlyCreated() {
|
||||
if (configInstance == null) {
|
||||
throw new IllegalStateException("Configuration hasn't been loaded");
|
||||
}
|
||||
return newlyCreated;
|
||||
}
|
||||
|
||||
public synchronized static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
@@ -106,7 +99,7 @@ public final class ConfigHolder {
|
||||
private static Config loadConfig() throws IOException {
|
||||
if (Files.exists(configLocation)) {
|
||||
try {
|
||||
String content = new String(Files.readAllBytes(configLocation), UTF_8);
|
||||
String content = FileUtils.readText(configLocation);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
|
||||
@@ -17,18 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
final class ConfigUpgrader {
|
||||
private static final int VERSION = 0;
|
||||
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
@@ -37,10 +34,22 @@ final class ConfigUpgrader {
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
* @return true if config version is upgraded
|
||||
*/
|
||||
static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
boolean upgraded;
|
||||
if (deserialized.getConfigVersion() < VERSION) {
|
||||
deserialized.setConfigVersion(VERSION);
|
||||
// TODO: Add upgrade code here.
|
||||
upgraded = true;
|
||||
} else {
|
||||
upgraded = false;
|
||||
}
|
||||
|
||||
upgradeV2(deserialized, rawJson);
|
||||
upgradeV3(deserialized, rawJson);
|
||||
|
||||
return upgraded;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,20 +62,21 @@ final class ConfigUpgrader {
|
||||
// Convert OfflineAccounts whose stored uuid is important.
|
||||
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
|
||||
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
|
||||
List<OfflineAccount> accounts = new LinkedList<>();
|
||||
String selected = rawJson.containsKey("selectedAccount") ? null
|
||||
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
|
||||
|
||||
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
|
||||
((Map<?, ?>) uuidMap).forEach((key, value) -> {
|
||||
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
|
||||
accounts.add(account);
|
||||
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("type", "offline");
|
||||
storage.put("username", key);
|
||||
storage.put("uuid", value);
|
||||
if (key.equals(selected)) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
deserialized.getAccountStorages().add(storage);
|
||||
});
|
||||
});
|
||||
|
||||
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
|
||||
if (!rawJson.containsKey("selectedAccount"))
|
||||
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
|
||||
deserialized.setSelectedAccount(Accounts.accountId(account)));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -98,5 +108,22 @@ final class ConfigUpgrader {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tryCast(rawJson.get("selectedAccount"), String.class)
|
||||
.ifPresent(selected -> {
|
||||
deserialized.getAccountStorages().stream()
|
||||
.filter(storage -> {
|
||||
Object type = storage.get("type");
|
||||
if ("offline".equals(type)) {
|
||||
return selected.equals(storage.get("username") + ":" + storage.get("username"));
|
||||
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
|
||||
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(storage -> storage.put("selected", true));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -46,6 +48,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(Profile.Serializer.class)
|
||||
public final class Profile implements Observable {
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private final HMCLGameRepository repository;
|
||||
@@ -146,15 +149,17 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
private void checkSelectedVersion() {
|
||||
if (!repository.isLoaded()) return;
|
||||
String newValue = selectedVersion.get();
|
||||
if (!repository.hasVersion(newValue)) {
|
||||
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
|
||||
if (version.isPresent())
|
||||
selectedVersion.setValue(version.get());
|
||||
else if (newValue != null)
|
||||
selectedVersion.setValue(null);
|
||||
}
|
||||
JFXUtilities.runInFX(() -> {
|
||||
if (!repository.isLoaded()) return;
|
||||
String newValue = selectedVersion.get();
|
||||
if (!repository.hasVersion(newValue)) {
|
||||
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
|
||||
if (version.isPresent())
|
||||
selectedVersion.setValue(version.get());
|
||||
else if (newValue != null)
|
||||
selectedVersion.setValue(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
@@ -214,11 +219,6 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null)
|
||||
|
||||
@@ -22,7 +22,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
|
||||
@@ -104,7 +104,7 @@ public final class Profiles {
|
||||
private static void checkProfiles() {
|
||||
if (profiles.isEmpty()) {
|
||||
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
|
||||
Platform.runLater(() -> profiles.addAll(current, home));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
|
||||
@@ -64,7 +64,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
public static String getDefaultCommonDirectory() {
|
||||
return Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
|
||||
return Metadata.MINECRAFT_DIRECTORY.toString();
|
||||
}
|
||||
|
||||
public String getCommonDirectory() {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -34,6 +35,7 @@ import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
@JsonAdapter(Theme.TypeAdapter.class)
|
||||
public class Theme {
|
||||
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
|
||||
|
||||
@@ -43,7 +45,7 @@ public class Theme {
|
||||
Color.web("#43A047"), // green
|
||||
Color.web("#E67E22"), // orange
|
||||
Color.web("#9C27B0"), // purple
|
||||
Color.web("#F44336") // red
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
private final String color;
|
||||
@@ -82,14 +84,14 @@ public class Theme {
|
||||
.replace("%base-color%", color)
|
||||
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
css = Theme.class.getResource("/assets/css/blue.css").toExternalForm();
|
||||
css = "/assets/css/blue.css";
|
||||
}
|
||||
|
||||
return new String[]{
|
||||
css,
|
||||
Theme.class.getResource("/assets/css/root.css").toExternalForm()
|
||||
"/assets/css/root.css"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
@@ -43,6 +45,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(VersionSetting.Serializer.class)
|
||||
public final class VersionSetting {
|
||||
|
||||
public transient String id;
|
||||
@@ -277,6 +280,23 @@ public final class VersionSetting {
|
||||
noJVMArgsProperty.set(noJVMArgs);
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty notCheckJVMProperty = new ImmediateBooleanProperty(this, "notCheckJVM", false);
|
||||
|
||||
public ImmediateBooleanProperty notCheckJVMProperty() {
|
||||
return notCheckJVMProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if HMCL does not check JVM validity.
|
||||
*/
|
||||
public boolean isNotCheckJVM() {
|
||||
return notCheckJVMProperty.get();
|
||||
}
|
||||
|
||||
public void setNotCheckJVM(boolean notCheckJVM) {
|
||||
notCheckJVMProperty.set(notCheckJVM);
|
||||
}
|
||||
|
||||
private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false);
|
||||
|
||||
public ImmediateBooleanProperty notCheckGameProperty() {
|
||||
@@ -494,6 +514,7 @@ public final class VersionSetting {
|
||||
minecraftArgsProperty.addListener(listener);
|
||||
noJVMArgsProperty.addListener(listener);
|
||||
notCheckGameProperty.addListener(listener);
|
||||
notCheckJVMProperty.addListener(listener);
|
||||
showLogsProperty.addListener(listener);
|
||||
serverIpProperty.addListener(listener);
|
||||
fullscreenProperty.addListener(listener);
|
||||
@@ -532,11 +553,6 @@ public final class VersionSetting {
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) return JsonNull.INSTANCE;
|
||||
@@ -558,6 +574,7 @@ public final class VersionSetting {
|
||||
obj.addProperty("fullscreen", src.isFullscreen());
|
||||
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
|
||||
obj.addProperty("notCheckGame", src.isNotCheckGame());
|
||||
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
|
||||
obj.addProperty("showLogs", src.isShowLogs());
|
||||
obj.addProperty("gameDir", src.getGameDir());
|
||||
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
|
||||
@@ -595,6 +612,7 @@ public final class VersionSetting {
|
||||
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
|
||||
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
|
||||
@@ -17,33 +17,47 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountList;
|
||||
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.profile.ProfileList;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.GameList;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
|
||||
@@ -84,9 +98,10 @@ public final class Controllers {
|
||||
// FXThread
|
||||
public static AccountList getAccountListPage() {
|
||||
if (accountListPage == null) {
|
||||
accountListPage = new AccountList();
|
||||
AccountList accountListPage = new AccountList();
|
||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
|
||||
Controllers.accountListPage = accountListPage;
|
||||
}
|
||||
return accountListPage;
|
||||
}
|
||||
@@ -94,9 +109,10 @@ public final class Controllers {
|
||||
// FXThread
|
||||
public static ProfileList getProfileListPage() {
|
||||
if (profileListPage == null) {
|
||||
profileListPage = new ProfileList();
|
||||
ProfileList profileListPage = new ProfileList();
|
||||
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
Controllers.profileListPage = profileListPage;
|
||||
}
|
||||
return profileListPage;
|
||||
}
|
||||
@@ -121,8 +137,62 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static MainPage getMainPage() {
|
||||
if (mainPage == null)
|
||||
mainPage = new MainPage();
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
mainPage.setOnDragOver(event -> {
|
||||
if (event.getGestureSource() != mainPage && event.getDragboard().hasFiles()) {
|
||||
if (event.getDragboard().getFiles().stream().anyMatch(it -> "zip".equals(FileUtils.getExtension(it))))
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
mainPage.setOnDragDropped(event -> {
|
||||
List<File> files = event.getDragboard().getFiles();
|
||||
if (files != null) {
|
||||
List<File> modpacks = files.stream()
|
||||
.filter(it -> "zip".equals(FileUtils.getExtension(it)))
|
||||
.collect(Collectors.toList());
|
||||
if (!modpacks.isEmpty()) {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
event.setDropCompleted(true);
|
||||
}
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
mainPage.setCurrentGame(version);
|
||||
} else {
|
||||
mainPage.setCurrentGame(i18n("version.empty"));
|
||||
}
|
||||
});
|
||||
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
|
||||
mainPage.latestVersionProperty().bind(
|
||||
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
|
||||
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
|
||||
|
||||
Profiles.registerVersionsListener(profile -> {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
List<Node> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
|
||||
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.map(version -> {
|
||||
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
|
||||
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
|
||||
return node;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
JFXUtilities.runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile())
|
||||
mainPage.getVersions().setAll(children);
|
||||
});
|
||||
});
|
||||
Controllers.mainPage = mainPage;
|
||||
}
|
||||
return mainPage;
|
||||
}
|
||||
|
||||
@@ -131,6 +201,8 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
@@ -46,14 +46,14 @@ public class ListPageSkin extends SkinBase<ListPage> {
|
||||
{
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
VBox accountList = new VBox();
|
||||
accountList.maxWidthProperty().bind(scrollPane.widthProperty());
|
||||
accountList.setSpacing(10);
|
||||
accountList.setStyle("-fx-padding: 10 10 10 10;");
|
||||
VBox list = new VBox();
|
||||
list.maxWidthProperty().bind(scrollPane.widthProperty());
|
||||
list.setSpacing(10);
|
||||
list.setPadding(new Insets(10));
|
||||
|
||||
Bindings.bindContent(accountList.getChildren(), skinnable.itemsProperty());
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(accountList);
|
||||
scrollPane.setContent(list);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,43 +17,37 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
@@ -62,109 +56,142 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private final PopupMenu menu = new PopupMenu();
|
||||
private final JFXPopup popup = new JFXPopup(menu);
|
||||
|
||||
@FXML
|
||||
private StackPane main;
|
||||
@FXML
|
||||
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
|
||||
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
|
||||
private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion");
|
||||
private final ObservableList<Node> versions = FXCollections.observableArrayList();
|
||||
|
||||
private StackPane updatePane;
|
||||
@FXML
|
||||
private JFXButton btnLaunch;
|
||||
@FXML
|
||||
private JFXButton btnMenu;
|
||||
@FXML
|
||||
private JFXButton closeUpdateButton;
|
||||
@FXML
|
||||
private Label lblCurrentGame;
|
||||
@FXML
|
||||
private Label lblIcon;
|
||||
@FXML
|
||||
private TwoLineListItem lblLatestVersion;
|
||||
@FXML
|
||||
private Rectangle separator;
|
||||
private JFXButton menuButton;
|
||||
|
||||
{
|
||||
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
|
||||
setPadding(new Insets(25));
|
||||
|
||||
btnLaunch.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
btnMenu.setClip(new Rectangle(211, -100, 100, 200));
|
||||
menu.setMaxHeight(365);
|
||||
menu.setMinWidth(545);
|
||||
updatePane = new StackPane();
|
||||
updatePane.setVisible(false);
|
||||
updatePane.getStyleClass().add("bubble");
|
||||
FXUtils.setLimitWidth(updatePane, 230);
|
||||
FXUtils.setLimitHeight(updatePane, 55);
|
||||
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
|
||||
updatePane.setOnMouseClicked(e -> onUpgrade());
|
||||
FXUtils.onChange(showUpdateProperty(), this::doAnimation);
|
||||
|
||||
updatePane.visibleProperty().bind(UpdateChecker.outdatedProperty());
|
||||
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
|
||||
closeUpdateButton.setOnMouseClicked(event -> {
|
||||
Duration duration = Duration.millis(320);
|
||||
Timeline nowAnimation = new Timeline();
|
||||
nowAnimation.getKeyFrames().addAll(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(updatePane.translateXProperty(), 0, Interpolator.EASE_IN)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(updatePane.translateXProperty(), 260, Interpolator.EASE_IN)),
|
||||
new KeyFrame(duration, e -> {
|
||||
updatePane.visibleProperty().unbind();
|
||||
updatePane.setVisible(false);
|
||||
}));
|
||||
nowAnimation.play();
|
||||
});
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
lblLatestVersion.titleProperty().bind(
|
||||
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
|
||||
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(12);
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
StackPane.setAlignment(hBox, Pos.CENTER_LEFT);
|
||||
StackPane.setMargin(hBox, new Insets(9, 12, 9, 16));
|
||||
{
|
||||
Label lblIcon = new Label();
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(12);
|
||||
btnMenu.setGraphic(graphic);
|
||||
TwoLineListItem prompt = new TwoLineListItem();
|
||||
prompt.setTitleFill(Color.WHITE);
|
||||
prompt.setSubtitleFill(Color.WHITE);
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
prompt.setPickOnBounds(false);
|
||||
prompt.setStyle("-jfx-title-font-weight: BOLD;");
|
||||
prompt.titleProperty().bind(latestVersionProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
lblCurrentGame.setText(version);
|
||||
} else {
|
||||
lblCurrentGame.setText(i18n("version.empty"));
|
||||
hBox.getChildren().setAll(lblIcon, prompt);
|
||||
}
|
||||
});
|
||||
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
JFXButton closeUpdateButton = new JFXButton();
|
||||
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
|
||||
StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT);
|
||||
closeUpdateButton.getStyleClass().add("toggle-icon-tiny");
|
||||
StackPane.setMargin(closeUpdateButton, new Insets(5));
|
||||
closeUpdateButton.setOnMouseClicked(e -> closeUpdateBubble());
|
||||
|
||||
updatePane.getChildren().setAll(hBox, closeUpdateButton);
|
||||
}
|
||||
|
||||
StackPane launchPane = new StackPane();
|
||||
launchPane.setMaxWidth(230);
|
||||
launchPane.setMaxHeight(55);
|
||||
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
|
||||
{
|
||||
JFXButton launchButton = new JFXButton();
|
||||
launchButton.setPrefWidth(230);
|
||||
launchButton.setPrefHeight(55);
|
||||
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
launchButton.getStyleClass().add("jfx-button-raised");
|
||||
launchButton.setOnMouseClicked(e -> launch());
|
||||
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
|
||||
{
|
||||
VBox graphic = new VBox();
|
||||
graphic.setAlignment(Pos.CENTER);
|
||||
graphic.setTranslateX(-7);
|
||||
graphic.setMaxWidth(200);
|
||||
Label launchLabel = new Label(i18n("version.launch"));
|
||||
launchLabel.setStyle("-fx-font-size: 16px;");
|
||||
Label currentLabel = new Label();
|
||||
currentLabel.setStyle("-fx-font-size: 12px;");
|
||||
currentLabel.textProperty().bind(currentGameProperty());
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
|
||||
launchButton.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.getStyleClass().add("darker-fill");
|
||||
separator.setWidth(1);
|
||||
separator.setHeight(57);
|
||||
separator.setTranslateX(95);
|
||||
separator.setMouseTransparent(true);
|
||||
|
||||
menuButton = new JFXButton();
|
||||
menuButton.setPrefHeight(55);
|
||||
menuButton.setPrefWidth(230);
|
||||
menuButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
menuButton.getStyleClass().add("jfx-button-raised");
|
||||
menuButton.setStyle("-fx-font-size: 15px;");
|
||||
menuButton.setOnMouseClicked(e -> onMenu());
|
||||
menuButton.setClip(new Rectangle(211, -100, 100, 200));
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(12);
|
||||
menuButton.setGraphic(graphic);
|
||||
|
||||
launchPane.getChildren().setAll(launchButton, separator, menuButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(updatePane, launchPane);
|
||||
|
||||
menu.setMaxHeight(365);
|
||||
menu.setMaxWidth(545);
|
||||
menu.setAlwaysShowingVBar(true);
|
||||
menu.setOnMouseClicked(e -> popup.hide());
|
||||
Bindings.bindContent(menu.getContent(), versions);
|
||||
}
|
||||
|
||||
private void loadVersions(Profile profile) {
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
List<Node> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator.comparing(Version::getReleaseTime).thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.map(version -> {
|
||||
StackPane pane = new StackPane();
|
||||
GameItem item = new GameItem(profile, version.getId());
|
||||
pane.getChildren().setAll(item);
|
||||
pane.getStyleClass().setAll("menu-container");
|
||||
item.setMouseTransparent(true);
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
container.setOnMouseClicked(e -> {
|
||||
profile.setSelectedVersion(version.getId());
|
||||
popup.hide();
|
||||
});
|
||||
return container;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
JFXUtilities.runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile())
|
||||
menu.getContent().setAll(children);
|
||||
});
|
||||
private void doAnimation(boolean show) {
|
||||
Duration duration = Duration.millis(320);
|
||||
Timeline nowAnimation = new Timeline();
|
||||
nowAnimation.getKeyFrames().addAll(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE)));
|
||||
if (show) nowAnimation.getKeyFrames().add(
|
||||
new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true)));
|
||||
else nowAnimation.getKeyFrames().add(
|
||||
new KeyFrame(duration, e -> updatePane.setVisible(false)));
|
||||
nowAnimation.play();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void launch() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onMenu() {
|
||||
popup.show(btnMenu, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -btnMenu.getHeight());
|
||||
popup.show(menuButton, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -menuButton.getHeight());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onUpgrade() {
|
||||
RemoteVersion target = UpdateChecker.getLatestVersion();
|
||||
if (target == null) {
|
||||
@@ -173,6 +200,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
|
||||
private void closeUpdateBubble() {
|
||||
showUpdate.unbind();
|
||||
showUpdate.set(false);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
@@ -185,4 +217,44 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getCurrentGame() {
|
||||
return currentGame.get();
|
||||
}
|
||||
|
||||
public StringProperty currentGameProperty() {
|
||||
return currentGame;
|
||||
}
|
||||
|
||||
public void setCurrentGame(String currentGame) {
|
||||
this.currentGame.set(currentGame);
|
||||
}
|
||||
|
||||
public boolean isShowUpdate() {
|
||||
return showUpdate.get();
|
||||
}
|
||||
|
||||
public BooleanProperty showUpdateProperty() {
|
||||
return showUpdate;
|
||||
}
|
||||
|
||||
public void setShowUpdate(boolean showUpdate) {
|
||||
this.showUpdate.set(showUpdate);
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
}
|
||||
|
||||
public StringProperty latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public void setLatestVersion(String latestVersion) {
|
||||
this.latestVersion.set(latestVersion);
|
||||
}
|
||||
|
||||
public ObservableList<Node> getVersions() {
|
||||
return versions;
|
||||
}
|
||||
}
|
||||
@@ -30,21 +30,33 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelectedPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
@@ -200,6 +212,32 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
LOG.info("Exporting logs to " + logFile);
|
||||
try {
|
||||
Files.write(logFile, Logging.getRawLogs());
|
||||
} catch (IOException e) {
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageBox.ERROR_MESSAGE));
|
||||
LOG.log(Level.WARNING, "Failed to export logs", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHelp() {
|
||||
FXUtils.openLink(Metadata.HELP_URL);
|
||||
|
||||
@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
public abstract class SettingsView extends StackPane {
|
||||
@@ -333,34 +332,49 @@ public abstract class SettingsView extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
fontPane.setSpacing(5);
|
||||
settingsPane.getContent().add(fontPane);
|
||||
ComponentSublist logPane = new ComponentSublist();
|
||||
logPane.setTitle(i18n("settings.launcher.log"));
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
fontPane.getChildren().add(borderPane);
|
||||
{
|
||||
Label left = new Label(i18n("settings.launcher.log_font"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(left);
|
||||
}
|
||||
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
|
||||
logButton.setOnMouseClicked(e -> onExportLogs());
|
||||
logButton.getStyleClass().setAll("jfx-button-border");
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
|
||||
cboFont = new FontComboBox(12, false);
|
||||
txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
logPane.setHeaderRight(logButton);
|
||||
}
|
||||
|
||||
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
|
||||
fontPane.getChildren().add(lblDisplay);
|
||||
{
|
||||
VBox fontPane = new VBox();
|
||||
fontPane.setSpacing(5);
|
||||
|
||||
{
|
||||
BorderPane borderPane = new BorderPane();
|
||||
fontPane.getChildren().add(borderPane);
|
||||
{
|
||||
Label left = new Label(i18n("settings.launcher.log.font"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
borderPane.setLeft(left);
|
||||
}
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
|
||||
cboFont = new FontComboBox(12, false);
|
||||
txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
}
|
||||
|
||||
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
|
||||
fontPane.getChildren().add(lblDisplay);
|
||||
|
||||
logPane.getContent().add(fontPane);
|
||||
}
|
||||
settingsPane.getContent().add(logPane);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -477,4 +491,5 @@ public abstract class SettingsView extends StackPane {
|
||||
|
||||
protected abstract void onUpdate();
|
||||
protected abstract void onHelp();
|
||||
protected abstract void onExportLogs();
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class AccountListItem extends RadioButton {
|
||||
getStyleClass().clear();
|
||||
setUserData(account);
|
||||
|
||||
StringBuilder subtitleString = new StringBuilder(Accounts.getAccountTypeName(account));
|
||||
StringBuilder subtitleString = new StringBuilder(Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)));
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
||||
subtitleString.append(", ").append(i18n("account.injector.server")).append(": ").append(server.getName());
|
||||
|
||||
@@ -80,8 +80,13 @@ public class AddAccountPane extends StackPane {
|
||||
selectDefaultServer();
|
||||
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.setConverter(stringConverter(Accounts::getAccountTypeName));
|
||||
cboType.getSelectionModel().select(0);
|
||||
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||
// try selecting the preferred login type
|
||||
cboType.getSelectionModel().select(
|
||||
cboType.getItems().stream()
|
||||
.filter(type -> Accounts.getLoginType(type).equals(config().getPreferredLoginType()))
|
||||
.findFirst()
|
||||
.orElse(Accounts.FACTORY_OFFLINE));
|
||||
|
||||
btnAddServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
btnManageServer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
@@ -91,6 +96,9 @@ public class AddAccountPane extends StackPane {
|
||||
|
||||
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
|
||||
|
||||
// remember the last used login type
|
||||
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
|
||||
|
||||
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
|
||||
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
|
||||
|
||||
@@ -290,6 +298,8 @@ public class AddAccountPane extends StackPane {
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
|
||||
@@ -116,7 +118,9 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
|
||||
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
|
||||
} else {
|
||||
lblCreationWarning.setText(resolveFetchExceptionMessage(variables.<Exception>get("lastException")));
|
||||
Exception exception = variables.get("lastException");
|
||||
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
|
||||
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
|
||||
}
|
||||
}).start();
|
||||
|
||||
|
||||
@@ -88,11 +88,16 @@ class ComponentListCell extends StackPane {
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
boolean overrideHeaderLeft = false;
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
|
||||
if (leftNode != null)
|
||||
if (leftNode != null) {
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
} else {
|
||||
overrideHeaderLeft = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!overrideHeaderLeft) {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(list.titleProperty());
|
||||
labelVBox.getChildren().add(label);
|
||||
@@ -108,6 +113,7 @@ class ComponentListCell extends StackPane {
|
||||
groupNode.setLeft(labelVBox);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setSpacing(16);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
@@ -30,6 +28,7 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
@@ -39,27 +38,40 @@ public class Navigator extends StackPane {
|
||||
|
||||
private final Stack<Node> stack = new Stack<>();
|
||||
private final TransitionHandler animationHandler = new TransitionHandler(this);
|
||||
private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper();
|
||||
private boolean initialized = false;
|
||||
|
||||
public Navigator(Node init) {
|
||||
public void init(Node init) {
|
||||
stack.push(init);
|
||||
getChildren().setAll(init);
|
||||
|
||||
Platform.runLater(() ->
|
||||
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED)));
|
||||
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public void navigate(Node node) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
if (!initialized)
|
||||
throw new IllegalStateException("Navigator must have a root page");
|
||||
|
||||
Node from = stack.peek();
|
||||
if (from == node)
|
||||
return;
|
||||
|
||||
Logging.LOG.info("Navigate to " + node);
|
||||
|
||||
stack.push(node);
|
||||
fireEvent(new NavigationEvent(this, from, NavigationEvent.NAVIGATING));
|
||||
|
||||
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
|
||||
fireEvent(navigating);
|
||||
node.fireEvent(navigating);
|
||||
|
||||
setContent(node);
|
||||
fireEvent(new NavigationEvent(this, node, NavigationEvent.NAVIGATED));
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
|
||||
fireEvent(navigated);
|
||||
node.fireEvent(navigated);
|
||||
|
||||
EventHandler<PageCloseEvent> handler = event -> close(node);
|
||||
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
|
||||
@@ -79,14 +91,22 @@ public class Navigator extends StackPane {
|
||||
public void close(Node from) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
if (!initialized)
|
||||
throw new IllegalStateException("Navigator must have a root page");
|
||||
|
||||
Logging.LOG.info("Closed page " + from);
|
||||
|
||||
if (stack.peek() != from)
|
||||
throw new IllegalStateException();
|
||||
stack.pop();
|
||||
Node node = stack.peek();
|
||||
|
||||
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
|
||||
fireEvent(navigating);
|
||||
node.fireEvent(navigating);
|
||||
|
||||
setContent(node);
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
|
||||
fireEvent(navigated);
|
||||
node.fireEvent(navigated);
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.When;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
@@ -25,12 +28,14 @@ import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class PopupMenu extends Control {
|
||||
|
||||
private final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
private final BooleanProperty alwaysShowingVBar = new SimpleBooleanProperty();
|
||||
|
||||
public PopupMenu() {
|
||||
}
|
||||
@@ -39,11 +44,31 @@ public class PopupMenu extends Control {
|
||||
return content;
|
||||
}
|
||||
|
||||
public boolean isAlwaysShowingVBar() {
|
||||
return alwaysShowingVBar.get();
|
||||
}
|
||||
|
||||
public BooleanProperty alwaysShowingVBarProperty() {
|
||||
return alwaysShowingVBar;
|
||||
}
|
||||
|
||||
public void setAlwaysShowingVBar(boolean alwaysShowingVBar) {
|
||||
this.alwaysShowingVBar.set(alwaysShowingVBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new PopupMenuSkin();
|
||||
}
|
||||
|
||||
public static Node wrapPopupMenuItem(Node node) {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getChildren().setAll(node);
|
||||
pane.getStyleClass().setAll("menu-container");
|
||||
node.setMouseTransparent(true);
|
||||
return new RipplerContainer(pane);
|
||||
}
|
||||
|
||||
private class PopupMenuSkin extends SkinBase<PopupMenu> {
|
||||
|
||||
protected PopupMenuSkin() {
|
||||
@@ -52,6 +77,9 @@ public class PopupMenu extends Control {
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.vbarPolicyProperty().bind(new When(alwaysShowingVBar)
|
||||
.then(ScrollPane.ScrollBarPolicy.ALWAYS)
|
||||
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getStyleClass().add("menu");
|
||||
|
||||
@@ -27,7 +27,12 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleablePropertyFactory;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Background;
|
||||
@@ -39,8 +44,6 @@ import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@DefaultProperty("container")
|
||||
@@ -210,32 +213,12 @@ public class RipplerContainer extends StackPane {
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
}
|
||||
|
||||
private static class StyleableProperties {
|
||||
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = new CssMetaData<RipplerContainer, Paint>("-jfx-rippler-fill", StyleConverter.getPaintConverter(), Color.rgb(0, 200, 255)) {
|
||||
@Override
|
||||
public boolean isSettable(RipplerContainer control) {
|
||||
return control.ripplerFill == null || !control.ripplerFill.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(RipplerContainer control) {
|
||||
return control.ripplerFillProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
private StyleableProperties() {
|
||||
}
|
||||
|
||||
static {
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Node.getClassCssMetaData());
|
||||
Collections.addAll(styleables, RIPPLER_FILL);
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,11 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.css.*;
|
||||
import javafx.scene.Node;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleablePropertyFactory;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -28,8 +31,6 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class TwoLineListItem extends StackPane {
|
||||
@@ -152,69 +153,15 @@ public class TwoLineListItem extends StackPane {
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.STYLEABLES;
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
}
|
||||
|
||||
private static class StyleableProperties {
|
||||
private static final StyleablePropertyFactory<TwoLineListItem> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
|
||||
private static final FontCssMetaData<TwoLineListItem> TITLE_FONT = new FontCssMetaData<TwoLineListItem>("-jfx-title-font", Font.font(15)) {
|
||||
@Override
|
||||
public boolean isSettable(TwoLineListItem control) {
|
||||
return control.title == null || !control.title.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Font> getStyleableProperty(TwoLineListItem control) {
|
||||
return control.titleFontProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final FontCssMetaData<TwoLineListItem> SUBTITLE_FONT = new FontCssMetaData<TwoLineListItem>("-jfx-subtitle-font", Font.getDefault()) {
|
||||
@Override
|
||||
public boolean isSettable(TwoLineListItem control) {
|
||||
return control.subtitle == null || !control.subtitle.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Font> getStyleableProperty(TwoLineListItem control) {
|
||||
return control.subtitleFontProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<TwoLineListItem, Paint> TITLE_FILL = new CssMetaData<TwoLineListItem, Paint>("-jfx-title-fill", StyleConverter.getPaintConverter(), Color.BLACK) {
|
||||
@Override
|
||||
public boolean isSettable(TwoLineListItem control) {
|
||||
return control.titleFill == null || !control.titleFill.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(TwoLineListItem control) {
|
||||
return control.titleFillProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<TwoLineListItem, Paint> SUBTITLE_FILL = new CssMetaData<TwoLineListItem, Paint>("-jfx-subtitle-fill", StyleConverter.getPaintConverter(), Color.GRAY) {
|
||||
@Override
|
||||
public boolean isSettable(TwoLineListItem control) {
|
||||
return control.subtitleFill == null || !control.subtitleFill.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<Paint> getStyleableProperty(TwoLineListItem control) {
|
||||
return control.subtitleFillProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
|
||||
|
||||
private StyleableProperties() {
|
||||
}
|
||||
|
||||
static {
|
||||
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Node.getClassCssMetaData());
|
||||
Collections.addAll(styleables, TITLE_FONT);
|
||||
Collections.addAll(styleables, SUBTITLE_FONT);
|
||||
STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
private static final CssMetaData<TwoLineListItem, Font> TITLE_FONT = FACTORY.createFontCssMetaData("-jfx-title-font", s -> s.titleFont, Font.font(15));
|
||||
private static final CssMetaData<TwoLineListItem, Font> SUBTITLE_FONT = FACTORY.createFontCssMetaData("-jfx-subtitle-font", s -> s.subtitleFont);
|
||||
private static final CssMetaData<TwoLineListItem, Paint> TITLE_FILL = FACTORY.createPaintCssMetaData("-jfx-title-fill", s -> s.titleFill);
|
||||
private static final CssMetaData<TwoLineListItem, Paint> SUBTITLE_FILL = FACTORY.createPaintCssMetaData("-jfx-subtitle-fill", s -> s.subtitleFill, Color.GREY);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -30,7 +30,7 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
public class DecoratorControl extends Control {
|
||||
public class Decorator extends Control {
|
||||
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ListProperty<Node> content = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
private final ListProperty<Node> container = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
@@ -49,7 +49,7 @@ public class DecoratorControl extends Control {
|
||||
private final Stage primaryStage;
|
||||
private StackPane drawerWrapper;
|
||||
|
||||
public DecoratorControl(Stage primaryStage) {
|
||||
public Decorator(Stage primaryStage) {
|
||||
this.primaryStage = primaryStage;
|
||||
|
||||
primaryStage.initStyle(StageStyle.UNDECORATED);
|
||||
@@ -40,7 +40,7 @@ import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.Config;
|
||||
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -69,7 +69,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
public class DecoratorController {
|
||||
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
|
||||
|
||||
private final DecoratorControl decorator;
|
||||
private final Decorator decorator;
|
||||
private final ImageView welcomeView;
|
||||
private final Navigator navigator;
|
||||
private final Node mainPage;
|
||||
@@ -80,13 +80,14 @@ public class DecoratorController {
|
||||
public DecoratorController(Stage stage, Node mainPage) {
|
||||
this.mainPage = mainPage;
|
||||
|
||||
decorator = new DecoratorControl(stage);
|
||||
decorator = new Decorator(stage);
|
||||
decorator.titleProperty().set(Metadata.TITLE);
|
||||
decorator.setOnCloseButtonAction(Launcher::stopApplication);
|
||||
|
||||
navigator = new Navigator(mainPage);
|
||||
navigator = new Navigator();
|
||||
navigator.setOnNavigating(this::onNavigating);
|
||||
navigator.setOnNavigated(this::onNavigated);
|
||||
navigator.init(mainPage);
|
||||
|
||||
decorator.getContent().setAll(navigator);
|
||||
decorator.onCloseNavButtonActionProperty().set(e -> close());
|
||||
@@ -106,18 +107,34 @@ public class DecoratorController {
|
||||
);
|
||||
nowAnimation.play();
|
||||
});
|
||||
if (ConfigHolder.isNewlyCreated() && config().getLocalization().getLocale() == Locale.CHINA)
|
||||
decorator.getContainer().setAll(welcomeView);
|
||||
|
||||
if (switchedToNewUI()) {
|
||||
if (config().getLocalization().getLocale() == Locale.CHINA) {
|
||||
// currently, user guide is only available in Chinese
|
||||
decorator.getContainer().setAll(welcomeView);
|
||||
}
|
||||
}
|
||||
|
||||
setupBackground();
|
||||
|
||||
setupAuthlibInjectorDnD();
|
||||
}
|
||||
|
||||
public DecoratorControl getDecorator() {
|
||||
public Decorator getDecorator() {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the user is seeing the current version of UI for the first time.
|
||||
*/
|
||||
private boolean switchedToNewUI() {
|
||||
if (config().getUiVersion() < Config.CURRENT_UI_VERSION) {
|
||||
config().setUiVersion(Config.CURRENT_UI_VERSION);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==== Background ====
|
||||
|
||||
private void setupBackground() {
|
||||
|
||||
@@ -42,7 +42,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
|
||||
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
|
||||
|
||||
@@ -62,14 +62,14 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
public DecoratorSkin(DecoratorControl control) {
|
||||
public DecoratorSkin(Decorator control) {
|
||||
super(control);
|
||||
|
||||
primaryStage = control.getPrimaryStage();
|
||||
|
||||
minus.fillProperty().bind(Theme.foregroundFillBinding());
|
||||
|
||||
DecoratorControl skinnable = getSkinnable();
|
||||
Decorator skinnable = getSkinnable();
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.getStyleClass().setAll("jfx-decorator", "resize-border");
|
||||
@@ -146,7 +146,7 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
titleContainer.setOnMouseMoved(this::onMouseMoved);
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.setMinHeight(40);
|
||||
titleContainer.getStyleClass().setAll("jfx-tool-bar");
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar", "window-title-bar");
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
|
||||
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
|
||||
if (!isDragging) allowMove = false;
|
||||
@@ -169,9 +169,9 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
|
||||
titleWrapper.setCenter(lblTitle);
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.getStyleClass().add("separator");
|
||||
separator.heightProperty().bind(titleWrapper.heightProperty());
|
||||
separator.setWidth(1);
|
||||
separator.setFill(Color.GRAY);
|
||||
titleWrapper.setRight(separator);
|
||||
}
|
||||
titleContainer.setLeft(titleWrapper);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
private final Profile profile;
|
||||
private final File file;
|
||||
|
||||
public ModpackInstallWizardProvider(Profile profile) {
|
||||
this(profile, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(Profile profile, File modpackFile) {
|
||||
this.profile = profile;
|
||||
this.file = modpackFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
if (file != null)
|
||||
settings.put(ModpackPage.MODPACK_FILE, file);
|
||||
settings.put(PROFILE, profile);
|
||||
}
|
||||
|
||||
private Task finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
|
||||
return null;
|
||||
|
||||
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
|
||||
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
|
||||
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
|
||||
if (selected == null || modpack == null || name == null) return null;
|
||||
|
||||
return ModpackHelper.getInstallTask(profile, selected, name, modpack)
|
||||
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", new FailureCallback() {
|
||||
@Override
|
||||
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
|
||||
if (exception instanceof CurseCompletionException) {
|
||||
if (exception.getCause() instanceof FileNotFoundException) {
|
||||
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
|
||||
}
|
||||
} else if (exception instanceof DownloadException) {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return finishModpackInstallingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new ModpackPage(controller);
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final String PROFILE = "PROFILE";
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WebStage;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
@@ -39,7 +40,9 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackPage extends StackPane implements WizardPage {
|
||||
@@ -75,31 +78,41 @@ public final class ModpackPage extends StackPane implements WizardPage {
|
||||
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) Platform.runLater(controller::onEnd);
|
||||
else {
|
||||
File selectedFile;
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
selectedFile = filePath.get();
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) {
|
||||
Platform.runLater(controller::onEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.getSettings().put(MODPACK_FILE, selectedFile);
|
||||
}
|
||||
|
||||
try {
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile);
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
txtModpackName.getValidators().addAll(
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
|
||||
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
|
||||
);
|
||||
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
|
||||
|
||||
try {
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile);
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
} catch (UnsupportedModpackException e) {
|
||||
txtModpackName.setText(i18n("modpack.task.install.error"));
|
||||
btnInstall.setDisable(true);
|
||||
}
|
||||
} catch (UnsupportedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
|
||||
Platform.runLater(controller::onEnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-38
@@ -21,32 +21,29 @@ import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class DownloadWizardProvider implements WizardProvider {
|
||||
private Profile profile;
|
||||
private final int type;
|
||||
public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
private final Profile profile;
|
||||
|
||||
public DownloadWizardProvider(int type) {
|
||||
this.type = type;
|
||||
public VanillaInstallWizardProvider(Profile profile) {
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
profile = Profiles.getSelectedProfile();
|
||||
settings.put(PROFILE, profile);
|
||||
}
|
||||
|
||||
@@ -70,29 +67,21 @@ public final class DownloadWizardProvider implements WizardProvider {
|
||||
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
|
||||
}
|
||||
|
||||
private Task finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
|
||||
return null;
|
||||
|
||||
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
|
||||
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
|
||||
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
|
||||
if (selected == null || modpack == null || name == null) return null;
|
||||
|
||||
return ModpackHelper.getInstallTask(profile, selected, name, modpack)
|
||||
.then(Task.of(Schedulers.javafx(), () -> profile.setSelectedVersion(name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_message", i18n("install.failed"));
|
||||
settings.put("failure_callback", new FailureCallback() {
|
||||
@Override
|
||||
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
|
||||
if (exception instanceof DownloadException) {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
switch (type) {
|
||||
case 0: return finishVersionDownloadingAsync(settings);
|
||||
case 1: return finishModpackInstallingAsync(settings);
|
||||
default: return null;
|
||||
}
|
||||
return finishVersionDownloadingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,14 +89,7 @@ public final class DownloadWizardProvider implements WizardProvider {
|
||||
DownloadProvider provider = profile.getDependency().getDownloadProvider();
|
||||
switch (step) {
|
||||
case 0:
|
||||
switch (type) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", provider, "game", () -> controller.onNext(new InstallersPage(controller, profile.getRepository(), provider)));
|
||||
case 1:
|
||||
return new ModpackPage(controller);
|
||||
default:
|
||||
throw new IllegalStateException("Error step " + step + ", subStep " + type + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", provider, "game", () -> controller.onNext(new InstallersPage(controller, profile.getRepository(), provider)));
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
@@ -81,11 +81,14 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
dependency = dependency.then(Task.of(() -> {
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Config exported = new Config();
|
||||
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
exported.setBackgroundImage(config().getBackgroundImage());
|
||||
exported.setTheme(config().getTheme());
|
||||
exported.setDownloadType(config().getDownloadType());
|
||||
exported.setPreferredLoginType(config().getPreferredLoginType());
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
|
||||
public GameAdvancedListItem() {
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
|
||||
if (version != null) {
|
||||
if (version != null && Profiles.getSelectedProfile() != null &&
|
||||
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
|
||||
setTitle(version);
|
||||
setSubtitle(null);
|
||||
setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
|
||||
|
||||
@@ -33,11 +33,13 @@ import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -66,7 +68,8 @@ public class GameList extends Control implements DecoratorPage {
|
||||
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
|
||||
List<GameListItem> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator.comparing(Version::getReleaseTime).thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
|
||||
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.map(version -> new GameListItem(toggleGroup, profile, version.getId()))
|
||||
.collect(Collectors.toList());
|
||||
JFXUtilities.runInFX(() -> {
|
||||
@@ -98,12 +101,18 @@ public class GameList extends Control implements DecoratorPage {
|
||||
return new GameListSkin(this);
|
||||
}
|
||||
|
||||
public void addNewGame() {
|
||||
Controllers.getDecorator().startWizard(new DownloadWizardProvider(0), i18n("install.new_game"));
|
||||
void addNewGame() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
if (profile.getRepository().isLoaded()) {
|
||||
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game"));
|
||||
}
|
||||
}
|
||||
|
||||
public void importModpack() {
|
||||
Controllers.getDecorator().startWizard(new DownloadWizardProvider(1), i18n("install.modpack"));
|
||||
void importModpack() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
if (profile.getRepository().isLoaded()) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack"));
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
|
||||
@@ -28,7 +28,9 @@ import javafx.scene.control.Tab;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
@@ -62,6 +64,8 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
@FXML
|
||||
private JFXButton btnExport;
|
||||
@FXML
|
||||
private JFXButton btnTestGame;
|
||||
@FXML
|
||||
private StackPane rootPane;
|
||||
@FXML
|
||||
private StackPane contentPane;
|
||||
@@ -85,7 +89,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
new IconedMenuItem(null, i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> onBrowse("config"), browsePopup)),
|
||||
new IconedMenuItem(null, i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> onBrowse("resourcepacks"), browsePopup)),
|
||||
new IconedMenuItem(null, i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> onBrowse("screenshots"), browsePopup)),
|
||||
new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> onBrowse("resourcepacks"), browsePopup))
|
||||
new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> onBrowse("saves"), browsePopup))
|
||||
);
|
||||
|
||||
PopupMenu managementList = new PopupMenu();
|
||||
@@ -94,7 +98,8 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
new IconedMenuItem(null, i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> Versions.renameVersion(profile, version), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> Versions.deleteVersion(profile, version), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), FXUtils.withJFXPopupClosing(() -> new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getResolvedVersion(version)).start(), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.remove_libraries"), FXUtils.withJFXPopupClosing(() -> FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries")), managementPopup))
|
||||
new IconedMenuItem(null, i18n("version.manage.remove_libraries"), FXUtils.withJFXPopupClosing(() -> FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries")), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(() -> Versions.cleanVersion(profile, version), managementPopup))
|
||||
);
|
||||
|
||||
FXUtils.installTooltip(btnDelete, i18n("version.manage.remove"));
|
||||
@@ -102,6 +107,9 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||
FXUtils.installTooltip(btnExport, i18n("modpack.export"));
|
||||
|
||||
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
|
||||
FXUtils.installTooltip(btnTestGame, i18n("version.launch.test"));
|
||||
|
||||
setEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
|
||||
@@ -134,6 +142,11 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
load(this.version, this.profile);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onTestGame() {
|
||||
Versions.testGame(profile, version);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onBrowseMenu() {
|
||||
browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, btnBrowseMenu.getHeight());
|
||||
|
||||
@@ -90,6 +90,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
@FXML private Label lblPhysicalMemory;
|
||||
@FXML private JFXToggleButton chkNoJVMArgs;
|
||||
@FXML private JFXToggleButton chkNoGameCheck;
|
||||
@FXML private JFXToggleButton chkNoJVMCheck;
|
||||
@FXML private MultiFileItem<JavaVersion> javaItem;
|
||||
@FXML private MultiFileItem<EnumGameDirectory> gameDirItem;
|
||||
@FXML private JFXToggleButton chkShowLogs;
|
||||
@@ -181,6 +182,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
FXUtils.unbindString(txtServerIP, lastVersionSetting.serverIpProperty());
|
||||
FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty());
|
||||
FXUtils.unbindBoolean(chkNoGameCheck, lastVersionSetting.notCheckGameProperty());
|
||||
FXUtils.unbindBoolean(chkNoJVMCheck, lastVersionSetting.notCheckJVMProperty());
|
||||
FXUtils.unbindBoolean(chkNoJVMArgs, lastVersionSetting.noJVMArgsProperty());
|
||||
FXUtils.unbindBoolean(chkShowLogs, lastVersionSetting.showLogsProperty());
|
||||
FXUtils.unbindEnum(cboLauncherVisibility);
|
||||
@@ -208,6 +210,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
FXUtils.bindString(txtServerIP, versionSetting.serverIpProperty());
|
||||
FXUtils.bindBoolean(chkFullscreen, versionSetting.fullscreenProperty());
|
||||
FXUtils.bindBoolean(chkNoGameCheck, versionSetting.notCheckGameProperty());
|
||||
FXUtils.bindBoolean(chkNoJVMCheck, versionSetting.notCheckJVMProperty());
|
||||
FXUtils.bindBoolean(chkNoJVMArgs, versionSetting.noJVMArgsProperty());
|
||||
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
|
||||
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
|
||||
|
||||
@@ -35,12 +35,14 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -100,6 +102,14 @@ public class Versions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanVersion(Profile profile, String id) {
|
||||
try {
|
||||
profile.getRepository().clean(id);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to clean game directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void generateLaunchScript(Profile profile, String id) {
|
||||
if (checkForLaunching(profile, id)) {
|
||||
GameRepository repository = profile.getRepository();
|
||||
|
||||
+3
-1
@@ -74,7 +74,9 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
|
||||
if (executor.getLastException() == null)
|
||||
return;
|
||||
String appendix = StringUtils.getStackTrace(executor.getLastException());
|
||||
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
|
||||
if (settings.get("failure_callback") instanceof WizardProvider.FailureCallback)
|
||||
((WizardProvider.FailureCallback)settings.get("failure_callback")).onFail(settings, executor.getLastException(), () -> onEnd());
|
||||
else if (settings.get("failure_message") instanceof String)
|
||||
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> onEnd());
|
||||
else if (!settings.containsKey("forbid_failure_message"))
|
||||
Controllers.dialog(appendix, i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> onEnd());
|
||||
|
||||
@@ -26,4 +26,8 @@ public interface WizardProvider {
|
||||
Object finish(Map<String, Object> settings);
|
||||
Node createPage(WizardController controller, int step, Map<String, Object> settings);
|
||||
boolean cancel();
|
||||
|
||||
interface FailureCallback {
|
||||
void onFail(Map<String, Object> settings, Exception exception, Runnable next);
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ public final class UpdateChecker {
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, "Update Checker", true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -30,6 +29,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
@@ -46,7 +46,6 @@ import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -231,7 +230,7 @@ public final class UpdateHandler {
|
||||
private static boolean isFirstLaunchAfterUpgrade() {
|
||||
Optional<Path> currentPath = JarUtils.thisJar();
|
||||
if (currentPath.isPresent()) {
|
||||
Path updated = Launcher.HMCL_DIRECTORY.toPath().resolve("HMCL-" + Metadata.VERSION + ".jar");
|
||||
Path updated = Metadata.HMCL_DIRECTORY.resolve("HMCL-" + Metadata.VERSION + ".jar");
|
||||
if (currentPath.get().toAbsolutePath().equals(updated.toAbsolutePath())) {
|
||||
return true;
|
||||
}
|
||||
@@ -240,10 +239,10 @@ public final class UpdateHandler {
|
||||
}
|
||||
|
||||
private static void breakForceUpdateFeature() {
|
||||
Path hmclVersionJson = Launcher.HMCL_DIRECTORY.toPath().resolve("hmclver.json");
|
||||
Path hmclVersionJson = Metadata.HMCL_DIRECTORY.resolve("hmclver.json");
|
||||
if (Files.isRegularFile(hmclVersionJson)) {
|
||||
try {
|
||||
Map<?, ?> content = new Gson().fromJson(new String(Files.readAllBytes(hmclVersionJson), UTF_8), Map.class);
|
||||
Map<?, ?> content = new Gson().fromJson(FileUtils.readText(hmclVersionJson), Map.class);
|
||||
Object ver = content.get("ver");
|
||||
if (ver instanceof String && ((String) ver).startsWith("3.")) {
|
||||
Files.delete(hmclVersionJson);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
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;
|
||||
@@ -83,6 +84,7 @@ public final class Locales {
|
||||
else throw new IllegalArgumentException("Unknown locale: " + locale);
|
||||
}
|
||||
|
||||
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
||||
public static class SupportedLocale {
|
||||
private final Locale locale;
|
||||
private final String name;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.root {
|
||||
-fx-base-color: #5c6bc0;
|
||||
-fx-base-darker-color: derive(-fx-base-color, -10%);
|
||||
-fx-base-check-color: derive(-fx-base-color, 30%);
|
||||
-fx-base-text-fill: white;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
.root {
|
||||
-fx-base-color: %base-color%;
|
||||
-fx-base-darker-color: derive(-fx-base-color, -10%);
|
||||
-fx-base-check-color: derive(-fx-base-color, 30%);
|
||||
-fx-base-text-fill: %font-color%;
|
||||
}
|
||||
@@ -34,8 +34,6 @@
|
||||
|
||||
.rippler-container {
|
||||
-jfx-rippler-fill: -fx-base-check-color;
|
||||
-fx-border-color: red;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.rippler-container:selected .label {
|
||||
@@ -104,6 +102,14 @@
|
||||
-jfx-subtitle-fill: gray;
|
||||
}
|
||||
|
||||
.window-title-bar .separator {
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
.darker-fill {
|
||||
-fx-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Tab Pane *
|
||||
@@ -538,7 +544,7 @@
|
||||
.jfx-button-raised {
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-font-size:14px;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-raised .jfx-rippler {
|
||||
@@ -550,6 +556,22 @@
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-border {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-border-color: gray;
|
||||
-fx-border-radius: 5px;
|
||||
-fx-border-width: 0.2px;
|
||||
-fx-padding: 8px;
|
||||
}
|
||||
|
||||
.jfx-button-border .jfx-rippler {
|
||||
-jfx-rippler-fill: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.jfx-button-border .label {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
.jfx-button-raised-round {
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-background-radius: 50px;
|
||||
@@ -1218,7 +1240,7 @@
|
||||
}
|
||||
|
||||
.tree-table-row-cell .jfx-text-field {
|
||||
-fx-focus-color: rgba(82, 100, 174);
|
||||
-fx-focus-color: rgb(82, 100, 174);
|
||||
}
|
||||
|
||||
.tree-table-row-cell .jfx-text-field:error {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<Label fx:id="lblInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
|
||||
GridPane.columnIndex="0" GridPane.rowIndex="1"/>
|
||||
|
||||
<JFXComboBox fx:id="cboServers" promptText="%general.prompt.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
<JFXComboBox fx:id="cboServers" promptText="%account.injector.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
|
||||
<HBox GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
|
||||
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
||||
@@ -67,10 +67,12 @@
|
||||
</body>
|
||||
<actions>
|
||||
<Label fx:id="lblCreationWarning"/>
|
||||
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
|
||||
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
|
||||
</SpinnerPane>
|
||||
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
|
||||
<HBox>
|
||||
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
|
||||
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
|
||||
</SpinnerPane>
|
||||
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
|
||||
</HBox>
|
||||
</actions>
|
||||
</JFXDialogLayout>
|
||||
</fx:root>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.jfoenix.controls.JFXButton?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
styleClass="transparent"
|
||||
style="-fx-padding: 10 16 10 16;"
|
||||
type="StackPane" pickOnBounds="false">
|
||||
<BorderPane pickOnBounds="false">
|
||||
<left>
|
||||
<HBox alignment="CENTER" mouseTransparent="true">
|
||||
<StackPane fx:id="imageViewContainer" FXUtils.limitWidth="32" FXUtils.limitHeight="32">
|
||||
<ImageView preserveRatio="true" fx:id="imageView" smooth="false">
|
||||
<Image url="/assets/img/icon.png" />
|
||||
</ImageView>
|
||||
</StackPane>
|
||||
<BorderPane style="-fx-padding: 0 0 0 10;">
|
||||
<top>
|
||||
<Label fx:id="lblTitle" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
|
||||
</top>
|
||||
<bottom>
|
||||
<Label fx:id="lblSubtitle" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</HBox>
|
||||
</left>
|
||||
<right>
|
||||
<HBox alignment="CENTER" pickOnBounds="false">
|
||||
<JFXButton fx:id="btnSettings" maxWidth="40" minWidth="40" prefWidth="40" styleClass="toggle-icon4">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/gear.fxml" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
</HBox>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</fx:root>
|
||||
@@ -19,10 +19,12 @@
|
||||
</body>
|
||||
<actions>
|
||||
<Label fx:id="lblCreationWarning" />
|
||||
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
|
||||
<SpinnerPane fx:id="nextPane" styleClass="small-spinner-pane">
|
||||
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
|
||||
</SpinnerPane>
|
||||
<HBox>
|
||||
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
|
||||
<SpinnerPane fx:id="nextPane" styleClass="small-spinner-pane">
|
||||
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
|
||||
</SpinnerPane>
|
||||
</HBox>
|
||||
</actions>
|
||||
</JFXDialogLayout>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<BorderPane><left><Label text="%archive.version"/></left><right><Label fx:id="lblVersion" /></right></BorderPane>
|
||||
<BorderPane><left><Label text="%archive.author"/></left><right><Label fx:id="lblAuthor" /></right></BorderPane>
|
||||
<BorderPane>
|
||||
<left><JFXButton fx:id="btnDescription" onMouseClicked="#onDescribe" text="%modpack.wizard.step.3" styleClass="jfx-button" /></left>
|
||||
<left><JFXButton fx:id="btnDescription" onMouseClicked="#onDescribe" text="%modpack.wizard.step.3" styleClass="jfx-button-border" /></left>
|
||||
<right><JFXButton buttonType="RAISED" fx:id="btnInstall" onMouseClicked="#onInstall" text="%button.install" styleClass="jfx-button-raised" /></right>
|
||||
</BorderPane>
|
||||
</ComponentList>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.jfoenix.controls.JFXButton?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.shape.Rectangle?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.TwoLineListItem?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<fx:root type="StackPane" pickOnBounds="false"
|
||||
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<StackPane fx:id="main" style="-fx-padding: 25;">
|
||||
<StackPane fx:id="updatePane" styleClass="bubble" FXUtils.limitWidth="230" FXUtils.limitHeight="55"
|
||||
onMouseClicked="#onUpgrade" StackPane.alignment="TOP_RIGHT">
|
||||
<HBox StackPane.alignment="CENTER_LEFT" alignment="CENTER_LEFT" spacing="12">
|
||||
<Label fx:id="lblIcon" />
|
||||
<TwoLineListItem titleFill="white" subtitleFill="white" subtitle="%update.bubble.subtitle" fx:id="lblLatestVersion" pickOnBounds="false"
|
||||
style="-jfx-title-font-weight: BOLD;" />
|
||||
|
||||
<StackPane.margin>
|
||||
<Insets left="16" top="9" right="12" bottom="9" />
|
||||
</StackPane.margin>
|
||||
</HBox>
|
||||
|
||||
<JFXButton fx:id="closeUpdateButton" StackPane.alignment="TOP_RIGHT" styleClass="toggle-icon-tiny">
|
||||
<StackPane.margin>
|
||||
<Insets topRightBottomLeft="5" />
|
||||
</StackPane.margin>
|
||||
</JFXButton>
|
||||
</StackPane>
|
||||
|
||||
<StackPane maxWidth="230" maxHeight="55" StackPane.alignment="BOTTOM_RIGHT">
|
||||
<JFXButton prefWidth="230" prefHeight="55" buttonType="RAISED" styleClass="jfx-button-raised"
|
||||
onMouseClicked="#launch" fx:id="btnLaunch">
|
||||
<graphic>
|
||||
<VBox alignment="CENTER" translateX="-7" maxWidth="200">
|
||||
<Label style="-fx-font-size: 16;" text="%version.launch"/>
|
||||
<Label style="-fx-font-size: 12px;" fx:id="lblCurrentGame"/>
|
||||
</VBox>
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
<Rectangle fx:id="separator" translateX="95" height="57" fill="gray" width="1" mouseTransparent="true"/>
|
||||
<JFXButton prefWidth="230" prefHeight="55" buttonType="RAISED" styleClass="jfx-button-raised"
|
||||
style="-fx-font-size: 15;" onMouseClicked="#onMenu" fx:id="btnMenu" />
|
||||
</StackPane>
|
||||
</StackPane>
|
||||
</fx:root>
|
||||
@@ -158,6 +158,14 @@
|
||||
<JFXToggleButton fx:id="chkNoGameCheck" size="8" FXUtils.limitHeight="20" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
<BorderPane>
|
||||
<left>
|
||||
<Label BorderPane.alignment="CENTER_LEFT" text="%settings.advanced.dont_check_jvm_validity"/>
|
||||
</left>
|
||||
<right>
|
||||
<JFXToggleButton fx:id="chkNoJVMCheck" size="8" FXUtils.limitHeight="20" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
</ComponentList>
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
</JFXTabPane>
|
||||
|
||||
<HBox alignment="TOP_RIGHT" style="-fx-padding: 4;" spacing="3" pickOnBounds="false">
|
||||
<JFXButton fx:id="btnTestGame" maxHeight="40.0" minHeight="40.0" onMouseClicked="#onTestGame"
|
||||
styleClass="toggle-icon3" />
|
||||
<JFXButton fx:id="btnBrowseMenu" maxHeight="40.0" minHeight="40.0" onMouseClicked="#onBrowseMenu"
|
||||
styleClass="toggle-icon3">
|
||||
<graphic>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 292 KiB |
@@ -43,6 +43,7 @@ account.failed.invalid_token=Please log out and re-input your password to login.
|
||||
account.failed.no_character=No character in this account.
|
||||
account.failed.server_response_malformed=Invalid server response. The authentication server may have a failure.
|
||||
account.injector.add=Add an authentication server
|
||||
account.injector.empty=Empty (Click the plus button right to add)
|
||||
account.injector.manage=Manage authentication servers
|
||||
account.injector.manage.title=Authentication servers
|
||||
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
|
||||
@@ -55,7 +56,7 @@ account.methods.authlib_injector=authlib-injector
|
||||
account.methods.offline=Offline
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.missing=No Account
|
||||
account.missing.add=Click the button on the right to add
|
||||
account.missing.add=Click here to add
|
||||
account.password=Password
|
||||
account.username=Name
|
||||
|
||||
@@ -109,8 +110,6 @@ folder.resourcepacks=Resource packs
|
||||
folder.saves=Saves
|
||||
folder.screenshots=Screenshots
|
||||
|
||||
general.prompt.empty=(None)
|
||||
|
||||
help=Documentations
|
||||
help.detail=For datapacks, modpacks, etc. makers.
|
||||
|
||||
@@ -121,6 +120,7 @@ input.url=Must be a valid URL.
|
||||
|
||||
install=Install New Game
|
||||
install.failed=Failed to install
|
||||
install.failed.downloading=Failed to install due to some files not downloaded successfully
|
||||
install.installer.choose=Choose a %s version
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Game
|
||||
@@ -139,7 +139,10 @@ install.success=Installed successfully
|
||||
lang=English
|
||||
lang.default=Use system language
|
||||
|
||||
launch.advice.corrected=We have already corrected JVM selection and game can be launched now. If you do want to keep your JVM choice, you can turn off the Java VM checker in game settings.
|
||||
launch.advice.uncorrected=If you are sure that game can be launched normally, you can turn off the Java VM checker in game settings.
|
||||
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 and above is not compatible with LiteLoader, please consider downgrade Forge to 2759 or earlier.
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. You are supposed to install the latest version of Java 8.
|
||||
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
|
||||
@@ -164,8 +167,8 @@ launcher=Launcher
|
||||
launcher.background=Background Image
|
||||
launcher.background.choose=Choose background path.
|
||||
launcher.background.default=Default
|
||||
launcher.common_directory=Cache Directory (Store downloaded files)
|
||||
launcher.common_directory.choose=Choose cache directory.
|
||||
launcher.common_directory=Download Cache Directory
|
||||
launcher.common_directory.choose=Choose download cache directory
|
||||
launcher.common_directory.default=Default
|
||||
launcher.common_directory.disabled=Disabled
|
||||
launcher.contact=Contact Us
|
||||
@@ -217,11 +220,13 @@ modpack.name=Modpack Name
|
||||
modpack.not_a_valid_name=Not a valid modpack name
|
||||
modpack.scan=Scanning this modpack
|
||||
modpack.task.install=Import Modpack
|
||||
modpack.task.install.error=Failed to install the modpack. Maybe the files is incorrect, or a management issue occurred.
|
||||
modpack.task.install.error=This modpack file cannot be recognized. Only Curse, MultiMC modpacks are supported.
|
||||
modpack.task.install.will=Install the modpack:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=Install relative files to Curse modpack
|
||||
modpack.type.curse.tolerable_error=But we cannot complete downloading all files of this Curse modpack. You can retry downloading when launching corresponding game version. You may retry for a couple of times due to network problems.
|
||||
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
|
||||
modpack.type.curse.not_found=Some of required resources are deleted and cannot be downloaded. Please consider the latest version or other modpacks.
|
||||
modpack.type.hmcl=HMCL
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
|
||||
@@ -289,6 +294,7 @@ settings=Game Settings
|
||||
|
||||
settings.advanced=Advanced Settings
|
||||
settings.advanced.dont_check_game_completeness=Don't check game completeness
|
||||
settings.advanced.dont_check_jvm_validity=Don't check whether JVM can launch the game or not
|
||||
settings.advanced.game_dir.default=Default (.minecraft/)
|
||||
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except assets,libraries)
|
||||
settings.advanced.java_args_default=Default java args: -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
|
||||
@@ -325,7 +331,11 @@ settings.launcher.common_path.tooltip=This app will save all game libraries and
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.launcher.enable_game_list=Display game list in main page
|
||||
settings.launcher.language=Language
|
||||
settings.launcher.log_font=Log Font
|
||||
settings.launcher.launcher_log.export=Export launcher logs
|
||||
settings.launcher.launcher_log.export.failed=Failed to export logs
|
||||
settings.launcher.launcher_log.export.success=Logs have been exported to %s
|
||||
settings.launcher.log=Log
|
||||
settings.launcher.log.font=Log Font
|
||||
settings.launcher.proxy=Proxy
|
||||
settings.launcher.proxy.authentication=Proxy Authentication
|
||||
settings.launcher.proxy.disable=Use system proxies
|
||||
@@ -377,6 +387,7 @@ version.launch_script.failed=Unable to make launch script.
|
||||
version.launch_script.save=Save the launch script
|
||||
version.launch_script.success=Finished script creation, %s.
|
||||
version.manage=Game List
|
||||
version.manage.clean=Clean game directory
|
||||
version.manage.manage=Manage Game
|
||||
version.manage.redownload_assets_index=Redownload Assets Index
|
||||
version.manage.remove=Delete this game
|
||||
|
||||
@@ -43,6 +43,7 @@ account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
|
||||
account.failed.no_character=該帳號沒有角色
|
||||
account.failed.server_response_malformed=無法解析認證伺服器響應,可能是伺服器故障
|
||||
account.injector.add=新增認證伺服器
|
||||
account.injector.empty=無(點擊右側加號添加)
|
||||
account.injector.manage=管理認證伺服器
|
||||
account.injector.manage.title=認證伺服器
|
||||
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
|
||||
@@ -55,7 +56,7 @@ account.methods.authlib_injector=authlib-injector 登入
|
||||
account.methods.offline=離線模式
|
||||
account.methods.yggdrasil=正版登入
|
||||
account.missing=沒有遊戲帳戶
|
||||
account.missing.add=點擊右邊按鈕添加
|
||||
account.missing.add=點擊此處添加帳戶
|
||||
account.password=密碼
|
||||
account.username=使用者名稱
|
||||
|
||||
@@ -109,8 +110,6 @@ folder.resourcepacks=資源包資料夾
|
||||
folder.saves=遊戲存檔資料夾
|
||||
folder.screenshots=截圖資料夾
|
||||
|
||||
general.prompt.empty=(無)
|
||||
|
||||
help=HMCL 幫助文檔
|
||||
help.detail=可查閱資料包、整合包製作指南等內容。
|
||||
|
||||
@@ -121,6 +120,7 @@ input.url=必須為有效連結
|
||||
|
||||
install=新增遊戲
|
||||
install.failed=安裝失敗
|
||||
install.failed.downloading=安裝失敗,部分文件未能完成下載
|
||||
install.installer.choose=選擇 %s 版本
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=遊戲
|
||||
@@ -139,7 +139,10 @@ install.success=安裝成功
|
||||
lang=正體中文
|
||||
lang.default=跟隨系統語言
|
||||
|
||||
launch.advice.corrected=我们已经修正了问题。如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
|
||||
launch.advice.uncorrected=如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
|
||||
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者更換 Forge 至 2759 或更舊的版本。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
|
||||
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod Minecraft 版本 不支援 Java 9 或更高版本,請使用 Java 8。
|
||||
@@ -164,8 +167,8 @@ launcher=啟動器
|
||||
launcher.background=背景位址
|
||||
launcher.background.choose=選擇背景路徑
|
||||
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
|
||||
launcher.common_directory=緩存目錄(檔案下載緩存)
|
||||
launcher.common_directory.choose=選擇緩存目錄
|
||||
launcher.common_directory=檔案下載緩存目錄
|
||||
launcher.common_directory.choose=選擇檔案下載緩存目錄
|
||||
launcher.common_directory.default=預設
|
||||
launcher.common_directory.disabled=停用
|
||||
launcher.contact=聯絡我們
|
||||
@@ -217,11 +220,13 @@ modpack.name=整合包名稱
|
||||
modpack.not_a_valid_name=整合包名稱無效
|
||||
modpack.scan=解析整合包
|
||||
modpack.task.install=匯入整合包
|
||||
modpack.task.install.error=安裝失敗,可能是整合包格式錯誤或操作檔案失敗。
|
||||
modpack.task.install.error=無法識別該整合包,目前僅支持導入 Curse、MultiMC、HMCL 整合包。
|
||||
modpack.task.install.will=將會安裝整合包:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=下載 Curse 整合包相關檔案
|
||||
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下載,您可以在啟動該遊戲版本時繼續 Curse 整合包文件的下載。由於網路問題,您可能需要重試多次。
|
||||
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
|
||||
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該整合包的最新版本或者安裝其他整合包。
|
||||
modpack.type.hmcl=HMCL
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.unsupported=不支援該整合包。僅 HMCL、MultiMC、Curse 整合包受支援。
|
||||
@@ -289,6 +294,7 @@ settings=遊戲設定
|
||||
|
||||
settings.advanced=進階設定
|
||||
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
|
||||
settings.advanced.dont_check_jvm_validity=不檢查 JVM 與遊戲的相容性
|
||||
settings.advanced.game_dir.default=預設(.minecraft/)
|
||||
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
|
||||
settings.advanced.java_args_default=啟動器預設新增的參數(請勿重複新增):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
|
||||
@@ -325,7 +331,11 @@ settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及依賴庫
|
||||
settings.launcher.download_source=下載來源
|
||||
settings.launcher.enable_game_list=在首頁內顯示遊戲列表
|
||||
settings.launcher.language=語言
|
||||
settings.launcher.log_font=記錄字體
|
||||
settings.launcher.launcher_log.export=導出啟動器日誌
|
||||
settings.launcher.launcher_log.export.failed=無法導出日誌
|
||||
settings.launcher.launcher_log.export.success=日誌已保存到 %s
|
||||
settings.launcher.log=記錄
|
||||
settings.launcher.log.font=記錄字體
|
||||
settings.launcher.proxy=代理
|
||||
settings.launcher.proxy.authentication=身份驗證
|
||||
settings.launcher.proxy.disable=使用系統代理
|
||||
@@ -377,6 +387,7 @@ version.launch_script.failed=生成啟動腳本失敗
|
||||
version.launch_script.save=儲存啟動腳本
|
||||
version.launch_script.success=啟動腳本已生成完畢:%s
|
||||
version.manage=遊戲列表
|
||||
version.manage.clean=清理遊戲目錄
|
||||
version.manage.manage=游戏管理
|
||||
version.manage.redownload_assets_index=更新資源列表
|
||||
version.manage.remove=刪除該版本
|
||||
|
||||
@@ -43,6 +43,7 @@ account.failed.invalid_token=请尝试登出并重新输入密码登录
|
||||
account.failed.no_character=该帐号没有角色
|
||||
account.failed.server_response_malformed=无法解析认证服务器响应,可能是服务器故障
|
||||
account.injector.add=添加认证服务器
|
||||
account.injector.empty=无(点击右侧加号添加)
|
||||
account.injector.manage=管理认证服务器
|
||||
account.injector.manage.title=认证服务器
|
||||
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
|
||||
@@ -55,7 +56,7 @@ account.methods.authlib_injector=外置登录 (authlib-injector)
|
||||
account.methods.offline=离线模式
|
||||
account.methods.yggdrasil=正版登录
|
||||
account.missing=没有游戏账户
|
||||
account.missing.add=点击右边按钮添加
|
||||
account.missing.add=点击此处添加账户
|
||||
account.password=密码
|
||||
account.username=用户名
|
||||
|
||||
@@ -109,8 +110,6 @@ folder.resourcepacks=资源包文件夹
|
||||
folder.saves=存档文件夹
|
||||
folder.screenshots=截图文件夹
|
||||
|
||||
general.prompt.empty=(无)
|
||||
|
||||
help=HMCL 帮助文档
|
||||
help.detail=可查阅数据包、整合包制作指南等内容。
|
||||
|
||||
@@ -121,6 +120,7 @@ input.url=必须是合法的链接
|
||||
|
||||
install=添加游戏
|
||||
install.failed=安装失败
|
||||
install.failed.downloading=安装失败,部分文件未能完成下载
|
||||
install.installer.choose=选择 %s 版本
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=游戏
|
||||
@@ -139,7 +139,10 @@ install.success=安装成功
|
||||
lang=简体中文
|
||||
lang.default=跟随系统语言
|
||||
|
||||
launch.advice.corrected=我们已经修复了问题。如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
|
||||
launch.advice.uncorrected=如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
|
||||
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2759 或更旧的版本。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
|
||||
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
|
||||
@@ -164,8 +167,8 @@ launcher=启动器
|
||||
launcher.background=背景地址
|
||||
launcher.background.choose=选择背景路径
|
||||
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
|
||||
launcher.common_directory=缓存目录(文件下载缓存)
|
||||
launcher.common_directory.choose=选择缓存目录
|
||||
launcher.common_directory=文件下载缓存目录
|
||||
launcher.common_directory.choose=选择文件下载缓存目录
|
||||
launcher.common_directory.default=默认(%AppData%/.minecraft 或者 ~/.minecraft)
|
||||
launcher.common_directory.disabled=禁用(总是使用游戏路径)
|
||||
launcher.contact=联系我们
|
||||
@@ -217,11 +220,13 @@ modpack.name=整合包名称
|
||||
modpack.not_a_valid_name=不是一个有效的整合包名称
|
||||
modpack.scan=解析整合包
|
||||
modpack.task.install=导入整合包
|
||||
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败。
|
||||
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。
|
||||
modpack.task.install.will=将会安装整合包:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=下载 Curse 整合包相关文件
|
||||
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次。
|
||||
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
|
||||
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
|
||||
modpack.type.hmcl=HMCL
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.unsupported=该整合包不被支持。仅 HMCL、MultiMC、Curse 整合包受支持。
|
||||
@@ -289,6 +294,7 @@ settings=游戏设置
|
||||
|
||||
settings.advanced=高级设置
|
||||
settings.advanced.dont_check_game_completeness=不检查游戏完整性
|
||||
settings.advanced.dont_check_jvm_validity=不检查 JVM 与游戏的兼容性
|
||||
settings.advanced.game_dir.default=默认(.minecraft/)
|
||||
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
|
||||
settings.advanced.java_args_default=启动器默认添加的参数(请不要重复添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
|
||||
@@ -325,7 +331,11 @@ settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库
|
||||
settings.launcher.download_source=下载源
|
||||
settings.launcher.enable_game_list=在主页内显示游戏列表
|
||||
settings.launcher.language=语言
|
||||
settings.launcher.log_font=日志字体
|
||||
settings.launcher.launcher_log.export=导出启动器日志
|
||||
settings.launcher.launcher_log.export.failed=无法导出日志
|
||||
settings.launcher.launcher_log.export.success=日志已保存到 %s
|
||||
settings.launcher.log=日志
|
||||
settings.launcher.log.font=日志字体
|
||||
settings.launcher.proxy=代理
|
||||
settings.launcher.proxy.authentication=身份验证
|
||||
settings.launcher.proxy.disable=使用系统代理
|
||||
@@ -377,6 +387,7 @@ version.launch_script.failed=生成启动脚本失败
|
||||
version.launch_script.save=保存启动脚本
|
||||
version.launch_script.success=启动脚本已生成完毕:%s
|
||||
version.manage=游戏列表
|
||||
version.manage.clean=清理游戏目录
|
||||
version.manage.manage=游戏管理
|
||||
version.manage.redownload_assets_index=更新资源列表
|
||||
version.manage.remove=删除该版本
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 941 KiB |
+38
-13
@@ -25,24 +25,24 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
private AuthlibInjectorServer server;
|
||||
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
|
||||
private AuthlibInjectorArtifactProvider downloader;
|
||||
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
super(service, username, characterUUID, session);
|
||||
|
||||
this.authlibInjectorDownloader = authlibInjectorDownloader;
|
||||
this.downloader = downloader;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@@ -56,10 +56,23 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
return inject(() -> super.logInWithPassword(password, selector));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
Optional<AuthInfo> auth = super.playOffline();
|
||||
Optional<AuthlibInjectorArtifactInfo> artifact = downloader.getArtifactInfoImmediately();
|
||||
Optional<String> prefetchedMeta = server.getMetadataResponse();
|
||||
|
||||
if (auth.isPresent() && artifact.isPresent() && prefetchedMeta.isPresent()) {
|
||||
return Optional.of(auth.get().withArguments(generateArguments(artifact.get(), server, prefetchedMeta.get())));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> loginAction) throws AuthenticationException {
|
||||
CompletableFuture<byte[]> prefetchedMetaTask = CompletableFuture.supplyAsync(() -> {
|
||||
try (InputStream in = new URL(server.getUrl()).openStream()) {
|
||||
return readFullyWithoutClosing(in);
|
||||
CompletableFuture<String> prefetchedMetaTask = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(new ServerDisconnectException(e));
|
||||
}
|
||||
@@ -67,14 +80,14 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
|
||||
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return authlibInjectorDownloader.get();
|
||||
return downloader.getArtifactInfo();
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(new AuthlibInjectorDownloadException(e));
|
||||
}
|
||||
});
|
||||
|
||||
AuthInfo auth = loginAction.get();
|
||||
byte[] prefetchedMeta;
|
||||
String prefetchedMeta;
|
||||
AuthlibInjectorArtifactInfo artifact;
|
||||
|
||||
try {
|
||||
@@ -91,10 +104,14 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
}
|
||||
}
|
||||
|
||||
return auth.withArguments(new Arguments().addJVMArguments(
|
||||
return auth.withArguments(generateArguments(artifact, server, prefetchedMeta));
|
||||
}
|
||||
|
||||
private static Arguments generateArguments(AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
|
||||
return new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
||||
"-Dauthlibinjector.side=client",
|
||||
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta)));
|
||||
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,4 +137,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
|
||||
return super.equals(another) && server.equals(another.server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("username", getUsername())
|
||||
.append("server", getServer())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
+5
-8
@@ -22,9 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
@@ -32,14 +29,14 @@ import java.util.function.Function;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
|
||||
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
|
||||
private AuthlibInjectorArtifactProvider downloader;
|
||||
private Function<String, AuthlibInjectorServer> serverLookup;
|
||||
|
||||
/**
|
||||
* @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url
|
||||
*/
|
||||
public AuthlibInjectorAccountFactory(ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, Function<String, AuthlibInjectorServer> serverLookup) {
|
||||
this.authlibInjectorDownloader = authlibInjectorDownloader;
|
||||
public AuthlibInjectorAccountFactory(AuthlibInjectorArtifactProvider downloader, Function<String, AuthlibInjectorServer> serverLookup) {
|
||||
this.downloader = downloader;
|
||||
this.serverLookup = serverLookup;
|
||||
}
|
||||
|
||||
@@ -52,7 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
|
||||
|
||||
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, authlibInjectorDownloader, username, null, null);
|
||||
server, downloader, username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
}
|
||||
@@ -71,6 +68,6 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
|
||||
|
||||
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, authlibInjectorDownloader, username, session.getSelectedProfile().getId(), session);
|
||||
server, downloader, username, session.getSelectedProfile().getId(), session);
|
||||
}
|
||||
}
|
||||
+29
@@ -17,10 +17,39 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class AuthlibInjectorArtifactInfo {
|
||||
|
||||
public static AuthlibInjectorArtifactInfo from(Path location) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(location.toFile())) {
|
||||
Attributes attributes = jarFile.getManifest().getMainAttributes();
|
||||
|
||||
String title = Optional.ofNullable(attributes.getValue("Implementation-Title"))
|
||||
.orElseThrow(() -> new IOException("Missing Implementation-Title"));
|
||||
if (!"authlib-injector".equals(title)) {
|
||||
throw new IOException("Bad Implementation-Title");
|
||||
}
|
||||
|
||||
String version = Optional.ofNullable(attributes.getValue("Implementation-Version"))
|
||||
.orElseThrow(() -> new IOException("Missing Implementation-Version"));
|
||||
|
||||
int buildNumber;
|
||||
try {
|
||||
buildNumber = Optional.ofNullable(attributes.getValue("Build-Number"))
|
||||
.map(Integer::parseInt)
|
||||
.orElseThrow(() -> new IOException("Missing Build-Number"));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Bad Build-Number", e);
|
||||
}
|
||||
return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private int buildNumber;
|
||||
private String version;
|
||||
private Path location;
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AuthlibInjectorArtifactProvider {
|
||||
|
||||
AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException;
|
||||
|
||||
Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately();
|
||||
|
||||
}
|
||||
+8
-29
@@ -32,13 +32,11 @@ import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class AuthlibInjectorDownloader {
|
||||
public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvider {
|
||||
|
||||
private static final String LATEST_BUILD_URL = "https://authlib-injector.yushi.moe/artifact/latest.json";
|
||||
|
||||
@@ -58,6 +56,7 @@ public class AuthlibInjectorDownloader {
|
||||
this.downloadProvider = downloadProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
|
||||
synchronized (artifactLocation) {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
|
||||
@@ -79,6 +78,11 @@ public class AuthlibInjectorDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
return getLocalArtifact();
|
||||
}
|
||||
|
||||
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
|
||||
LOG.info("Checking update of authlib-injector");
|
||||
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
|
||||
@@ -116,38 +120,13 @@ public class AuthlibInjectorDownloader {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(readArtifactInfo(artifactLocation));
|
||||
return Optional.of(AuthlibInjectorArtifactInfo.from(artifactLocation));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthlibInjectorArtifactInfo readArtifactInfo(Path location) throws IOException {
|
||||
try (JarFile jarFile = new JarFile(location.toFile())) {
|
||||
Attributes attributes = jarFile.getManifest().getMainAttributes();
|
||||
|
||||
String title = Optional.ofNullable(attributes.getValue("Implementation-Title"))
|
||||
.orElseThrow(() -> new IOException("Missing Implementation-Title"));
|
||||
if (!"authlib-injector".equals(title)) {
|
||||
throw new IOException("Bad Implementation-Title");
|
||||
}
|
||||
|
||||
String version = Optional.ofNullable(attributes.getValue("Implementation-Version"))
|
||||
.orElseThrow(() -> new IOException("Missing Implementation-Version"));
|
||||
|
||||
int buildNumber;
|
||||
try {
|
||||
buildNumber = Optional.ofNullable(attributes.getValue("Build-Number"))
|
||||
.map(Integer::parseInt)
|
||||
.orElseThrow(() -> new IOException("Missing Build-Number"));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Bad Build-Number", e);
|
||||
}
|
||||
return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthlibInjectorVersionInfo {
|
||||
@SerializedName("build_number")
|
||||
public int buildNumber;
|
||||
|
||||
+123
-30
@@ -20,25 +20,40 @@ package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray;
|
||||
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
public class AuthlibInjectorServer {
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
|
||||
@JsonAdapter(AuthlibInjectorServer.Deserializer.class)
|
||||
public class AuthlibInjectorServer implements Observable {
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
|
||||
public static AuthlibInjectorServer locateServer(String url) throws IOException {
|
||||
url = parseInputUrl(url);
|
||||
@@ -60,8 +75,11 @@ public class AuthlibInjectorServer {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
return parseResponse(url, readFullyWithoutClosing(conn.getInputStream()));
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
server.refreshMetadata(readFullyWithoutClosing(conn.getInputStream()));
|
||||
return server;
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
@@ -107,45 +125,83 @@ public class AuthlibInjectorServer {
|
||||
}
|
||||
|
||||
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
|
||||
try (InputStream in = new URL(url).openStream()) {
|
||||
return parseResponse(url, readFullyWithoutClosing(in));
|
||||
}
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer parseResponse(String url, byte[] rawResponse) throws IOException {
|
||||
try {
|
||||
JsonObject response = JsonUtils.fromNonNullJson(new String(rawResponse, UTF_8), JsonObject.class);
|
||||
String name = extractServerName(response).orElse(url);
|
||||
return new AuthlibInjectorServer(url, name);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> extractServerName(JsonObject response){
|
||||
return tryCast(response.get("meta"), JsonObject.class)
|
||||
.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString));
|
||||
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
|
||||
server.refreshMetadata();
|
||||
return server;
|
||||
}
|
||||
|
||||
private String url;
|
||||
private String name;
|
||||
@Nullable
|
||||
private String metadataResponse;
|
||||
private long metadataTimestamp;
|
||||
|
||||
public AuthlibInjectorServer(String url, String name) {
|
||||
@Nullable
|
||||
private transient String name;
|
||||
|
||||
private transient boolean metadataRefreshed;
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public AuthlibInjectorServer(String url) {
|
||||
this.url = url;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
public Optional<String> getMetadataResponse() {
|
||||
return Optional.ofNullable(metadataResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return url + " (" + name + ")";
|
||||
public long getMetadataTimestamp() {
|
||||
return metadataTimestamp;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return Optional.ofNullable(name)
|
||||
.orElse(url);
|
||||
}
|
||||
|
||||
public String fetchMetadataResponse() throws IOException {
|
||||
if (metadataResponse == null || !metadataRefreshed) {
|
||||
refreshMetadata();
|
||||
}
|
||||
return getMetadataResponse().get();
|
||||
}
|
||||
|
||||
public void refreshMetadata() throws IOException {
|
||||
refreshMetadata(readFullyAsByteArray(new URL(url).openStream()));
|
||||
}
|
||||
|
||||
private void refreshMetadata(byte[] rawResponse) throws IOException {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String text = new String(rawResponse, UTF_8);
|
||||
try {
|
||||
setMetadataResponse(text, timestamp);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed response\n" + text, e);
|
||||
}
|
||||
|
||||
metadataRefreshed = true;
|
||||
LOG.info("authlib-injector server metadata refreshed: " + url);
|
||||
Platform.runLater(helper::invalidate);
|
||||
}
|
||||
|
||||
private void setMetadataResponse(String metadataResponse, long metadataTimestamp) throws JsonParseException {
|
||||
JsonObject response = GSON.fromJson(metadataResponse, JsonObject.class);
|
||||
if (response == null) {
|
||||
throw new JsonParseException("Metadata response is empty");
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
this.metadataResponse = metadataResponse;
|
||||
this.metadataTimestamp = metadataTimestamp;
|
||||
|
||||
Optional<JsonObject> metaObject = tryCast(response.get("meta"), JsonObject.class);
|
||||
|
||||
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -162,4 +218,41 @@ public class AuthlibInjectorServer {
|
||||
AuthlibInjectorServer another = (AuthlibInjectorServer) obj;
|
||||
return this.url.equals(another.url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name == null ? url : url + " (" + name + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
}
|
||||
|
||||
public static class Deserializer implements JsonDeserializer<AuthlibInjectorServer> {
|
||||
@Override
|
||||
public AuthlibInjectorServer deserialize(JsonElement json, Type type, JsonDeserializationContext ctx) throws JsonParseException {
|
||||
JsonObject jsonObj = json.getAsJsonObject();
|
||||
AuthlibInjectorServer instance = new AuthlibInjectorServer(jsonObj.get("url").getAsString());
|
||||
|
||||
if (jsonObj.has("name")) {
|
||||
instance.name = jsonObj.get("name").getAsString();
|
||||
}
|
||||
|
||||
if (jsonObj.has("metadataResponse")) {
|
||||
try {
|
||||
instance.setMetadataResponse(jsonObj.get("metadataResponse").getAsString(), jsonObj.get("metadataTimestamp").getAsLong());
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Ignoring malformed metadata response cache: " + jsonObj.get("metadataResponse"), e);
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class SimpleAuthlibInjectorArtifactProvider implements AuthlibInjectorArtifactProvider {
|
||||
|
||||
private Path location;
|
||||
|
||||
public SimpleAuthlibInjectorArtifactProvider(Path location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
|
||||
return AuthlibInjectorArtifactInfo.from(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
try {
|
||||
return Optional.of(getArtifactInfo());
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
private Index index = null;
|
||||
|
||||
public DefaultCacheRepository() {
|
||||
this(OperatingSystem.getWorkingDirectory("minecraft").toPath());
|
||||
this(OperatingSystem.getWorkingDirectory("minecraft"));
|
||||
}
|
||||
|
||||
public DefaultCacheRepository(Path commonDirectory) {
|
||||
|
||||
@@ -35,9 +35,12 @@ public class LibraryDownloadTask extends Task {
|
||||
protected final Library library;
|
||||
protected final String url;
|
||||
protected boolean xz;
|
||||
private final Library originalLibrary;
|
||||
private boolean cached = false;
|
||||
|
||||
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
|
||||
this.originalLibrary = library;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
|
||||
if (library.is("net.minecraftforge", "forge"))
|
||||
@@ -86,7 +89,7 @@ public class LibraryDownloadTask extends Task {
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
Optional<Path> libPath = cacheRepository.getLibrary(library);
|
||||
Optional<Path> libPath = cacheRepository.getLibrary(originalLibrary);
|
||||
if (libPath.isPresent()) {
|
||||
try {
|
||||
FileUtils.copyFile(libPath.get().toFile(), jar);
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class EventManager<T extends Event> {
|
||||
register(consumer, EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
public void register(Consumer<T> consumer, EventPriority priority) {
|
||||
public synchronized void register(Consumer<T> consumer, EventPriority priority) {
|
||||
if (!handlers.get(priority).contains(consumer))
|
||||
handlers.put(priority, consumer);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public final class EventManager<T extends Event> {
|
||||
register(t -> runnable.run(), priority);
|
||||
}
|
||||
|
||||
public Event.Result fireEvent(T event) {
|
||||
public synchronized Event.Result fireEvent(T event) {
|
||||
for (EventPriority priority : EventPriority.values()) {
|
||||
for (Consumer<T> handler : handlers.get(priority))
|
||||
handler.accept(event);
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
@@ -28,6 +30,7 @@ import java.util.Map;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(Argument.Serializer.class)
|
||||
@Immutable
|
||||
public interface Argument extends Cloneable {
|
||||
|
||||
@@ -41,12 +44,6 @@ public interface Argument extends Cloneable {
|
||||
List<String> toString(Map<String, String> keys, Map<String, Boolean> features);
|
||||
|
||||
class Serializer implements JsonDeserializer<Argument>, JsonSerializer<Argument> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argument deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json.isJsonPrimitive())
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
@@ -35,6 +36,7 @@ import java.util.Optional;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(Library.Serializer.class)
|
||||
public class Library implements Comparable<Library> {
|
||||
|
||||
private final String groupId;
|
||||
@@ -189,12 +191,6 @@ public class Library implements Comparable<Library> {
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonDeserializer<Library>, JsonSerializer<Library> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Library deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json == JsonNull.INSTANCE)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
@@ -32,6 +33,7 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(RuledArgument.Serializer.class)
|
||||
@Immutable
|
||||
public class RuledArgument implements Argument {
|
||||
|
||||
@@ -74,12 +76,6 @@ public class RuledArgument implements Argument {
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<RuledArgument>, JsonDeserializer<RuledArgument> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(RuledArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject obj = new JsonObject();
|
||||
|
||||
@@ -61,7 +61,7 @@ public class Version implements Comparable<Version>, Validation {
|
||||
this.jar = jar;
|
||||
this.assetIndex = assetIndex;
|
||||
this.assets = assets;
|
||||
this.libraries = new LinkedList<>(libraries);
|
||||
this.libraries = libraries == null ? new LinkedList<>() : new LinkedList<>(libraries);
|
||||
this.compatibilityRules = compatibilityRules == null ? null : new LinkedList<>(compatibilityRules);
|
||||
this.downloads = downloads == null ? null : new HashMap<>(downloads);
|
||||
this.logging = logging == null ? null : new HashMap<>(logging);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
@@ -27,6 +28,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
@@ -103,6 +105,7 @@ public final class CurseCompletionTask extends Task {
|
||||
|
||||
AtomicBoolean flag = new AtomicBoolean(true);
|
||||
AtomicInteger finished = new AtomicInteger(0);
|
||||
AtomicBoolean notFound = new AtomicBoolean(false);
|
||||
|
||||
// Because in China, Curse is too difficult to visit,
|
||||
// if failed, ignore it and retry next time.
|
||||
@@ -113,6 +116,17 @@ public final class CurseCompletionTask extends Task {
|
||||
if (StringUtils.isBlank(file.getFileName())) {
|
||||
try {
|
||||
return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
|
||||
} catch (FileNotFoundException e) {
|
||||
try {
|
||||
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
|
||||
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
|
||||
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
|
||||
} catch (IOException | JsonParseException e2) {
|
||||
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), e2);
|
||||
notFound.set(true);
|
||||
return file;
|
||||
}
|
||||
|
||||
} catch (IOException ioe) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), ioe);
|
||||
flag.set(false);
|
||||
@@ -132,9 +146,13 @@ public final class CurseCompletionTask extends Task {
|
||||
}
|
||||
|
||||
// Let this task fail if the curse manifest has not been completed.
|
||||
if (!flag.get())
|
||||
// But continue other downloads.
|
||||
if (!flag.get() || notFound.get())
|
||||
dependencies.add(Task.of(() -> {
|
||||
throw new CurseCompletionException();
|
||||
if (notFound.get())
|
||||
throw new CurseCompletionException(new FileNotFoundException());
|
||||
else
|
||||
throw new CurseCompletionException();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -41,18 +41,22 @@ public final class CurseManifestFile implements Validation {
|
||||
|
||||
@SerializedName("fileName")
|
||||
private final String fileName;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
@SerializedName("required")
|
||||
private final boolean required;
|
||||
|
||||
public CurseManifestFile() {
|
||||
this(0, 0, "", true);
|
||||
this(0, 0, null, null, true);
|
||||
}
|
||||
|
||||
public CurseManifestFile(int projectID, int fileID, String fileName, boolean required) {
|
||||
public CurseManifestFile(int projectID, int fileID, String fileName, String url, boolean required) {
|
||||
this.projectID = projectID;
|
||||
this.fileID = fileID;
|
||||
this.fileName = fileName;
|
||||
this.url = url;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@@ -79,11 +83,16 @@ public final class CurseManifestFile implements Validation {
|
||||
}
|
||||
|
||||
public URL getUrl() {
|
||||
return NetworkUtils.toURL("https://minecraft.curseforge.com/projects/" + projectID + "/files/" + fileID + "/download");
|
||||
return url == null ? NetworkUtils.toURL("https://minecraft.curseforge.com/projects/" + projectID + "/files/" + fileID + "/download")
|
||||
: NetworkUtils.toURL(url);
|
||||
}
|
||||
|
||||
|
||||
public CurseManifestFile withFileName(String fileName) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, required);
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required);
|
||||
}
|
||||
|
||||
public CurseManifestFile withURL(String url) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
@Immutable
|
||||
public final class CurseMetaMod {
|
||||
@SerializedName("Id")
|
||||
private final int id;
|
||||
|
||||
@SerializedName("FileName")
|
||||
private final String fileName;
|
||||
|
||||
@SerializedName("FileNameOnDisk")
|
||||
private final String fileNameOnDisk;
|
||||
|
||||
@SerializedName("DownloadURL")
|
||||
private final String downloadURL;
|
||||
|
||||
public CurseMetaMod() {
|
||||
this(0, "", "", "");
|
||||
}
|
||||
|
||||
public CurseMetaMod(int id, String fileName, String fileNameOnDisk, String downloadURL) {
|
||||
this.id = id;
|
||||
this.fileName = fileName;
|
||||
this.fileNameOnDisk = fileNameOnDisk;
|
||||
this.downloadURL = downloadURL;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getFileNameOnDisk() {
|
||||
return fileNameOnDisk;
|
||||
}
|
||||
|
||||
public String getDownloadURL() {
|
||||
return downloadURL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
public class DownloadException extends IOException {
|
||||
|
||||
private final URL url;
|
||||
|
||||
public DownloadException(URL url, Throwable cause) {
|
||||
super("Unable to download " + url + ", " + cause.getMessage(), cause);
|
||||
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public URL getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.task;
|
||||
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.event.FailedEvent;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
@@ -218,12 +219,18 @@ public class FileDownloadTask extends Task {
|
||||
|
||||
HttpURLConnection con = NetworkUtils.createConnection(url);
|
||||
if (checkETag) repository.injectConnection(con);
|
||||
con.connect();
|
||||
con = NetworkUtils.resolveConnection(con);
|
||||
|
||||
if (con.getResponseCode() == 304) {
|
||||
if (con.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
// Handle cache
|
||||
Path cache = repository.getCachedRemoteFile(con);
|
||||
FileUtils.copyFile(cache.toFile(), file);
|
||||
try {
|
||||
Path cache = repository.getCachedRemoteFile(con);
|
||||
FileUtils.copyFile(cache.toFile(), file);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload it", e);
|
||||
repository.removeRemoteEntry(con);
|
||||
}
|
||||
} else if (con.getResponseCode() / 100 != 2) {
|
||||
throw new IOException("Server error, response code: " + con.getResponseCode());
|
||||
}
|
||||
@@ -322,7 +329,7 @@ public class FileDownloadTask extends Task {
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
throw new IOException("Unable to download file " + currentURL + ". " + exception.getMessage(), exception);
|
||||
throw new DownloadException(currentURL, exception);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -96,11 +95,17 @@ public final class GetTask extends TaskResult<String> {
|
||||
if (checkETag) repository.injectConnection(conn);
|
||||
conn.connect();
|
||||
|
||||
if (conn.getResponseCode() == 304) {
|
||||
if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
// Handle cache
|
||||
Path cache = repository.getCachedRemoteFile(conn);
|
||||
setResult(FileUtils.readText(cache));
|
||||
return;
|
||||
try {
|
||||
Path cache = repository.getCachedRemoteFile(conn);
|
||||
setResult(FileUtils.readText(cache));
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload it", e);
|
||||
repository.removeRemoteEntry(conn);
|
||||
continue;
|
||||
}
|
||||
} else if (conn.getResponseCode() / 100 != 2) {
|
||||
throw new IOException("Server error, response code: " + conn.getResponseCode());
|
||||
}
|
||||
@@ -135,7 +140,7 @@ public final class GetTask extends TaskResult<String> {
|
||||
}
|
||||
}
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
throw new DownloadException(url, exception);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -45,6 +46,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class CacheRepository {
|
||||
private Path commonDirectory;
|
||||
@@ -69,7 +71,7 @@ public class CacheRepository {
|
||||
} else
|
||||
index = new HashMap<>();
|
||||
} catch (IOException | JsonParseException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to read index file", e);
|
||||
LOG.log(Level.WARNING, "Unable to read index file", e);
|
||||
index = new HashMap<>();
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
@@ -162,6 +164,16 @@ public class CacheRepository {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void removeRemoteEntry(URLConnection conn) {
|
||||
String url = conn.getURL().toString();
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
index.remove(url);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void injectConnection(URLConnection conn) {
|
||||
String url = conn.getURL().toString();
|
||||
lock.readLock().lock();
|
||||
@@ -189,7 +201,7 @@ public class CacheRepository {
|
||||
Lock writeLock = lock.writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
index.put(url, eTagItem);
|
||||
index.compute(eTagItem.url, updateEntity(eTagItem));
|
||||
saveETagIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
@@ -208,13 +220,31 @@ public class CacheRepository {
|
||||
Lock writeLock = lock.writeLock();
|
||||
writeLock.lock();
|
||||
try {
|
||||
index.put(url, eTagItem);
|
||||
index.compute(eTagItem.url, updateEntity(eTagItem));
|
||||
saveETagIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private BiFunction<String, ETagItem, ETagItem> updateEntity(ETagItem newItem) {
|
||||
return (key, oldItem) -> {
|
||||
if (oldItem == null) {
|
||||
return newItem;
|
||||
} else if (oldItem.compareTo(newItem) < 0) {
|
||||
Path cached = getFile(SHA1, oldItem.hash);
|
||||
try {
|
||||
Files.deleteIfExists(cached);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Cannot delete old file");
|
||||
}
|
||||
return newItem;
|
||||
} else {
|
||||
return oldItem;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private final Map<String, ETagItem> joinETagIndexes(Collection<ETagItem>... indexes) {
|
||||
Map<String, ETagItem> eTags = new ConcurrentHashMap<>();
|
||||
@@ -223,12 +253,7 @@ public class CacheRepository {
|
||||
.reduce(Stream.empty(), Stream::concat);
|
||||
|
||||
stream.forEach(eTag -> {
|
||||
eTags.compute(eTag.url, (key, oldValue) -> {
|
||||
if (oldValue == null || oldValue.compareTo(eTag) < 0)
|
||||
return eTag;
|
||||
else
|
||||
return oldValue;
|
||||
});
|
||||
eTags.compute(eTag.url, updateEntity(eTag));
|
||||
});
|
||||
|
||||
return eTags;
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.logging.*;
|
||||
@@ -31,20 +32,16 @@ import java.util.logging.*;
|
||||
*/
|
||||
public final class Logging {
|
||||
|
||||
public static final Logger LOG;
|
||||
private static final ByteArrayOutputStream OUTPUT_STREAM = new ByteArrayOutputStream();
|
||||
public static final Logger LOG = Logger.getLogger("HMCL");
|
||||
private static ByteArrayOutputStream storedLogs = new ByteArrayOutputStream();
|
||||
|
||||
static {
|
||||
LOG = Logger.getLogger("HMCL");
|
||||
}
|
||||
|
||||
public static void start(File logFolder) {
|
||||
LOG.setLevel(Level.FINER);
|
||||
public static void start(Path logFolder) {
|
||||
LOG.setLevel(Level.ALL);
|
||||
LOG.setUseParentHandlers(false);
|
||||
|
||||
try {
|
||||
FileHandler fileHandler = new FileHandler(new File(logFolder, "hmcl.log").getAbsolutePath());
|
||||
fileHandler.setLevel(Level.FINEST);
|
||||
Files.createDirectories(logFolder);
|
||||
FileHandler fileHandler = new FileHandler(logFolder.resolve("hmcl.log").toAbsolutePath().toString());
|
||||
fileHandler.setFormatter(DefaultFormatter.INSTANCE);
|
||||
LOG.addHandler(fileHandler);
|
||||
} catch (IOException e) {
|
||||
@@ -52,31 +49,28 @@ public final class Logging {
|
||||
}
|
||||
|
||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||
consoleHandler.setLevel(Level.FINER);
|
||||
consoleHandler.setFormatter(DefaultFormatter.INSTANCE);
|
||||
LOG.addHandler(consoleHandler);
|
||||
|
||||
StreamHandler streamHandler = new StreamHandler(OUTPUT_STREAM, DefaultFormatter.INSTANCE) {
|
||||
StreamHandler streamHandler = new StreamHandler(storedLogs, DefaultFormatter.INSTANCE) {
|
||||
@Override
|
||||
public synchronized void publish(LogRecord record) {
|
||||
super.publish(record);
|
||||
flush();
|
||||
}
|
||||
};
|
||||
streamHandler.setLevel(Level.FINEST);
|
||||
LOG.addHandler(streamHandler);
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
for (Handler handler : LOG.getHandlers())
|
||||
LOG.removeHandler(handler);
|
||||
public static byte[] getRawLogs() {
|
||||
return storedLogs.toByteArray();
|
||||
}
|
||||
|
||||
public static String getLogs() {
|
||||
return OUTPUT_STREAM.toString();
|
||||
return storedLogs.toString();
|
||||
}
|
||||
|
||||
static final class DefaultFormatter extends Formatter {
|
||||
private static final class DefaultFormatter extends Formatter {
|
||||
|
||||
static final DefaultFormatter INSTANCE = new DefaultFormatter();
|
||||
private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
|
||||
|
||||
@@ -21,12 +21,6 @@ import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.game.Argument;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.RuledArgument;
|
||||
import org.jackhuang.hmcl.game.StringArgument;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
@@ -39,13 +33,8 @@ public final class JsonUtils {
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.enableComplexMapKeySerialization()
|
||||
.setPrettyPrinting()
|
||||
.registerTypeAdapter(Library.class, Library.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(Argument.class, Argument.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(StringArgument.class, Argument.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(RuledArgument.class, RuledArgument.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(Date.class, DateTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(Platform.class, Platform.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE)
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.util.io;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.spi.FileSystemProvider;
|
||||
import java.util.HashMap;
|
||||
@@ -88,7 +87,7 @@ public final class CompressingUtils {
|
||||
*/
|
||||
public static String readTextZipEntry(File zipFile, String name) throws IOException {
|
||||
try (FileSystem fs = createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
return new String(Files.readAllBytes(fs.getPath(name)));
|
||||
return FileUtils.readText(fs.getPath(name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static void writeBytes(File file, byte[] array) throws IOException {
|
||||
Files.createDirectories(file.toPath().getParent());
|
||||
Files.write(file.toPath(), array);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@@ -57,13 +59,76 @@ public final class NetworkUtils {
|
||||
|
||||
public static HttpURLConnection createConnection(URL url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setUseCaches(false);
|
||||
connection.setConnectTimeout(15000);
|
||||
connection.setReadTimeout(15000);
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see <a href="https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
|
||||
* @param location
|
||||
* @return
|
||||
*/
|
||||
public static String encodeLocation(String location) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean left = true;
|
||||
for (char ch : location.toCharArray()) {
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
if (left) sb.append("%20");
|
||||
else sb.append('+');
|
||||
break;
|
||||
case '?':
|
||||
left = false;
|
||||
default:
|
||||
if (ch >= 0x80)
|
||||
sb.append(encodeURL(Character.toString(ch)));
|
||||
else
|
||||
sb.append(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method aims to solve problem when "Location" in stupid server's response is not encoded.
|
||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||
* @param conn
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
|
||||
int redirect = 0;
|
||||
while (true) {
|
||||
|
||||
conn.setUseCaches(false);
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(15000);
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
Map<String, List<String>> properties = conn.getRequestProperties();
|
||||
int code = conn.getResponseCode();
|
||||
if (code >= 300 && code <= 307 && code != 306 && code != 304) {
|
||||
String newURL = conn.getHeaderField("Location");
|
||||
conn.disconnect();
|
||||
|
||||
if (redirect > 20) {
|
||||
throw new IOException("Too much redirects");
|
||||
}
|
||||
|
||||
HttpURLConnection redirected = (HttpURLConnection) new URL(conn.getURL(), encodeLocation(newURL)).openConnection();
|
||||
properties.forEach((key, value) -> value.forEach(element -> redirected.addRequestProperty(key, element)));
|
||||
conn = redirected;
|
||||
++redirect;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
public static String doGet(URL url) throws IOException {
|
||||
return IOUtils.readFullyAsString(createConnection(url).getInputStream());
|
||||
}
|
||||
@@ -110,10 +175,12 @@ public final class NetworkUtils {
|
||||
}
|
||||
|
||||
public static String detectFileName(URL url) throws IOException {
|
||||
HttpURLConnection conn = createConnection(url);
|
||||
conn.connect();
|
||||
if (conn.getResponseCode() / 100 != 2)
|
||||
throw new IOException("Response code " + conn.getResponseCode());
|
||||
HttpURLConnection conn = resolveConnection(createConnection(url));
|
||||
int code = conn.getResponseCode();
|
||||
if (code == 404)
|
||||
throw new FileNotFoundException();
|
||||
if (code / 100 != 2)
|
||||
throw new IOException(url + ": response code " + conn.getResponseCode());
|
||||
|
||||
return detectFileName(conn);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
|
||||
public final class PropertyUtils {
|
||||
private PropertyUtils() {
|
||||
}
|
||||
|
||||
public static class PropertyHandle {
|
||||
public final WritableValue<Object> accessor;
|
||||
public final Observable observable;
|
||||
|
||||
public PropertyHandle(WritableValue<Object> accessor, Observable observable) {
|
||||
this.accessor = accessor;
|
||||
this.observable = observable;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public static Map<String, Function<Object, PropertyHandle>> getPropertyHandleFactories(Class<?> type) {
|
||||
Map<String, Method> collectionGetMethods = new LinkedHashMap<>();
|
||||
Map<String, Method> propertyMethods = new LinkedHashMap<>();
|
||||
for (Method method : type.getMethods()) {
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (method.getParameterCount() == 0
|
||||
&& !returnType.equals(void.class)) {
|
||||
String name = method.getName();
|
||||
if (name.endsWith("Property")) {
|
||||
String propertyName = name.substring(0, name.length() - "Property".length());
|
||||
if (!propertyName.isEmpty() && Property.class.isAssignableFrom(returnType)) {
|
||||
propertyMethods.put(propertyName, method);
|
||||
}
|
||||
} else if (name.startsWith("get")) {
|
||||
String propertyName = name.substring("get".length());
|
||||
if (!propertyName.isEmpty() &&
|
||||
(ObservableList.class.isAssignableFrom(returnType)
|
||||
|| ObservableSet.class.isAssignableFrom(returnType)
|
||||
|| ObservableMap.class.isAssignableFrom(returnType))) {
|
||||
propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1);
|
||||
collectionGetMethods.put(propertyName, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
propertyMethods.keySet().forEach(collectionGetMethods::remove);
|
||||
|
||||
Map<String, Function<Object, PropertyHandle>> result = new LinkedHashMap<>();
|
||||
propertyMethods.forEach((propertyName, method) -> {
|
||||
result.put(propertyName, instance -> {
|
||||
Property returnValue;
|
||||
try {
|
||||
returnValue = (Property<?>) method.invoke(instance);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return new PropertyHandle(returnValue, returnValue);
|
||||
});
|
||||
});
|
||||
|
||||
collectionGetMethods.forEach((propertyName, method) -> {
|
||||
result.put(propertyName, instance -> {
|
||||
Object returnValue;
|
||||
try {
|
||||
returnValue = method.invoke(instance);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
WritableValue<Object> accessor;
|
||||
if (returnValue instanceof ObservableList) {
|
||||
accessor = new WritableValue<Object>() {
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
((ObservableList) returnValue).setAll((List) value);
|
||||
}
|
||||
};
|
||||
} else if (returnValue instanceof ObservableSet) {
|
||||
accessor = new WritableValue<Object>() {
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
ObservableSet target = (ObservableSet) returnValue;
|
||||
target.clear();
|
||||
target.addAll((Set) value);
|
||||
}
|
||||
};
|
||||
} else if (returnValue instanceof ObservableMap) {
|
||||
accessor = new WritableValue<Object>() {
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object value) {
|
||||
ObservableMap target = (ObservableMap) returnValue;
|
||||
target.clear();
|
||||
target.putAll((Map) value);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return new PropertyHandle(accessor, (Observable) returnValue);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void copyProperties(Object from, Object to) {
|
||||
Class<?> type = from.getClass();
|
||||
while (!type.isInstance(to))
|
||||
type = type.getSuperclass();
|
||||
|
||||
getPropertyHandleFactories(type)
|
||||
.forEach((name, factory) -> {
|
||||
PropertyHandle src = factory.apply(from);
|
||||
PropertyHandle target = factory.apply(to);
|
||||
target.accessor.setValue(src.accessor.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
public static void attachListener(Object instance, InvalidationListener listener) {
|
||||
getPropertyHandleFactories(instance.getClass())
|
||||
.forEach((name, factory) -> {
|
||||
factory.apply(instance).observable.addListener(listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import javax.management.JMException;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.ObjectName;
|
||||
@@ -29,6 +26,8 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -135,30 +134,24 @@ public enum OperatingSystem {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static void setClipboard(String string) {
|
||||
ClipboardContent c = new ClipboardContent();
|
||||
c.putString(string);
|
||||
Clipboard.getSystemClipboard().setContent(c);
|
||||
}
|
||||
|
||||
public static void forceGC() {
|
||||
System.gc();
|
||||
System.runFinalization();
|
||||
System.gc();
|
||||
}
|
||||
|
||||
public static File getWorkingDirectory(String folder) {
|
||||
public static Path getWorkingDirectory(String folder) {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX:
|
||||
return new File(home, "." + folder + "/");
|
||||
return Paths.get(home, "." + folder);
|
||||
case WINDOWS:
|
||||
String appdata = System.getenv("APPDATA");
|
||||
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
|
||||
return Paths.get(Lang.nonNull(appdata, home), "." + folder);
|
||||
case OSX:
|
||||
return new File(home, "Library/Application Support/" + folder);
|
||||
return Paths.get(home, "Library", "Application Support", folder);
|
||||
default:
|
||||
return new File(home, folder + "/");
|
||||
return Paths.get(home, folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
@@ -27,6 +28,7 @@ import java.lang.reflect.Type;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@JsonAdapter(Platform.Serializer.class)
|
||||
public enum Platform {
|
||||
BIT_32("32"),
|
||||
BIT_64("64"),
|
||||
@@ -56,12 +58,6 @@ public enum Platform {
|
||||
* The json serializer to {@link Platform}.
|
||||
*/
|
||||
public static class Serializer implements JsonSerializer<Platform>, JsonDeserializer<Platform> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Platform t, Type type, JsonSerializationContext jsc) {
|
||||
if (t == null)
|
||||
|
||||
@@ -332,4 +332,6 @@ public class VersionNumber implements Comparable<VersionNumber> {
|
||||
public int hashCode() {
|
||||
return canonical.hashCode();
|
||||
}
|
||||
|
||||
public static Comparator<String> VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
|
||||
}
|
||||
@@ -118,3 +118,4 @@ No plugin API.
|
||||
|`-Dhmcl.self_integrity_check.disable=true`|Bypass the self integrity check when checking for update.|
|
||||
|`-Dhmcl.version.override=<version>`|Override the version number.|
|
||||
|`-Dhmcl.update_source.override=<url>`|Override the update source.|
|
||||
|`-Dhmcl.authlibinjector.location=<path>`|Use specified authlib-injector (instead of downloading one).|
|
||||
+57
-55
@@ -1,55 +1,57 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
|
||||
group 'org.jackhuang'
|
||||
version '3.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'idea'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
dependencies {
|
||||
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
|
||||
compile group: 'org.tukaani', name: 'xz', version: '1.8'
|
||||
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
|
||||
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
|
||||
compile group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
|
||||
|
||||
testCompile group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
|
||||
group 'org.jackhuang'
|
||||
version '3.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'idea'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
compileJava.options.encoding = "UTF-8"
|
||||
compileTestJava.options.encoding = "UTF-8"
|
||||
|
||||
dependencies {
|
||||
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
|
||||
compile group: 'org.tukaani', name: 'xz', version: '1.8'
|
||||
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
|
||||
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
|
||||
compile group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
|
||||
|
||||
testCompile group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
||||
Binary file not shown.
Reference in new issue
Block a user