Compare commits

...
49 Commits
Author SHA1 Message Date
huanghongxun 1c346c97a8 Add an option to skip JVM compatibility 2018-10-31 22:36:35 +08:00
yushijinhun d890b41380 Refactor Accounts to support login type name 2018-10-28 01:08:36 +08:00
yushijinhun cbcc89b48f Refactor RipplerContainer.StyleableProperties 2018-10-27 20:38:55 +08:00
yushijinhun 521bd00a70 Refactor TwoLineListItem.StyleableProperties 2018-10-27 20:16:05 +08:00
yushijinhun b4693423ef Logs the exception if auth server failed to be added 2018-10-27 19:38:30 +08:00
yushijinhun 594a2741d7 Remember the last used login type 2018-10-27 19:30:56 +08:00
yushijinhun a1d9b52b9d Refactor uiVersion 2018-10-27 19:10:02 +08:00
huanghongxun c9d825b6c1 Add a button for cleaning crash-reports and logs manually 2018-10-23 14:08:51 +08:00
huanghongxun 7dcd0eb616 Only display installer when game repository finished loading 2018-10-23 13:52:47 +08:00
huanghongxun e79a95b885 Fix NPE when version json missing "libraries" 2018-10-23 13:42:16 +08:00
huanghongxun 5d232c3f5e Fix NPE when app stopped 2018-10-23 13:36:45 +08:00
huanghongxun ee1c0d362d Fix NPE 2018-10-23 13:34:37 +08:00
huanghongxun aafcd57c82 Attempt to solve #481 2018-10-23 13:32:25 +08:00
huanghongxun 66a4566dc5 Update JFoenix.jar 2018-10-21 11:26:07 +08:00
huanghongxun 4c317f5b03 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-21 11:05:33 +08:00
huanghongxun 12d706a812 Don't show AuthenticationException class name 2018-10-21 11:05:27 +08:00
yushijinhun c006068580 Keep 'Next' button and 'Cancel' button on the same line 2018-10-20 21:33:34 +08:00
yushijinhun f8a838506d Replace 'Open logs folder' with 'Export logs'
Related: 8905f2e855
2018-10-20 17:52:06 +08:00
yushijinhun 270844bbb1 Start logging earlier 2018-10-20 17:44:43 +08:00
yushijinhun c9466cf8ef Move MINECRAFT_DIRECTORY & HMCL_DIRECTORY to Metadata 2018-10-20 17:43:35 +08:00
yushijinhun 7d6f1f98f1 Fix verison comparing
Related: fb9418dead
2018-10-20 15:56:14 +08:00
huanghongxun fb9418dead Fixed NPE 2018-10-17 13:51:49 +08:00
huanghongxun 4a1b06b80c Update I18N 2018-10-16 20:32:52 +08:00
huanghongxun 8905f2e855 Add a button to open log directory 2018-10-16 00:20:56 +08:00
huanghongxun 6c74364fa5 Fix writeText 2018-10-15 22:27:26 +08:00
huanghongxun f0720c1755 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-15 22:08:31 +08:00
huanghongxun f1acc1d2c1 Fix NPE 2018-10-15 13:05:32 +08:00
Haowei Wen 82bddbfa6e Merge pull request #474 from outloudvi/patch-1
Fix opening resourcepacks/ while opening saves/
2018-10-15 00:26:53 +08:00
Outvi V 841755fe56 Fix opening resourcepacks/ while opening saves/ 2018-10-14 22:39:22 +08:00
huanghongxun 340ee751e8 Suppress VersionNotFoundException 2018-10-14 19:10:04 +08:00
huanghongxun cf70f72f9c Fix #471 2018-10-14 19:04:27 +08:00
huanghongxun 16ea88a659 Fixed failing to download file when cached entry was removed by user 2018-10-14 19:04:21 +08:00
huanghongxun eaa6790013 Update welcome.png 2018-10-14 18:50:47 +08:00
huanghongxun af571d533d Maintain statement when no account 2018-10-14 12:45:52 +08:00
huanghongxun 004c3c588a Log navigation to catch bugs 2018-10-14 12:39:22 +08:00
huanghongxun 2a44167c22 Suppress NPE 2018-10-14 12:39:03 +08:00
huanghongxun cf6b735026 Removed JFXDrawer and JFXHamburger in JFoenix.jar 2018-10-14 12:20:03 +08:00
huanghongxun 5918b04b48 Prevent from calling register and fireEvent at the same time 2018-10-13 11:17:16 +08:00
huanghongxun 8004cb3a64 Change default red to darker one 2018-10-11 22:26:50 +08:00
huanghongxun 9981423927 Change separator fill in title bar and launch button to reduce color difference 2018-10-11 22:20:41 +08:00
huanghongxun 0e5250dfb3 Satisfy requirements metioned in #469 2018-10-11 22:06:34 +08:00
huanghongxun 2dc956b357 Fix request mojang server for skin info every time 2018-10-11 22:05:24 +08:00
huanghongxun b8bd0a08e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-11 14:05:35 +08:00
huanghongxun fb31eebcf2 Add a warning that forge 2760 is not compatible with liteloader 2018-10-11 14:04:52 +08:00
huanghongxun 3c8689a3f1 Close launching progress dialog when game crashed before opening window 2018-10-11 13:51:37 +08:00
huanghongxun 2957d66dd6 Add test game button in versions page 2018-10-11 13:41:11 +08:00
huanghongxun 6f6efe0874 Rename DecoratorControl to Decorator 2018-10-11 13:32:39 +08:00
huanghongxun 355fc04069 Drop modpack file to install 2018-10-11 13:32:11 +08:00
huanghongxun b4738f383b Fix not on FX thread 2018-10-11 12:59:06 +08:00
67 changed files with 1043 additions and 590 deletions

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
# 公告
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
我们发现,对于不经常检查邮箱的人来说,GitHub issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
# 问题提交
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
* 启动器版本:
* 操作系统:
* Java 版本:
* 错误截图(最好请将整个电脑屏幕的截图发上来):
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(在启动器设置中打开日志文件夹并将文件夹内所有文件打包发在这里):
* 问题描述(如何触发问题):
@@ -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;
@@ -55,7 +54,8 @@ public final class Launcher extends Application {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
try {
// 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);
@@ -66,27 +66,21 @@ public final class Launcher extends Application {
UpdateChecker.init();
primaryStage.show();
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
});
}
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 +137,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;
@@ -42,6 +44,8 @@ public final class Main {
checkDirectoryPath();
checkDSTRootCAX3();
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();
}
@@ -93,6 +93,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");
}
@@ -247,6 +247,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;
@@ -294,8 +299,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;
}
}
@@ -347,6 +354,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.
@@ -354,12 +362,24 @@ 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 and above will crash game with LiteLoader.
if (!flag) {
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it -> VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2760") >= 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();
}
@@ -458,6 +478,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;
@@ -487,28 +540,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();
}
}
@@ -517,6 +549,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,7 +23,7 @@ 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;
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.Schedulers;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
@@ -61,31 +62,40 @@ 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,
new AuthlibInjectorDownloader(Metadata.HMCL_DIRECTORY, DownloadProviders::getDownloadProvider)::getArtifactInfo,
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));
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);
}
static String accountId(Account account) {
@@ -141,7 +151,7 @@ public final class Accounts {
static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
storage.put("type", getLoginType(getAccountFactory(account)));
return storage;
}
@@ -255,18 +265,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;
@@ -50,6 +50,8 @@ 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)
@@ -86,7 +88,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();
@@ -145,6 +147,22 @@ 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() {
@@ -440,4 +458,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;
}
}
@@ -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");
@@ -29,6 +29,8 @@ 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 +39,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;
}
/**
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -146,15 +147,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() {
@@ -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() {
@@ -43,7 +43,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;
@@ -277,6 +277,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 +511,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);
@@ -558,6 +576,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 +614,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);
}
}
@@ -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,78 @@
package org.jackhuang.hmcl.ui.download;
import javafx.application.Platform;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
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.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
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 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_message", i18n("install.failed"));
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);
}
}
@@ -21,8 +21,6 @@ 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.Schedulers;
@@ -30,23 +28,19 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
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 +64,12 @@ 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"));
switch (type) {
case 0: return finishVersionDownloadingAsync(settings);
case 1: return finishModpackInstallingAsync(settings);
default: return null;
}
return finishVersionDownloadingAsync(settings);
}
@Override
@@ -100,14 +77,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());
}
@@ -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();
@@ -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;
@@ -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,7 +239,7 @@ 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);
@@ -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%;
}
+25 -3
View File
@@ -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;
@@ -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.
@@ -139,7 +138,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 +166,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,7 +219,7 @@ 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
@@ -290,6 +292,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
@@ -326,7 +329,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
@@ -378,6 +385,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=可查閱資料包、整合包製作指南等內容。
@@ -139,7 +138,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 +166,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,7 +219,7 @@ 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 整合包相關檔案
@@ -290,6 +292,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
@@ -326,7 +329,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=使用系統代理
@@ -378,6 +385,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=可查阅数据包、整合包制作指南等内容。
@@ -139,7 +138,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 +166,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,7 +219,7 @@ 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 整合包相关文件
@@ -290,6 +292,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
@@ -326,7 +329,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=使用系统代理
@@ -378,6 +385,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

@@ -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) {
@@ -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);
@@ -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);
@@ -222,8 +222,14 @@ public class FileDownloadTask extends Task {
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());
}
@@ -96,11 +96,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());
}
@@ -164,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();
@@ -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");
@@ -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);
}
@@ -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);
}
}
}
@@ -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);
}
BIN
View File
Binary file not shown.