Compare commits

..
116 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
huanghongxun b12711969c Rewrite encode location 2018-10-10 16:37:04 +08:00
huanghongxun 94727353c4 Fix response code 400 when downloading mods from curse 2018-10-10 15:27:27 +08:00
huanghongxun 5eb7449b74 Delete old files if etag was updated 2018-10-10 01:36:31 +08:00
huanghongxun dae41dfb30 Add an entrance directly to add authlib-injector server 2018-10-09 22:15:46 +08:00
huanghongxun 07a701e572 Cache asset indexes by ETag cache 2018-10-09 20:22:54 +08:00
huanghongxun 5323aaed6d ETag caching 2018-10-09 20:16:16 +08:00
huanghongxun daf659373f Removed static login method 2018-10-08 21:23:01 +08:00
huanghongxun 0eb81c29df Update I18N 2018-10-08 01:31:02 +08:00
huanghongxun 8aea89c103 Login when launcher opened to reduce launching time 2018-10-08 01:27:48 +08:00
huanghongxun f1d97454c2 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-06 12:42:50 +08:00
huanghongxun ffb6a45e28 Remove toLowerCase 2018-10-06 12:42:30 +08:00
huanghongxun f0d1c9b361 Merge pull request #468 from yushijinhun/authlib-injector-ali
支持 authlib-injector API 地址指示
2018-10-06 12:38:04 +08:00
huanghongxun aedd4c290e Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-06 12:20:48 +08:00
huanghongxun 749ecfe37f Fix #467 2018-10-06 12:20:42 +08:00
yushijinhun c51da28e31 Add error message for authlib-injector download failure 2018-10-06 12:06:31 +08:00
yushijinhun f3dca75a10 Do not check for update of authlib-injector each time 2018-10-06 11:56:02 +08:00
yushijinhun c3dcb0292c Support -Dauthlibinjector.side=client argument
See yushijinhun/authlib-injector#16
2018-10-06 11:38:55 +08:00
yushijinhun 2a42ecda78 Support authlib-injector ALI
See yushijinhun/authlib-injector#18
2018-10-06 00:47:36 +08:00
huanghongxun 08be1896ee Forbid adding profiles with already existent name 2018-10-05 23:04:56 +08:00
huanghongxun 6b3152c4b5 Try to fix #462. 2018-10-05 22:27:19 +08:00
yushijinhun 19ea27dece emmm... Forgot to add DialogAware.java
Related: b9e26a4112
2018-10-04 12:48:29 +08:00
yushijinhun b9e26a4112 Add DialogAware & Improve add auth server dialog 2018-10-04 12:45:04 +08:00
yushijinhun a71aea8e43 Update translation: account.failed.server_response_malformed 2018-10-04 11:56:12 +08:00
yushijinhun 1b1f22b9fa Fix Java list display error on JDK10
如何重现:
1. 关闭当前选中版本的版本特定设置
2. 重启HMCL
3. 从主界面进入版本设置
4. 打开版本特定设置
5. 展开Java列表

问题:Java列表中的项目除了自定义项外,在外观上仍保持被禁用的状态(但是可以点击)。

原因未知。

此问题只在Java10上出现。
若在进入版本设置界面前,版本特定设置已经被打开,则不会出现此问题。

已知Java项目的styleClass、pseudoClass、disable、disabled属性均正常。

试过但没用的解决方法:
1. 删除HMCL全部CSS
2. 将Java列表默认展开
3. 使用JavaFX原生组件替代JFoenix组件

本提交中的代码由于玄学原因,可以解决此问题。
2018-10-03 22:40:17 +08:00
yushijinhun 705a5659b6 Fix cursor doesn't change when hanging on proxy settings 2018-10-03 21:41:22 +08:00
yushijinhun 4121921aca Fix compile errors 2018-10-03 19:20:47 +08:00
yushijinhun 8cb4ca0e8b Remove Settings.font 2018-10-03 17:17:16 +08:00
yushijinhun 6205937a10 Refactor update channel selecting 2018-10-03 17:15:42 +08:00
yushijinhun 2f522b744f Add MappedProperty & reservedSelectedPropertyFor(CheckBox) 2018-10-03 17:09:12 +08:00
yushijinhun b8a84f831a Refactor proxy settings 2018-10-03 16:43:48 +08:00
yushijinhun 5f9490d780 Add SafeStringConverter & Refactor font settings 2018-10-03 16:38:21 +08:00
huanghongxun a1c5b87b55 Fix crash when no game installed 2018-10-03 15:55:38 +08:00
yushijinhun 641e308c53 Remove right graphic of Game List&Launcher Settings 2018-10-03 13:40:09 +08:00
yushijinhun 64bc70a15d Fix Account/VersionListItem can be unselected 2018-10-03 13:01:30 +08:00
yushijinhun acadbdb5c9 Fix potential NPE
Related: 3323ea2be0
2018-10-03 12:10:19 +08:00
yushijinhun 3323ea2be0 Remove JFXButton in AdvancedListItem 2018-10-03 12:02:17 +08:00
huanghongxun 45f55b2075 Adjust looking of LeftPaneController 2018-10-03 01:32:55 +08:00
yushijinhun 372005dae1 Partially revert 223c2bc7c7 2018-10-02 22:07:57 +08:00
yushijinhun 2c7ca6d354 Remove some methods in Lang 2018-10-02 22:01:42 +08:00
yushijinhun e21f315239 Fix VersionNumber.equals() and hashCode() may not be consistent 2018-10-02 21:25:29 +08:00
yushijinhun 223c2bc7c7 Change update.bubble.subtitle to 'Click here to upgrade' 2018-10-02 21:15:14 +08:00
yushijinhun 3facbba4ee Use databind for update bubble 2018-10-02 21:15:14 +08:00
yushijinhun b8bf56c8c3 Add pseudo class 'first' for 'options-list-item'
Related: caf85d2a92
2018-10-02 20:38:41 +08:00
huanghongxun 2fa08e0a70 Change default profile directory to ".minecraft" 2018-10-02 18:34:09 +08:00
huanghongxun 8e1e13058b Fix #463 2018-10-02 18:33:48 +08:00
huanghongxun 92520e46a3 Remove settings entrance in main page 2018-10-02 18:11:52 +08:00
huanghongxun 6ae6c9c971 Name changes 2018-10-02 18:11:13 +08:00
huanghongxun caf85d2a92 Fix showing a gray horizontal line in component list cell 2018-10-02 18:06:24 +08:00
huanghongxun 030b577dee Add an explicit entry to game list page 2018-10-02 12:44:36 +08:00
huanghongxun 83516ba679 Fix #461 2018-10-02 12:15:52 +08:00
huanghongxun c8f74ee8e3 Add test game in game list page 2018-10-02 11:47:53 +08:00
yushijinhun c50e41ab91 Use setItems in DatapackListPage 2018-10-01 21:01:43 +08:00
yushijinhun c03a0feb75 Refactor ProfileList 2018-10-01 20:59:11 +08:00
yushijinhun 7da5b8fbc8 Add AutomatedToggleGroup & Refactor AccountList 2018-10-01 20:39:00 +08:00
yushijinhun 83f7e61d37 Add selectedItemPropertyFor(ToggleGroup) 2018-10-01 20:03:57 +08:00
yushijinhun 86765275c8 Make SelectionModelSelectedItemProperty a util class 2018-10-01 15:38:12 +08:00
yushijinhun a01e15d841 Add MultiStepBinding&ReadWriteComposedProperty 2018-10-01 15:35:39 +08:00
yushijinhun 5e5db332d0 Remove 'Downloads' paragraph in readme, as GitHub Releases is available 2018-10-01 11:29:12 +08:00
huanghongxun 2de2f45124 Fix always showing horizontal scrollbar of game list popup in main page 2018-09-30 20:07:57 +08:00
huanghongxun b1b3eb9369 Fix always using custom common directory 2018-09-30 20:07:57 +08:00
yushijinhun 507665cd91 Fix #460 2018-09-30 19:45:39 +08:00
yushijinhun c779172cc2 Fix custom toggle is not selected when using customized java 2018-09-30 19:30:22 +08:00
yushijinhun 684813e131 Refactor Java lookup 2018-09-30 19:18:29 +08:00
huanghongxun 76ef47efc4 Suppress exceptions thrown when checking Java installation 2018-09-30 13:22:42 +08:00
huanghongxun 71a8fa0e66 Display platform of java installations 2018-09-28 19:30:41 +08:00
huanghongxun 48d98b2424 Conceal NPE 2018-09-26 19:07:09 +08:00
huanghongxun 5a0e17b121 Fix crash when user set common directory to an invalid path 2018-09-26 19:00:31 +08:00
133 changed files with 3505 additions and 1831 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):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(在启动器设置中打开日志文件夹并将文件夹内所有文件打包发在这里):
* 问题描述(如何触发问题):
+5 -2
View File
@@ -1,5 +1,6 @@
plugins {
id 'com.github.johnrengelman.shadow' version '2.0.4'
id 'com.github.johnrengelman.shadow' version '4.0.0'
id 'application'
}
import java.nio.file.FileSystems
@@ -23,6 +24,8 @@ if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
compile project(":HMCLCore")
compile rootProject.files("lib/JFoenix.jar")
@@ -75,7 +78,7 @@ def repack(File file) {
jar {
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class': 'org.jackhuang.hmcl.Main',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version
}
@@ -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");
}
@@ -150,7 +160,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public Image getVersionIconImage(String id) {
if (id == null)
if (id == null || !isLoaded())
return new Image("/assets/img/grass.png");
Version version = getVersion(id);
@@ -61,6 +61,8 @@ public class HMCLModpackExportTask extends Task {
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
if (path.isEmpty())
return true;
for (String s : blackList)
if (path.equals(s))
return false;
@@ -36,7 +36,7 @@ public final class HMCLModpackManager {
public static final List<String> MODPACK_BLACK_LIST = Lang.immutableListOf(
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"pack.json", "launcher.jar", "hmclmc.log", // HMCL
"pack.json", "launcher.jar", "hmclmc.log", "cache", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -40,7 +41,10 @@ import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
@@ -50,6 +54,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -62,39 +67,58 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LauncherHelper {
public static final LauncherHelper INSTANCE = new LauncherHelper();
private LauncherHelper(){}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
private final Profile profile;
private final Account account;
private final String selectedVersion;
private File scriptFile;
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
this.account = Objects.requireNonNull(account);
this.selectedVersion = Objects.requireNonNull(selectedVersion);
this.setting = profile.getVersionSetting(selectedVersion);
this.launcherVisibility = setting.getLauncherVisibility();
this.showLogs = setting.isShowLogs();
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
public void launch(Profile profile, Account account, String selectedVersion, File scriptFile) {
if (account == null)
throw new IllegalArgumentException("No account");
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
showLogs = true;
}
public void launch() {
Logging.LOG.info("Launching game version: " + selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile));
Schedulers.newThread().schedule(this::launch0);
});
} catch (InterruptedException ignore) {
}
});
}
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
public void makeLaunchScript(File scriptFile) {
this.scriptFile = Objects.requireNonNull(scriptFile);
launch();
}
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
@@ -134,7 +158,7 @@ public final class LauncherHelper {
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
setting.getLauncherVisibility() == LauncherVisibility.CLOSE
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
@@ -154,7 +178,7 @@ public final class LauncherHelper {
if (scriptFile == null) {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(it -> {
@@ -193,7 +217,10 @@ public final class LauncherHelper {
if (ex != null) {
String message;
if (ex instanceof CurseCompletionException) {
message = i18n("modpack.type.curse.error");
if (ex.getCause() instanceof FileNotFoundException)
message = i18n("modpack.type.curse.not_found");
else
message = i18n("modpack.type.curse.error");
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
@@ -220,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;
@@ -238,7 +270,7 @@ public final class LauncherHelper {
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
@@ -263,20 +295,22 @@ public final class LauncherHelper {
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
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;
}
}
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0.51");
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51");
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
@@ -293,7 +327,7 @@ public final class LauncherHelper {
final JavaVersion java32 = java;
// First find if same java version but whose platform is 64-bit installed.
Optional<JavaVersion> java64 = JavaVersion.getJREs().stream()
Optional<JavaVersion> java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
@@ -302,7 +336,7 @@ public final class LauncherHelper {
final boolean java8requiredFinal = java8required, newJavaRequiredFinal = newJavaRequired;
// Then find if other java version which satisfies requirements installed.
java64 = JavaVersion.getJREs().stream()
java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> {
if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8;
@@ -320,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.
@@ -327,21 +362,28 @@ 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();
}
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
@@ -351,8 +393,8 @@ public final class LauncherHelper {
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
}
private void checkExit(LauncherVisibility v) {
switch (v) {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.getStage()::show);
break;
@@ -401,7 +443,6 @@ public final class LauncherHelper {
private final VersionSetting setting;
private final Map<String, String> forbiddenTokens;
private final LauncherVisibility visibility;
private ManagedProcess process;
private boolean lwjgl;
private LogWindow logWindow;
@@ -422,7 +463,6 @@ public final class LauncherHelper {
pair(authInfo.getUsername(), "<player>")
);
visibility = setting.getLauncherVisibility();
logs = new LinkedList<>();
}
@@ -430,7 +470,7 @@ public final class LauncherHelper {
public void setProcess(ManagedProcess process) {
this.process = process;
if (setting.isShowLogs())
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
@@ -438,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;
@@ -453,7 +526,7 @@ public final class LauncherHelper {
if (logs.size() > config().getLogLines())
logs.removeFirst();
if (setting.isShowLogs()) {
if (showLogs) {
try {
latch.await();
logWindow.waitForLoaded();
@@ -467,28 +540,7 @@ public final class LauncherHelper {
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
lwjgl = true;
switch (visibility) {
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();
}
}
@@ -497,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();
@@ -518,8 +573,14 @@ public final class LauncherHelper {
});
});
checkExit(visibility);
checkExit();
}
}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
}
@@ -23,9 +23,10 @@ import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
@@ -35,8 +36,10 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
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;
@@ -59,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) {
@@ -91,9 +103,9 @@ public final class Accounts {
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(accounts);
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@@ -139,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;
}
@@ -181,11 +193,20 @@ public final class Accounts {
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
selectedAccount.set(
accounts.stream()
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null));
Account selected = accounts.stream()
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null);
selectedAccount.set(selected);
Schedulers.io().schedule(() -> {
if (selected != null)
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
}
});
}
public static ObservableList<Account> getAccounts() {
@@ -244,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();
@@ -95,7 +97,7 @@ public final class Config implements Cloneable, Observable {
private BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
@SerializedName("proxyType")
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.DIRECT);
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.HTTP);
@SerializedName("proxyHost")
private StringProperty proxyHost = new SimpleStringProperty();
@@ -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,12 +18,14 @@
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;
import javafx.beans.property.*;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -139,21 +141,23 @@ public final class Profile implements Observable {
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
this.selectedVersion.addListener(o -> checkSelectedVersion());
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion()));
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
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() {
@@ -19,20 +19,20 @@ package org.jackhuang.hmcl.setting;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
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;
import java.io.File;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
@@ -43,8 +43,6 @@ public final class Profiles {
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
private static InvalidationListener listener = o -> loadVersion();
private Profiles() {
}
@@ -65,16 +63,14 @@ public final class Profiles {
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
profiles.addListener(onInvalidating(this::invalidated));
this.addListener(this::change);
}
@Override
protected void invalidated() {
Profile profile = get();
if (!initialized)
return;
if (get() != null)
get().removeListener(listener);
Profile profile = get();
if (profiles.isEmpty()) {
if (profile != null) {
@@ -88,25 +84,27 @@ public final class Profiles {
}
}
if (!initialized)
return;
config().setSelectedProfile(profile == null ? "" : profile.getName());
loadVersion();
}
private void change(ObservableValue<? extends Profile> observableValue, Profile oldProfile, Profile newProfile) {
if (oldProfile != null)
oldProfile.selectedVersionProperty().removeListener(listener);
if (newProfile != null)
newProfile.selectedVersionProperty().addListener(listener);
if (profile != null) {
if (profile.getRepository().isLoaded())
selectedVersion.bind(profile.selectedVersionProperty());
else {
selectedVersion.unbind();
selectedVersion.set(null);
// bind when repository was reloaded.
profile.getRepository().refreshVersionsAsync().start();
}
} else {
selectedVersion.unbind();
selectedVersion.set(null);
}
}
};
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));
}
}
@@ -154,19 +152,23 @@ public final class Profiles {
// Platform.runLater is necessary or profiles will be empty
// since checkProfiles adds 2 base profile later.
Platform.runLater(() -> {
initialized = true;
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
initialized = true;
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
if (selectedProfile.get() != null && selectedProfile.get().getRepository() == event.getSource())
loadVersion();
Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
selectedVersion.bind(profile.selectedVersionProperty());
for (Consumer<Profile> listener : versionsListeners)
listener.accept(profile);
}
});
});
}
@@ -197,14 +199,17 @@ public final class Profiles {
return selectedVersion.getReadOnlyProperty();
}
// Guaranteed that the repository is loaded.
public static String getSelectedVersion() {
return selectedVersion.get();
}
private static void loadVersion() {
Profile profile = selectedProfile.get();
if (profile == null || !profile.getRepository().isLoaded()) return;
JFXUtilities.runInFX(() ->
selectedVersion.set(profile.getSelectedVersion()));
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
listener.accept(profile);
versionsListeners.add(listener);
}
}
@@ -17,11 +17,14 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import javafx.beans.binding.Bindings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class Settings {
@@ -49,20 +52,19 @@ public class Settings {
Profiles.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(config().commonDirectoryProperty());
}
public Font getFont() {
return Font.font(config().getFontFamily(), config().getFontSize());
}
public void setFont(Font font) {
config().setFontFamily(font.getFamily());
config().setFontSize(font.getSize());
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
String str = getCommonDirectory();
try {
Paths.get(str);
return str;
} catch (InvalidPathException e) {
return getDefaultCommonDirectory();
}
}, config().commonDirectoryProperty(), config().commonDirTypeProperty()));
}
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;
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -276,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() {
@@ -455,12 +473,12 @@ public final class VersionSetting {
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(new File(getJavaDir()));
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
} catch (IOException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJREs().stream()
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
@@ -493,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);
@@ -557,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());
@@ -594,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());
@@ -44,14 +44,14 @@ import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SelectionModelSelectedItemProperty;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
@@ -90,13 +90,13 @@ public final class FXUtils {
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onChange(value, consumer);
consumer.accept(value.getValue());
onChange(value, consumer);
}
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onWeakChange(value, consumer);
consumer.accept(value.getValue());
onWeakChange(value, consumer);
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
@@ -223,7 +223,11 @@ public final class FXUtils {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
try {
loader.load();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static void installTooltip(Node node, String tooltip) {
@@ -348,7 +352,7 @@ public final class FXUtils {
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @deprecated Use {@link SelectionModelSelectedItemProperty#selectedItemPropertyFor(ComboBox)}
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -29,18 +30,17 @@ import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.ui.versions.Versions;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
@@ -54,22 +54,39 @@ public final class LeftPaneController extends AdvancedListBox {
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
}
});
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage()));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
IconedItem launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), "iconed-item");
launcherSettingsItem.getLabel().setText(i18n("settings.launcher"));
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(new Image("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(new Image("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
this
.startCategory(i18n("account").toUpperCase())
.add(accountListItem)
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
@@ -22,26 +22,43 @@ import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public abstract void add();
public ListProperty<T> itemsProperty() {
return items;
}
public BooleanProperty loadingProperty() {
return loading;
}
@Override
protected Skin<?> createDefaultSkin() {
return new ListPageSkin(this);
}
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
}
@@ -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);
}
@@ -33,7 +33,6 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
@@ -176,7 +175,7 @@ public final class LogWindow extends Stage {
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", Settings.instance().getFont().getSize() + "px \"" + Settings.instance().getFont().getFamily() + "\""));
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
@@ -17,45 +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.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
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.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
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.versioning.VersionNumber;
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 {
@@ -63,128 +55,143 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final PopupMenu menu = new PopupMenu();
private final JFXPopup popup = new JFXPopup(menu);
private InvalidationListener updateListener;
@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 closeButton;
@FXML
private Label lblCurrentGame;
@FXML
private Label lblIcon;
@FXML
private TwoLineListItem lblLatestVersion;
@FXML
private Rectangle separator;
private JFXButton menuButton;
private Profile profile;
{
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(400);
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());
closeButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
closeButton.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));
updateListener = any -> {
if (UpdateChecker.isOutdated()) {
lblLatestVersion.setTitle(i18n("update.bubble.title", UpdateChecker.getLatestVersion().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));
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());
hBox.getChildren().setAll(lblIcon, prompt);
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
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);
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());
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
lblCurrentGame.setText(version);
} else {
lblCurrentGame.setText(i18n("version.empty"));
}
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
loadVersions((HMCLGameRepository) event.getSource());
});
Profiles.selectedProfileProperty().addListener((a, b, newValue) -> profile = newValue);
profile = Profiles.getSelectedProfile();
if (profile != null) {
if (profile.getRepository().isLoaded())
loadVersions(profile.getRepository());
else
profile.getRepository().refreshVersionsAsync().start();
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(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> {
StackPane pane = new StackPane();
GameItem item = new GameItem(repository.getProfile(), version.getId());
pane.getChildren().setAll(item);
pane.getStyleClass().setAll("menu-container");
item.setMouseTransparent(true);
RipplerContainer container = new RipplerContainer(pane);
container.setOnMouseClicked(e -> {
repository.getProfile().setSelectedVersion(version.getId());
popup.hide();
});
return container;
})
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile())
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) {
@@ -193,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();
}
@@ -205,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;
}
}
@@ -158,4 +158,8 @@ public final class SVG {
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
}
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
}
}
@@ -30,30 +30,40 @@ 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.Lang;
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.Objects;
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.SelectionModelSelectedItemProperty.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelectedPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
public final class SettingsPage extends SettingsView implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("settings.launcher"));
private ObjectProperty<Proxy.Type> selectedProxyType;
private InvalidationListener updateListener;
public SettingsPage() {
@@ -64,24 +74,19 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
// ====
cboFont.initValue(Settings.instance().getFont());
cboFont.valueProperty().addListener((a, b, newValue) -> {
Font font = Font.font(newValue, Settings.instance().getFont().getSize());
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
});
// ==== Font ====
cboFont.valueProperty().bindBidirectional(config().fontFamilyProperty());
txtFontSize.setText(Double.toString(Settings.instance().getFont().getSize()));
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
txtFontSize.textProperty().addListener((a, b, newValue) -> {
if (txtFontSize.validate()) {
Font font = Font.font(Settings.instance().getFont().getFamily(), Double.parseDouble(newValue));
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
}
});
txtFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
SafeStringConverter.fromFiniteDouble()
.restrict(it -> it > 0)
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtFontSize)));
lblDisplay.setStyle("-fx-font: " + Settings.instance().getFont().getSize() + " \"" + Settings.instance().getFont().getFamily() + "\";");
lblDisplay.fontProperty().bind(Bindings.createObjectBinding(
() -> Font.font(config().getFontFamily(), config().getFontSize()),
config().fontFamilyProperty(), config().fontSizeProperty()));
// ====
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
@@ -97,40 +102,15 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkDisableProxy.setSelected(!config().hasProxy());
chkDisableProxy.selectedProperty().addListener(o -> config().setHasProxy(!chkDisableProxy.isSelected()));
config().hasProxyProperty().addListener(o -> chkDisableProxy.setSelected(!config().hasProxy()));
reservedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
invalidated();
}
@Override
protected void invalidated() {
Proxy.Type type = Objects.requireNonNull(get());
if (type == Proxy.Type.DIRECT) {
set(Proxy.Type.HTTP); // HTTP by default
} else {
chkProxyHttp.setSelected(type == Proxy.Type.HTTP);
chkProxySocks.setSelected(type == Proxy.Type.SOCKS);
}
}
};
selectedProxyType.bindBidirectional(config().proxyTypeProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyHttp.setUserData(Proxy.Type.HTTP);
chkProxyHttp.setToggleGroup(proxyConfigurationGroup);
chkProxySocks.setUserData(Proxy.Type.SOCKS);
chkProxySocks.setToggleGroup(proxyConfigurationGroup);
proxyConfigurationGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
selectedProxyType.set((Proxy.Type) toggle.getUserData());
}
}));
selectedItemPropertyFor(proxyConfigurationGroup, Proxy.Type.class).bindBidirectional(config().proxyTypeProperty());
// ====
fileCommonLocation.loadChildren(Arrays.asList(
@@ -175,27 +155,12 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
lblUpdateNote.setWrappingWidth(470);
ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<UpdateChannel>() {
@Override
protected void invalidated() {
UpdateChannel updateChannel = Objects.requireNonNull(get());
chkUpdateDev.setSelected(updateChannel == UpdateChannel.DEVELOPMENT);
chkUpdateStable.setSelected(updateChannel == UpdateChannel.STABLE);
}
};
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
updateChannelGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
updateChannel.set((UpdateChannel) toggle.getUserData());
}
}));
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class).bindBidirectional(config().updateChannelProperty());
// ====
// ==== Background ====
@@ -247,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 {
@@ -240,7 +239,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyHost = new JFXTextField();
txtProxyHost.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyHost, 1);
GridPane.setColumnIndex(txtProxyHost, 1);
gridPane.getChildren().add(txtProxyHost);
@@ -256,7 +254,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyPort = new JFXTextField();
txtProxyPort.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyPort, 2);
GridPane.setColumnIndex(txtProxyPort, 1);
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
@@ -293,7 +290,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyUsername = new JFXTextField();
txtProxyUsername.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyUsername, 0);
GridPane.setColumnIndex(txtProxyUsername, 1);
authPane.getChildren().add(txtProxyUsername);
@@ -308,7 +304,6 @@ public abstract class SettingsView extends StackPane {
{
txtProxyPassword = new JFXPasswordField();
txtProxyPassword.getStyleClass().setAll("fit-width");
GridPane.setRowIndex(txtProxyPassword, 1);
GridPane.setColumnIndex(txtProxyPassword, 1);
authPane.getChildren().add(txtProxyPassword);
@@ -337,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);
}
{
@@ -481,4 +491,5 @@ public abstract class SettingsView extends StackPane {
protected abstract void onUpdate();
protected abstract void onHelp();
protected abstract void onExportLogs();
}
@@ -24,7 +24,9 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -36,21 +38,21 @@ public class AccountAdvancedListItem extends AdvancedListItem {
protected void invalidated() {
Account account = get();
if (account == null) {
titleProperty().set(i18n("account.missing"));
subtitleProperty().set(i18n("account.missing.add"));
imageProperty().set(new Image("/assets/img/craft_table.png"));
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
setImage(new Image("/assets/img/craft_table.png"));
} else {
titleProperty().set(account.getCharacter());
subtitleProperty().set(accountSubtitle(account));
setTitle(account.getCharacter());
setSubtitle(accountSubtitle(account));
final int scaleRatio = 4;
Image defaultSkin = AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
imageProperty().set(AccountHelper.getHead(defaultSkin, scaleRatio));
setImage(AccountHelper.getHead(defaultSkin, scaleRatio));
if (account instanceof YggdrasilAccount) {
AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
imageProperty().set(AccountHelper.getHead(image, scaleRatio));
setImage(AccountHelper.getHead(image, scaleRatio));
});
}
}
@@ -58,6 +60,7 @@ public class AccountAdvancedListItem extends AdvancedListItem {
};
public AccountAdvancedListItem() {
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
public ObjectProperty<Account> accountProperty() {
@@ -19,48 +19,22 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class AccountList extends ListPage<AccountListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(i18n("account.manage"));
private ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
itemsProperty().addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
Account selected = get();
itemsProperty().forEach(item -> item.selectedProperty().set(item.getAccount() == selected));
}
};
private final ListProperty<Account> accounts = new SimpleListProperty<>(FXCollections.observableArrayList());
private ToggleGroup toggleGroup;
private final ObservableList<AccountListItem> accountItems;
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage"));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
public AccountList() {
toggleGroup = new ToggleGroup();
accountItems = MappedObservableList.create(
accountsProperty(),
account -> new AccountListItem(toggleGroup, account));
itemsProperty().bindContent(accountItems);
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null || toggle.getUserData() == null) return;
selectedAccount.set(((AccountListItem) toggle.getUserData()).getAccount());
});
setItems(MappedObservableList.create(accounts, AccountListItem::new));
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
}
public ObjectProperty<Account> selectedAccountProperty() {
@@ -18,9 +18,8 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.*;
import javafx.scene.control.Control;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
@@ -33,19 +32,19 @@ import org.jackhuang.hmcl.task.Schedulers;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItem extends Control {
public class AccountListItem extends RadioButton {
private final Account account;
private final ToggleGroup toggleGroup;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final BooleanProperty selected = new SimpleBooleanProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
public AccountListItem(ToggleGroup toggleGroup, Account account) {
public AccountListItem(Account account) {
this.account = account;
this.toggleGroup = toggleGroup;
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());
@@ -56,7 +55,6 @@ public class AccountListItem extends Control {
else
title.set(account.getUsername() + " - " + account.getCharacter());
subtitle.set(subtitleString.toString());
selected.set(Accounts.selectedAccountProperty().get() == account);
final int scaleRatio = 4;
Image image = account instanceof YggdrasilAccount ?
@@ -70,30 +68,6 @@ public class AccountListItem extends Control {
return new AccountListItemSkin(this);
}
public ToggleGroup getToggleGroup() {
return toggleGroup;
}
public Account getAccount() {
return account;
}
public StringProperty titleProperty() {
return title;
}
public StringProperty subtitleProperty() {
return subtitle;
}
public BooleanProperty selectedProperty() {
return selected;
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void refresh() {
if (account instanceof YggdrasilAccount) {
// progressBar.setVisible(true);
@@ -113,4 +87,44 @@ public class AccountListItem extends Control {
public void remove() {
Accounts.getAccounts().remove(account);
}
public Account getAccount() {
return account;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public StringProperty titleProperty() {
return title;
}
public String getSubtitle() {
return subtitle.get();
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public StringProperty subtitleProperty() {
return subtitle;
}
public Image getImage() {
return image.get();
}
public void setImage(Image image) {
this.image.set(image);
}
public ObjectProperty<Image> imageProperty() {
return image;
}
}
@@ -26,7 +26,6 @@ import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -41,25 +40,23 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
BorderPane root = new BorderPane();
JFXRadioButton chkSelected = new JFXRadioButton();
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.setUserData(skinnable);
chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty());
chkSelected.setToggleGroup(skinnable.getToggleGroup());
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
imageViewContainer.getChildren().setAll(imageView);
TwoLineListItem item = new TwoLineListItem();
BorderPane.setAlignment(item, Pos.CENTER);
@@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
@@ -65,6 +66,8 @@ public class AddAccountPane extends StackPane {
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept;
@FXML private JFXButton btnAddServer;
@FXML private JFXButton btnManageServer;
@FXML private SpinnerPane acceptPane;
public AddAccountPane() {
@@ -77,20 +80,30 @@ 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());
cboServers.getItems().addListener(onInvalidating(this::checkIfNoServer));
checkIfNoServer();
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());
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
@@ -188,6 +201,11 @@ public class AddAccountPane extends StackPane {
Controllers.navigate(Controllers.getServersPage());
}
@FXML
private void onAddInjecterServer() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
private class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
@@ -278,6 +296,10 @@ public class AddAccountPane extends StackPane {
return i18n("account.failed.invalid_password");
}
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();
}
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
@@ -29,17 +29,20 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
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 {
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
@FXML private StackPane addServerContainer;
@FXML private Label lblServerUrl;
@@ -67,16 +70,13 @@ public class AddAuthlibInjectorServerPane extends StackPane {
transitionHandler = new TransitionHandler(addServerContainer);
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
btnAddNext.disableProperty().bind(
Bindings.createBooleanBinding(txtServerUrl::validate, txtServerUrl.textProperty()).not());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
}
private String fixInputUrl(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
@Override
public void onDialogShown() {
txtServerUrl.requestFocus();
}
private String resolveFetchExceptionMessage(Throwable exception) {
@@ -99,13 +99,13 @@ public class AddAuthlibInjectorServerPane extends StackPane {
lblCreationWarning.setText("");
String url = fixInputUrl(txtServerUrl.getText());
String url = txtServerUrl.getText();
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.fetchServerInfo(url);
serverBeingAdded = AuthlibInjectorServer.locateServer(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();
@@ -118,7 +118,9 @@ public class AddAuthlibInjectorServerPane extends StackPane {
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();
@@ -17,34 +17,86 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public AdvancedListItem() {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void setImage(Image image) {
this.image.set(image);
}
public Node getRightGraphic() {
return rightGraphic.get();
}
public ObjectProperty<Node> rightGraphicProperty() {
return rightGraphic;
}
public void setRightGraphic(Node rightGraphic) {
this.rightGraphic.set(rightGraphic);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public boolean isActionButtonVisible() {
return actionButtonVisible.get();
}
public BooleanProperty actionButtonVisibleProperty() {
return actionButtonVisible;
}
public void setActionButtonVisible(boolean actionButtonVisible) {
this.actionButtonVisible.set(actionButtonVisible);
}
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@@ -28,9 +27,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -38,6 +35,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
super(skinnable);
StackPane stackPane = new StackPane();
RipplerContainer container = new RipplerContainer(stackPane);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
@@ -84,20 +82,27 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.setPickOnBounds(false);
JFXButton settings = new JFXButton();
FXUtils.setLimitWidth(settings, 40);
settings.getStyleClass().setAll("toggle-icon4");
settings.setGraphic(SVG.gear(Theme.blackFillBinding(), -1, -1));
right.getChildren().setAll(settings);
right.setMouseTransparent(true);
right.getStyleClass().setAll("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
right.getChildren().clear();
} else {
right.getChildren().setAll(newGraphic);
}
});
root.setRight(right);
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().setAll("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
getChildren().setAll(stackPane);
getChildren().setAll(container);
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -21,18 +21,17 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@DefaultProperty("content")
@@ -101,23 +100,33 @@ public class ComponentList extends Control {
}
protected static class Skin extends SkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private final ObservableList<Node> list;
private final ObjectBinding<Node> firstItem;
protected Skin(ComponentList control) {
super(control);
list = MappedObservableList.create(control.getContent(), this::mapper);
list = MappedObservableList.create(control.getContent(), node -> {
ComponentListCell cell = new ComponentListCell(node);
cell.getStyleClass().add("options-list-item");
return cell;
});
firstItem = Bindings.valueAt(list, 0);
firstItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
});
if (!list.isEmpty())
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
VBox vbox = new VBox();
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
}
private Node mapper(Node node) {
StackPane child = new StackPane();
child.getChildren().add(new ComponentListCell(node));
child.getStyleClass().add("options-list-item");
return child;
}
}
}
@@ -24,6 +24,7 @@ import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
@@ -78,7 +79,6 @@ class ComponentListCell extends StackPane {
content.getStyleClass().add("options-sublist");
BorderPane groupNode = new BorderPane();
groupNode.getStyleClass().add("options-list-item-header");
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
@@ -86,21 +86,25 @@ class ComponentListCell extends StackPane {
expandButton.getStyleClass().add("options-list-item-expand-button");
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());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
}
@@ -109,6 +113,8 @@ 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();
if (rightNode != null)
@@ -116,21 +122,13 @@ class ComponentListCell extends StackPane {
}
right.getChildren().add(expandButton);
groupNode.setRight(right);
labelVBox.setAlignment(Pos.CENTER_LEFT);
right.setAlignment(Pos.CENTER_RIGHT);
VBox container = new VBox();
container.setStyle("-fx-padding: 8 0 0 0;");
container.setPadding(new Insets(8, 0, 0, 0));
FXUtils.setLimitHeight(container, 0);
Rectangle clipRect = new Rectangle();
clipRect.widthProperty().bind(container.widthProperty());
clipRect.heightProperty().bind(container.heightProperty());
container.setClip(clipRect);
FXUtils.setOverflowHidden(container, true);
container.getChildren().setAll(content);
VBox holder = new VBox();
holder.getChildren().setAll(groupNode, container);
holder.getStyleClass().add("options-list-item-container");
groupNode.setBottom(container);
expandButton.setOnMouseClicked(e -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
@@ -162,7 +160,7 @@ class ComponentListCell extends StackPane {
expandedProperty().addListener((a, b, newValue) ->
expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(holder);
getChildren().setAll(groupNode);
} else
getChildren().setAll(content);
}
@@ -0,0 +1,31 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.ui.Controllers;
/**
* @author yushijinhun
* @see Controllers#dialog(javafx.scene.layout.Region)
*/
public interface DialogAware {
default void onDialogShown() {
}
}
@@ -17,20 +17,26 @@
*/
package org.jackhuang.hmcl.ui.construct;
import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import com.jfoenix.controls.JFXComboBox;
import javafx.beans.NamedArg;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ListCell;
import javafx.scene.text.Font;
public class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false;
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize,
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
valueProperty().addListener((a, b, newValue) -> {
if (enableStyle)
setStyle("-fx-font-family: \"" + newValue + "\";");
});
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new ListCell<String>() {
@Override
@@ -43,15 +49,15 @@ public class FontComboBox extends JFXComboBox<String> {
}
});
itemsProperty().bind(MultiStepBinding.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
if (loaded) return;
getItems().setAll(Font.getFamilies());
if (loaded)
return;
itemsProperty().unbind();
setItems(observableList(Font.getFamilies()));
loaded = true;
});
}
public void initValue(Font font) {
getItems().setAll(font.getFamily());
getSelectionModel().select(font.getFamily());
}
}
@@ -24,6 +24,7 @@ import javafx.beans.NamedArg;
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;
@@ -138,7 +139,7 @@ public class MultiFileItem<T> extends ComponentList {
public Node createChildren(String title, String subtitle, T userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 20);
JFXRadioButton left = new JFXRadioButton(title);
@@ -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,13 +91,25 @@ 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();
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);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> from.removeEventHandler(PageCloseEvent.CLOSE, (EventHandler<PageCloseEvent>) handler));
@@ -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);
}
}
@@ -1,37 +0,0 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
import java.net.MalformedURLException;
import java.net.URL;
public class URLValidator extends ValidatorBase {
private final ObservableList<String> protocols = FXCollections.observableArrayList();
public URLValidator() {
super();
}
public ObservableList<String> getProtocols() {
return protocols;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
try {
URL url = new URL(((TextInputControl) srcControl.get()).getText());
if (protocols.isEmpty())
hasErrors.set(false);
else
hasErrors.set(!protocols.contains(url.getProtocol()));
} catch (MalformedURLException e) {
hasErrors.set(true);
}
}
}
}
@@ -17,12 +17,37 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.scene.control.TextInputControl;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
public final class Validator extends ValidatorBase {
public static Consumer<Predicate<String>> addTo(JFXTextField control) {
return addTo(control, null);
}
/**
* @see SafeStringConverter#asPredicate(Consumer)
*/
public static Consumer<Predicate<String>> addTo(JFXTextField control, String message) {
return predicate -> {
Validator validator = new Validator(message, predicate);
InvalidationListener listener = any -> control.validate();
validator.getProperties().put(validator, listener);
control.textProperty().addListener(new WeakInvalidationListener(listener));
control.getValidators().add(validator);
};
}
private final Predicate<String> validator;
/**
@@ -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);
@@ -24,6 +24,8 @@ import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
@@ -38,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;
@@ -67,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;
@@ -78,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());
@@ -104,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() {
@@ -303,6 +322,23 @@ public class DecoratorController {
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(DialogCloseEvent.CLOSE, handler);
if (node instanceof DialogAware) {
DialogAware dialogAware = (DialogAware) node;
if (dialog.isVisible()) {
dialogAware.onDialogShown();
} else {
dialog.visibleProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (newValue) {
dialogAware.onDialogShown();
observable.removeListener(this);
}
}
});
}
}
}
@SuppressWarnings("unchecked")
@@ -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());
}
@@ -22,6 +22,8 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
public class ProfileAdvancedListItem extends AdvancedListItem {
@@ -32,14 +34,15 @@ public class ProfileAdvancedListItem extends AdvancedListItem {
Profile profile = get();
if (profile == null) {
} else {
titleProperty().set(Profiles.getProfileDisplayName(profile));
subtitleProperty().set(profile.getGameDir().toString());
setTitle(Profiles.getProfileDisplayName(profile));
setSubtitle(profile.getGameDir().toString());
}
}
};
public ProfileAdvancedListItem() {
imageProperty().set(new Image("/assets/img/craft_table.png"));
setImage(new Image("/assets/img/craft_table.png"));
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
public ObjectProperty<Profile> profileProperty() {
@@ -19,48 +19,23 @@ package org.jackhuang.hmcl.ui.profile;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class ProfileList extends ListPage<ProfileListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(i18n("profile.manage"));
private ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
itemsProperty().addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
Profile selected = get();
itemsProperty().forEach(item -> item.selectedProperty().set(item.getProfile() == selected));
}
};
private final ListProperty<Profile> profiles = new SimpleListProperty<>(FXCollections.observableArrayList());
private ToggleGroup toggleGroup;
private final ObservableList<ProfileListItem> profileItems;
private ObjectProperty<Profile> selectedProfile;
public ProfileList() {
toggleGroup = new ToggleGroup();
profileItems = MappedObservableList.create(
profilesProperty(),
profile -> new ProfileListItem(toggleGroup, profile));
itemsProperty().bindContent(profileItems);
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null || toggle.getUserData() == null) return;
selectedProfile.set(((ProfileListItem) toggle.getUserData()).getProfile());
});
setItems(MappedObservableList.create(profilesProperty(), ProfileListItem::new));
selectedProfile = createSelectedItemPropertyFor(getItems(), Profile.class);
}
public ObjectProperty<Profile> selectedProfileProperty() {
@@ -17,30 +17,26 @@
*/
package org.jackhuang.hmcl.ui.profile;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Control;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
public class ProfileListItem extends Control {
public class ProfileListItem extends RadioButton {
private final Profile profile;
private final ToggleGroup toggleGroup;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final BooleanProperty selected = new SimpleBooleanProperty();
public ProfileListItem(ToggleGroup toggleGroup, Profile profile) {
public ProfileListItem(Profile profile) {
this.profile = profile;
this.toggleGroup = toggleGroup;
getStyleClass().clear();
setUserData(profile);
title.set(Profiles.getProfileDisplayName(profile));
subtitle.set(profile.getGameDir().toString());
selected.set(Profiles.selectedProfileProperty().get() == profile);
}
@Override
@@ -48,27 +44,35 @@ public class ProfileListItem extends Control {
return new ProfileListItemSkin(this);
}
public ToggleGroup getToggleGroup() {
return toggleGroup;
public void remove() {
Profiles.getProfiles().remove(profile);
}
public Profile getProfile() {
return profile;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public StringProperty titleProperty() {
return title;
}
public String getSubtitle() {
return subtitle.get();
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public StringProperty subtitleProperty() {
return subtitle;
}
public BooleanProperty selectedProperty() {
return selected;
}
public void remove() {
Profiles.getProfiles().remove(profile);
}
}
@@ -26,7 +26,6 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -39,25 +38,23 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
BorderPane root = new BorderPane();
JFXRadioButton chkSelected = new JFXRadioButton();
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.setUserData(skinnable);
chkSelected.selectedProperty().bindBidirectional(skinnable.selectedProperty());
chkSelected.setToggleGroup(skinnable.getToggleGroup());
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().set(new Image("/assets/img/craft_table.png"));
imageViewContainer.getChildren().setAll(imageView);
TwoLineListItem item = new TwoLineListItem();
BorderPane.setAlignment(item, Pos.CENTER);
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.ui.profile;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -36,6 +38,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.nio.file.Paths;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -60,11 +63,24 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
title = new ReadOnlyStringWrapper(this, "title",
profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName);
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(""));
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(".minecraft"));
FXUtils.loadFXML(this, "/assets/fxml/profile.fxml");
txtProfileName.setText(profileDisplayName);
txtProfileName.getValidators().add(new ValidatorBase() {
{
setMessage(i18n("profile.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
if (Profiles.getProfiles().stream().anyMatch(profile -> profile.getName().equals(control.getText())))
hasErrors.set(true);
else
hasErrors.set(false);
}
});
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
@@ -2,7 +2,6 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.scene.input.TransferMode;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.Datapack;
@@ -24,7 +23,6 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPage<DatapackListItem> implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty();
private final ObservableList<DatapackListItem> list; // Hold weak references
private final Path worldDir;
private final Datapack datapack;
@@ -36,14 +34,14 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
datapack = new Datapack(worldDir.resolve("datapacks"));
datapack.loadFromDir();
list = MappedObservableList.create(datapack.getInfo(), pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
}));
itemsProperty().bindContent(list);
setItems(MappedObservableList.create(datapack.getInfo(),
pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack);
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
})));
setOnDragOver(event -> {
if (event.getGestureSource() != this && event.getDragboard().hasFiles())
@@ -17,37 +17,31 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.beans.InvalidationListener;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import java.io.File;
import java.util.Objects;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
public GameAdvancedListItem() {
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
FXUtils.runLaterIf(() -> !Objects.nonNull(Profiles.getSelectedProfile()), () -> {
imageProperty().set(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
if (version != null) {
titleProperty().set(version);
subtitleProperty().set(null);
} else {
titleProperty().set(i18n("version.empty"));
subtitleProperty().set(i18n("version.empty.add"));
}
});
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
setTitle(version);
setSubtitle(null);
setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
} else {
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
setImage(new Image("/assets/img/grass.png"));
}
});
setRightGraphic(SVG.gear(Theme.blackFillBinding(), -1, -1));
}
}
@@ -25,18 +25,21 @@ import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
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.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;
@@ -47,38 +50,30 @@ public class GameList extends Control implements DecoratorPage {
private final BooleanProperty loading = new SimpleBooleanProperty(true);
private final ListProperty<GameListItem> items = new SimpleListProperty<>(FXCollections.observableArrayList());
private Profile profile;
private ToggleGroup toggleGroup;
public GameList() {
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
loadVersions((HMCLGameRepository) event.getSource());
});
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
JFXUtilities.runInFX(() -> loading.set(true));
});
Profiles.selectedProfileProperty().addListener((a, b, newValue) -> profile = newValue);
profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded())
loadVersions(profile.getRepository());
else
profile.getRepository().refreshVersionsAsync().start();
Profiles.registerVersionsListener(this::loadVersions);
}
private void loadVersions(HMCLGameRepository repository) {
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
toggleGroup = new ToggleGroup();
WeakListenerHolder listenerHolder = new WeakListenerHolder();
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
List<GameListItem> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.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(() -> {
if (profile == repository.getProfile()) {
if (profile == Profiles.getSelectedProfile()) {
loading.set(false);
items.setAll(children);
children.forEach(GameListItem::checkSelection);
@@ -106,20 +101,26 @@ 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() {
profile.getRepository().refreshVersionsAsync().start();
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
public void modifyGlobalGameSettings() {
Versions.modifyGlobalSettings(profile);
Versions.modifyGlobalSettings(Profiles.getSelectedProfile());
}
@Override
@@ -17,13 +17,12 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
public class GameListItem extends Control {
private final Profile profile;
@@ -83,12 +82,11 @@ public class GameListItem extends Control {
}
public void launch() {
Versions.launch(profile, version);
Versions.testGame(profile, version);
}
public void modifyGameSettings() {
Controllers.getVersionPage().load(version, profile);
Controllers.navigate(Controllers.getVersionPage());
Versions.modifyGameSettings(profile, version);
}
public void generateLaunchScript() {
@@ -63,7 +63,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(skinnable::browse, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch"), FXUtils.withJFXPopupClosing(skinnable::launch, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(skinnable::launch, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(skinnable::generateLaunchScript, popup)));
HBox right = new HBox();
@@ -23,6 +23,7 @@ import javafx.scene.input.TransferMode;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -77,6 +78,10 @@ public final class ModListPage extends ListPage<ModItem> {
});
}
public void loadVersion(Profile profile, String id) {
loadMods(profile.getModManager(), id);
}
public void loadMods(ModManager modManager, String versionId) {
this.modManager = modManager;
this.versionId = versionId;
@@ -20,16 +20,20 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXTabPane;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
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;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -60,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;
@@ -83,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();
@@ -92,13 +98,19 @@ 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"));
FXUtils.installTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
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);
}
public void load(String id, Profile profile) {
@@ -107,14 +119,34 @@ public final class VersionPage extends StackPane implements DecoratorPage {
title.set(i18n("version.manage.manage") + " - " + id);
versionSettings.loadVersionSetting(profile, id);
versionSettings.loadVersion(profile, id);
mod.setParentTab(tabPane);
modTab.setUserData(mod);
mod.loadMods(profile.getModManager(), id);
mod.loadVersion(profile, id);
installer.loadVersion(profile, id);
world.loadVersion(profile, id);
}
private void onNavigated(Navigator.NavigationEvent event) {
if (this.version == null || this.profile == null)
throw new IllegalStateException();
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (!this.profile.getRepository().isLoaded() ||
!this.profile.getRepository().hasVersion(version)) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
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());
@@ -53,6 +53,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
@@ -89,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;
@@ -106,11 +108,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.smoothScrolling(scroll);
Task.of(variables -> variables.set("list", JavaVersion.getJREs()))
Task.of(variables -> variables.set("list", JavaVersion.getJavas()))
.subscribe(Schedulers.javafx(), variables -> {
javaItem.loadChildren(
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion() + i18n("settings.game.java_directory.bit", javaVersion.getPlatform().getBit()), javaVersion.getBinary().toString(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
@@ -138,7 +140,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
else
profile.getRepository().globalizeVersionSetting(versionId);
Platform.runLater(() -> loadVersionSetting(profile, versionId));
Platform.runLater(() -> loadVersion(profile, versionId));
});
specificSettingsListener = o -> {
@@ -149,7 +151,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
advancedSettingsPane.disableProperty().bind(chkEnableSpecificSettings.selectedProperty().not());
}
public void loadVersionSetting(Profile profile, String versionId) {
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
@@ -180,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);
@@ -207,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());
@@ -245,7 +249,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
if (lastVersionSetting.isUsesCustomJavaDir()) {
javaItem.setSelectedData(null);
javaItem.getGroup().getToggles().stream()
.filter(javaItem::isCustomToggle)
.findFirst().get()
.setSelected(true);
} else {
try {
javaItem.setSelectedData(lastVersionSetting.getJavaVersion());
@@ -259,7 +266,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
variables -> javaItem.setSubtitle(variables.<JavaVersion>getOptional("java")
.map(JavaVersion::getBinary).map(File::getAbsolutePath).orElse("Invalid Java Directory"))));
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Directory"))));
}
@FXML
@@ -27,7 +27,6 @@ import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
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.task.TaskExecutor;
@@ -36,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;
@@ -101,12 +102,17 @@ public class Versions {
}
}
public static void generateLaunchScript(Profile profile, String id) {
GameRepository repository = profile.getRepository();
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);
}
}
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else {
public static void generateLaunchScript(Profile profile, String id) {
if (checkForLaunching(profile, id)) {
GameRepository repository = profile.getRepository();
FileChooser chooser = new FileChooser();
if (repository.getRunDirectory(id).isDirectory())
chooser.setInitialDirectory(repository.getRunDirectory(id));
@@ -116,22 +122,41 @@ public class Versions {
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file);
new LauncherHelper(profile, Accounts.getSelectedAccount(), id).makeLaunchScript(file);
}
}
public static void launch(Profile profile, String id) {
if (checkForLaunching(profile, id))
new LauncherHelper(profile, Accounts.getSelectedAccount(), id).launch();
}
public static void testGame(Profile profile, String id) {
if (checkForLaunching(profile, id)) {
LauncherHelper helper = new LauncherHelper(profile, Accounts.getSelectedAccount(), id);
helper.setTestMode();
helper.launch();
}
}
private static boolean checkForLaunching(Profile profile, String id) {
if (Accounts.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
else if (id == null)
else if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id))
Controllers.dialog(i18n("version.empty.launch"));
else
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
return true;
return false;
}
public static void modifyGlobalSettings(Profile profile) {
VersionSettingsPage page = new VersionSettingsPage();
page.loadVersionSetting(profile, null);
page.loadVersion(profile, null);
Controllers.navigate(page);
}
public static void modifyGameSettings(Profile profile, String version) {
Controllers.getVersionPage().load(version, profile);
Controllers.navigate(Controllers.getVersionPage());
}
}
@@ -11,7 +11,6 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.versioning.IntVersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
@@ -67,7 +66,7 @@ public class WorldListItem extends Control {
public void manageDatapacks() {
if (world.getGameVersion() == null || // old game will not write game version to level.dat
(IntVersionNumber.isIntVersionNumber(world.getGameVersion()) // we don't parse snapshot version
(VersionNumber.isIntVersionNumber(world.getGameVersion()) // we don't parse snapshot version
&& VersionNumber.asVersion(world.getGameVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
Controllers.dialog(i18n("world.datapack.1_13"));
return;
@@ -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;
@@ -159,7 +158,7 @@ public final class UpdateHandler {
private static void startJava(Path jar, String... appArgs) throws IOException {
List<String> commandline = new ArrayList<>();
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath());
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
commandline.add("-jar");
commandline.add(jar.toAbsolutePath().toString());
for (String arg : appArgs) {
@@ -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%;
}
+27 -8
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;
@@ -732,11 +754,8 @@
-fx-font-size: 12;
}
.options-list-item-ahead {
-fx-background-radius: 2 2 0 0;
-fx-background-color: white;
-fx-padding: 10 16 10 16;
-fx-font-size: 12;
.options-list-item:first {
-fx-border-width: 0;
}
.options-list-item-expand-button {
@@ -29,10 +29,20 @@
<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"/>
<Hyperlink fx:id="linkManageInjectorServers" text="%account.injector.manage"
onMouseClicked="#onManageInjecterServers" GridPane.columnIndex="2" GridPane.rowIndex="1"/>
<HBox GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
<graphic>
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</graphic>
</JFXButton>
<JFXButton fx:id="btnManageServer" styleClass="toggle-icon4" onMouseClicked="#onManageInjecterServers">
<graphic>
<fx:include source="/assets/svg/gear.fxml" />
</graphic>
</JFXButton>
</HBox>
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
@@ -57,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>
@@ -4,7 +4,6 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
<?import org.jackhuang.hmcl.ui.construct.URLValidator?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns="http://javafx.com/javafx"
@@ -16,23 +15,16 @@
<Label text="%account.injector.add" />
</heading>
<body>
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url" onAction="#onAddNext">
<validators>
<URLValidator message="%input.url">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
</protocols>
</URLValidator>
</validators>
</JFXTextField>
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url" onAction="#onAddNext" />
</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="closeButton" 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>
@@ -20,14 +20,17 @@
<BorderPane> <!-- Name -->
<left>
<VBox>
<Label text="%profile" BorderPane.alignment="CENTER_LEFT"/>
<VBox alignment="CENTER_LEFT">
<Label text="%profile.name" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</left>
<right>
<JFXTextField fx:id="txtProfileName" BorderPane.alignment="CENTER_RIGHT">
<BorderPane.margin>
<Insets top="8" bottom="8" />
</BorderPane.margin>
<validators>
<RequiredFieldValidator/>
<RequiredFieldValidator message="%input.not_empty" />
</validators>
</JFXTextField>
</right>
@@ -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.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 292 KiB

@@ -36,12 +36,14 @@ account.create=Create a new account
account.email=Email
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.injector_download_failure=Failed to download authlib-injector. Check your network and try switching to another download source.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
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. Is this a valid authentication server?
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.
@@ -54,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
@@ -108,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.
@@ -138,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.
@@ -163,8 +166,8 @@ launcher=Launcher
launcher.background=Background Image
launcher.background.choose=Choose background path.
launcher.background.default=Default
launcher.common_directory=Common Directory
launcher.common_directory.choose=Choose common 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
@@ -216,11 +219,12 @@ modpack.name=Modpack Name
modpack.not_a_valid_name=Not a valid modpack name
modpack.scan=Scanning this modpack
modpack.task.install=Import Modpack
modpack.task.install.error=Failed to install the modpack. Maybe the files is incorrect, or a management issue occurred.
modpack.task.install.error=This modpack file cannot be recognized. Only Curse, MultiMC modpacks are supported.
modpack.task.install.will=Install the modpack:
modpack.type.curse=Curse
modpack.type.curse.completion=Install relative files to Curse modpack
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are deleted and cannot be downloaded. Please consider the latest version or other modpacks.
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
@@ -268,11 +272,13 @@ world.name.enter=Enter the world name
world.time=EEE, MMM d, yyyy HH:mm:ss
profile=Game Directories
profile.already_exists=This name already exists, please consider another name.
profile.default=Current directory
profile.home=User home
profile.instance_directory=Game Directory
profile.instance_directory.choose=Choose Game Directory
profile.manage=Game Directory List
profile.name=Name
profile.new=New Config
profile.title=Game Directories
profile.selected=Selected
@@ -286,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
@@ -309,6 +316,7 @@ settings.game.dimension=Game Window Dimension
settings.game.exploration=Explore
settings.game.fullscreen=Fullscreen
settings.game.java_directory=Java Directory
settings.game.java_directory.bit=, %s-Bit
settings.game.java_directory.choose=Choose Java Directory.
settings.game.management=Manage
settings.game.working_directory=Working Directory
@@ -321,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
@@ -351,7 +363,7 @@ update.failed=Failed to perform upgrade
update.found=Update Available!
update.newest_version=Latest version: %s
update.bubble.title=Update Available: %s
update.bubble.subtitle=Click here for details
update.bubble.subtitle=Click here to upgrade
update.note=Development version contains more functionality and bug fixes as well as more possible bugs.
update.latest=This is latest Version.
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
@@ -360,18 +372,20 @@ update.tooltip=Update
version=Games
version.cannot_read=Unable to gather the game version. Cannot continue auto-installing.
version.empty=No game
version.empty.add=Click btn right to install game
version.empty.add=Install game in game list
version.empty.launch=No game to launch, please install new game via game list page.
version.forbidden_name=Forbidden name, do not use this.
version.game.old=Old
version.game.release=Release
version.game.snapshot=Snapshot
version.launch=Play
version.launch.test=Test Game
version.launch_script=Make Launching Script
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
@@ -36,12 +36,14 @@ account.create=建立帳戶
account.email=電子信箱
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
account.failed.injector_download_failure=無法下載 authlib-injector,請檢查網路或嘗試切換下載源
account.failed.invalid_credentials=您的使用者名稱或密碼錯誤,或者登入次數過多被暫時禁止登入,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
account.failed.no_character=該帳號沒有角色
account.failed.server_response_malformed=這個伺服器不是合法的登入驗證伺服器
account.failed.server_response_malformed=無法解析認證伺服器響應,可能是伺服器故障
account.injector.add=新增認證伺服器
account.injector.empty=無(點擊右側加號添加)
account.injector.manage=管理認證伺服器
account.injector.manage.title=認證伺服器
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
@@ -54,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=使用者名稱
@@ -108,8 +110,6 @@ folder.resourcepacks=資源包資料夾
folder.saves=遊戲存檔資料夾
folder.screenshots=截圖資料夾
general.prompt.empty=(無)
help=HMCL 幫助文檔
help.detail=可查閱資料包、整合包製作指南等內容。
@@ -138,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。
@@ -163,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=聯絡我們
@@ -216,11 +219,12 @@ modpack.name=整合包名稱
modpack.not_a_valid_name=整合包名稱無效
modpack.scan=解析整合包
modpack.task.install=匯入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式錯誤或操作檔案失敗。
modpack.task.install.error=無法識別該整合包,目前僅支持導入 Curse、MultiMC、HMCL 整合包。
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關檔案
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該整合包的最新版本或者安裝其他整合包。
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=不支援該整合包。僅 HMCL、MultiMC、Curse 整合包受支援。
@@ -268,11 +272,13 @@ world.name.enter=輸入世界名稱
world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
profile=遊戲目錄
profile.already_exists=該名稱已存在
profile.default=目前目錄
profile.home=官方啟動器
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.manage=遊戲目錄列表
profile.name=名稱
profile.new=建立設定
profile.title=遊戲目錄
profile.selected=已選取
@@ -286,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
@@ -309,6 +316,7 @@ settings.game.dimension=遊戲介面解析度大小
settings.game.exploration=瀏覽
settings.game.fullscreen=全螢幕
settings.game.java_directory=Java 路徑
settings.game.java_directory.bit=,%s 位
settings.game.java_directory.choose=選擇 Java 路徑
settings.game.management=管理
settings.game.working_directory=運行路徑(版本隔離)
@@ -321,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=使用系統代理
@@ -351,7 +363,7 @@ update.failed=更新失敗
update.found=發現到更新
update.newest_version=最新版本為:%s
update.bubble.title=發現更新:%s
update.bubble.subtitle=點擊此處查看詳情
update.bubble.subtitle=點擊此處進行升級
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.latest=目前版本為最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址打開頁面
@@ -360,18 +372,20 @@ update.tooltip=更新
version=遊戲
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
version.empty=無遊戲版本
version.empty.add=點擊右側按鈕安裝
version.empty.add=進入遊戲列表安裝
version.empty.launch=沒有可啟動的遊戲,你可以點擊左側遊戲欄內的設置按鈕進入遊戲列表安裝遊戲
version.forbidden_name=此版本名稱不受支援,請換一個名字
version.game.old=老舊版本
version.game.release=穩定版本
version.game.snapshot=測試版本
version.launch=啟動遊戲
version.launch.test=測試遊戲
version.launch_script=生成啟動腳本
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=刪除該版本
@@ -34,14 +34,16 @@ account.character=角色
account.choose=选择一个角色
account.create=新建账户
account.email=邮箱
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.injector_download_failure=无法下载 authlib-injector,请检查网络或尝试切换下载源
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_character=该帐号没有角色
account.failed.server_response_malformed=这个服务器不是合法的登录验证服务器
account.failed.server_response_malformed=无法解析认证服务器响应,可能是服务器故障
account.injector.add=添加认证服务器
account.injector.empty=无(点击右侧加号添加)
account.injector.manage=管理认证服务器
account.injector.manage.title=认证服务器
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
@@ -50,11 +52,11 @@ account.injector.server_url=服务器地址
account.injector.server_name=服务器名称
account.manage=账户列表
account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
account.methods.authlib_injector=外置登录 (authlib-injector)
account.methods.offline=离线模式
account.methods.yggdrasil=正版登录
account.missing=没有游戏账户
account.missing.add=点击右边按钮添加
account.missing.add=点击此处添加账户
account.password=密码
account.username=用户名
@@ -108,8 +110,6 @@ folder.resourcepacks=资源包文件夹
folder.saves=存档文件夹
folder.screenshots=截图文件夹
general.prompt.empty=(无)
help=HMCL 帮助文档
help.detail=可查阅数据包、整合包制作指南等内容。
@@ -138,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。
@@ -163,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=联系我们
@@ -216,11 +219,12 @@ modpack.name=整合包名称
modpack.not_a_valid_name=不是一个有效的整合包名称
modpack.scan=解析整合包
modpack.task.install=导入整合包
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败。
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。
modpack.task.install.will=将会安装整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=该整合包不被支持。仅 HMCL、MultiMC、Curse 整合包受支持。
@@ -268,11 +272,13 @@ world.name.enter=输入世界名称
world.time=yyyy 年 MM 月 dd 日 HH:mm:ss
profile=游戏目录
profile.already_exists=该名称已存在
profile.default=当前目录
profile.home=官方启动器
profile.instance_directory=游戏路径
profile.instance_directory.choose=选择游戏路径
profile.manage=游戏目录列表
profile.name=名称
profile.new=新建配置
profile.title=游戏目录
profile.selected=已选中
@@ -286,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
@@ -309,6 +316,7 @@ settings.game.dimension=游戏窗口分辨率
settings.game.exploration=浏览
settings.game.fullscreen=全屏
settings.game.java_directory=Java 路径
settings.game.java_directory.bit=,%s 位
settings.game.java_directory.choose=选择 Java 路径
settings.game.management=管理
settings.game.working_directory=运行路径(版本隔离)
@@ -321,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=使用系统代理
@@ -351,7 +363,7 @@ update.failed=更新失败
update.found=发现更新
update.newest_version=最新版本为:%s
update.bubble.title=发现更新:%s
update.bubble.subtitle=点击此处查看详情
update.bubble.subtitle=点击此处进行升级
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
@@ -360,18 +372,20 @@ update.tooltip=更新
version=游戏
version.cannot_read=读取游戏版本失败,无法进行自动安装
version.empty=没有游戏版本
version.empty.add=点击右侧按钮安装
version.empty.add=进入游戏列表安装
version.empty.launch=没有可启动的游戏,你可以点击左侧游戏栏内的设置按钮进入游戏列表安装游戏
version.forbidden_name=此版本名称不受支持,请换一个名字
version.game.old=远古版
version.game.release=稳定版
version.game.snapshot=测试版
version.launch=启动游戏
version.launch.test=测试游戏
version.launch_script=生成启动脚本
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

@@ -20,21 +20,20 @@ package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private AuthlibInjectorServer server;
@@ -48,7 +47,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
}
@Override
public AuthInfo logIn() throws AuthenticationException {
public synchronized AuthInfo logIn() throws AuthenticationException {
return inject(super::logIn);
}
@@ -58,41 +57,44 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
}
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> loginAction) throws AuthenticationException {
// Pre-fetch metadata
GetTask metadataFetchTask = new GetTask(NetworkUtils.toURL(server.getUrl()));
Thread metadataFetchThread = Lang.thread(() -> {
try {
metadataFetchTask.run();
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to pre-fetch Yggdrasil metadata", e);
CompletableFuture<byte[]> prefetchedMetaTask = CompletableFuture.supplyAsync(() -> {
try (InputStream in = new URL(server.getUrl()).openStream()) {
return readFullyWithoutClosing(in);
} catch (IOException e) {
throw new CompletionException(new ServerDisconnectException(e));
}
}, "Yggdrasil metadata fetch thread");
});
// Update authlib-injector
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return authlibInjectorDownloader.get();
} catch (IOException e) {
throw new CompletionException(new AuthlibInjectorDownloadException(e));
}
});
AuthInfo auth = loginAction.get();
byte[] prefetchedMeta;
AuthlibInjectorArtifactInfo artifact;
try {
artifact = authlibInjectorDownloader.get();
} catch (IOException e) {
throw new AuthenticationException("Failed to download authlib-injector", e);
}
// Perform authentication
AuthInfo info = loginAction.get();
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl());
// Wait for metadata to be fetched
try {
metadataFetchThread.join();
prefetchedMeta = prefetchedMetaTask.get();
artifact = artifactTask.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Optional<String> metadata = Optional.ofNullable(metadataFetchTask.getResult());
if (metadata.isPresent()) {
arguments = arguments.addJVMArguments(
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(metadata.get().getBytes(UTF_8)));
throw new AuthenticationException(e);
} catch (ExecutionException e) {
if (e.getCause() instanceof AuthenticationException) {
throw (AuthenticationException) e.getCause();
} else {
throw new AuthenticationException(e.getCause());
}
}
return info.withArguments(arguments);
return auth.withArguments(new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client",
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta)));
}
@Override
@@ -17,16 +17,16 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.decodeURL;
import java.util.Optional;
import java.util.function.Consumer;
import javafx.event.EventHandler;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Optional;
import java.util.function.Consumer;
/**
* @author yushijinhun
* @see https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF
@@ -44,11 +44,7 @@ public final class AuthlibInjectorDnD {
String[] uriElements = uri.split(":");
if (uriElements.length == 3 && SCHEME.equals(uriElements[0]) && PATH_YGGDRASIL_SERVER.equals(uriElements[1])) {
try {
return Optional.of(URLDecoder.decode(uriElements[2], "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
return Optional.of(decodeURL(uriElements[2]));
}
return Optional.empty();
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -15,37 +15,27 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util.versioning;
package org.jackhuang.hmcl.auth.authlibinjector;
import java.util.Objects;
import org.jackhuang.hmcl.auth.AuthenticationException;
/**
* If a version string contains alphabets, a {@code StringVersionNumber}
* will be constructed.
*
* @author huangyuhui
* @author yushijinhun
*/
public final class StringVersionNumber extends VersionNumber {
public class AuthlibInjectorDownloadException extends AuthenticationException {
private final String version;
StringVersionNumber(String version) {
Objects.requireNonNull(version);
this.version = version;
public AuthlibInjectorDownloadException() {
}
public String getVersion() {
return version;
public AuthlibInjectorDownloadException(String message, Throwable cause) {
super(message, cause);
}
@Override
public int hashCode() {
return version.hashCode();
public AuthlibInjectorDownloadException(String message) {
super(message);
}
@Override
public String toString() {
return version;
public AuthlibInjectorDownloadException(Throwable cause) {
super(cause);
}
}
@@ -45,6 +45,11 @@ public class AuthlibInjectorDownloader {
private Path artifactLocation;
private Supplier<DownloadProvider> downloadProvider;
/**
* The flag will be reset after application restart.
*/
private boolean updateChecked = false;
/**
* @param artifactsDirectory where to save authlib-injector artifacts
*/
@@ -57,14 +62,17 @@ public class AuthlibInjectorDownloader {
synchronized (artifactLocation) {
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
try {
update(local);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to download authlib-injector", e);
if (!local.isPresent()) {
throw e;
if (!local.isPresent() || !updateChecked) {
try {
update(local);
updateChecked = true;
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to download authlib-injector", e);
if (!local.isPresent()) {
throw e;
}
LOG.warning("Fallback to use cached artifact: " + local.get());
}
LOG.warning("Fallback to use cached artifact: " + local.get());
}
return getLocalArtifact().orElseThrow(() -> new IOException("The updated authlib-inejector cannot be recognized"));
@@ -72,6 +80,7 @@ public class AuthlibInjectorDownloader {
}
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
LOG.info("Checking update of authlib-injector");
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
if (local.isPresent() && local.get().getBuildNumber() >= latest.buildNumber) {
@@ -17,23 +17,104 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Optional;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorServer {
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
private static final int MAX_REDIRECTS = 5;
public static AuthlibInjectorServer locateServer(String url) throws IOException {
url = parseInputUrl(url);
HttpURLConnection conn;
int redirectCount = 0;
for (;;) {
conn = (HttpURLConnection) new URL(url).openConnection();
Optional<String> ali = getApiLocationIndication(conn);
if (ali.isPresent()) {
conn.disconnect();
url = ali.get();
if (redirectCount >= MAX_REDIRECTS) {
throw new IOException("Exceeded maximum number of redirects (" + MAX_REDIRECTS + ")");
}
redirectCount++;
LOG.info("Redirect API root to: " + url);
} else {
break;
}
}
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
return parseResponse(url, readFullyWithoutClosing(conn.getInputStream()));
} finally {
conn.disconnect();
}
}
private static Optional<String> getApiLocationIndication(URLConnection conn) {
return Optional.ofNullable(conn.getHeaderFields().get("X-Authlib-Injector-API-Location"))
.flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)))
.flatMap(indication -> {
String currentUrl = appendSuffixSlash(conn.getURL().toString());
String newUrl;
try {
newUrl = appendSuffixSlash(new URL(conn.getURL(), indication).toString());
} catch (MalformedURLException e) {
LOG.warning("Failed to resolve absolute ALI, the header is [" + indication + "]. Ignore it.");
return Optional.empty();
}
if (newUrl.equals(currentUrl)) {
return Optional.empty();
} else {
return Optional.of(newUrl);
}
});
}
private static String parseInputUrl(String url) {
String lowercased = url.toLowerCase();
if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) {
url = "https://" + url;
}
url = appendSuffixSlash(url);
return url;
}
private static String appendSuffixSlash(String url) {
if (!url.endsWith("/")) {
return url + "/";
} else {
return url;
}
}
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
try (InputStream in = new URL(url).openStream()) {
return parseResponse(url, readFullyWithoutClosing(in));
}
}
private static AuthlibInjectorServer parseResponse(String url, byte[] rawResponse) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(new String(rawResponse, UTF_8), JsonObject.class);
String name = extractServerName(response).orElse(url);
return new AuthlibInjectorServer(url, name);
} catch (JsonParseException e) {
@@ -64,7 +64,7 @@ public class YggdrasilAccount extends Account {
}
@Override
public AuthInfo logIn() throws AuthenticationException {
public synchronized AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) {
if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true;
@@ -84,7 +84,7 @@ public class YggdrasilAccount extends Account {
}
@Override
public AuthInfo logInWithPassword(String password) throws AuthenticationException {
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
}
@@ -16,7 +16,6 @@ import java.net.URL;
import java.util.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.liftFunction;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -114,11 +113,15 @@ public class YggdrasilService {
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
Objects.requireNonNull(profile);
return Optional.ofNullable(profile.getProperties())
.flatMap(properties -> Optional.ofNullable(properties.get("textures")))
.map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), UTF_8))
.map(liftFunction(textures -> fromJson(textures, TextureResponse.class)))
.flatMap(response -> Optional.ofNullable(response.textures));
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties())
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
if (encodedTextures.isPresent()) {
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class);
return Optional.ofNullable(texturePayload.textures);
} else {
return Optional.empty();
}
}
private static YggdrasilSession handleAuthenticationResponse(String responseText, String clientToken) throws AuthenticationException {
@@ -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) {
@@ -66,9 +66,7 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
versions.clear();
for (Map.Entry<String, int[]> entry : root.getGameVersions().entrySet()) {
Optional<String> gameVersion = VersionNumber.parseVersion(entry.getKey());
if (!gameVersion.isPresent())
continue;
String gameVersion = VersionNumber.normalize(entry.getKey());
for (int v : entry.getValue()) {
ForgeVersion version = root.getNumber().get(v);
if (version == null)
@@ -84,7 +82,7 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
if (jar == null)
continue;
versions.put(gameVersion.get(), new ForgeRemoteVersion(
versions.put(gameVersion, new ForgeRemoteVersion(
version.getGameVersion(), version.getVersion(), jar
));
}
@@ -58,7 +58,7 @@ public final class GameAssetIndexDownloadTask extends Task {
}
@Override
public void execute() throws Exception {
public void execute() {
AssetIndexInfo assetIndexInfo = version.getAssetIndex();
File assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
@@ -67,10 +67,7 @@ public final class GameAssetIndexDownloadTask extends Task {
dependencies.add(new FileDownloadTask(
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(assetIndexInfo.getUrl())),
assetIndexFile
).setCaching(true)
.setCacheRepository(dependencyManager.getCacheRepository())
.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory()
.resolve("assets").resolve("indexes").resolve(assetIndexInfo.getId() + ".json")));
).setCacheRepository(dependencyManager.getCacheRepository()));
}
}
@@ -35,9 +35,12 @@ public class LibraryDownloadTask extends Task {
protected final Library library;
protected final String url;
protected boolean xz;
private final Library originalLibrary;
private boolean cached = false;
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
this.originalLibrary = library;
setSignificance(TaskSignificance.MODERATE);
if (library.is("net.minecraftforge", "forge"))
@@ -86,7 +89,7 @@ public class LibraryDownloadTask extends Task {
@Override
public void preExecute() throws Exception {
Optional<Path> libPath = cacheRepository.getLibrary(library);
Optional<Path> libPath = cacheRepository.getLibrary(originalLibrary);
if (libPath.isPresent()) {
try {
FileUtils.copyFile(libPath.get().toFile(), jar);
@@ -101,7 +104,7 @@ public class LibraryDownloadTask extends Task {
try {
URL packXz = NetworkUtils.toURL(url + ".pack.xz");
if (NetworkUtils.URLExists(packXz)) {
if (NetworkUtils.urlExists(packXz)) {
task = new FileDownloadTask(packXz, xzFile, null);
xz = true;
} else {
@@ -71,11 +71,10 @@ public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemot
for (Map.Entry<String, LiteLoaderGameVersions> entry : root.getVersions().entrySet()) {
String gameVersion = entry.getKey();
LiteLoaderGameVersions liteLoader = entry.getValue();
Optional<String> gg = VersionNumber.parseVersion(gameVersion);
if (!gg.isPresent())
continue;
doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false);
doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true);
String gg = VersionNumber.normalize(gameVersion);
doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false);
doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true);
}
} finally {
lock.writeLock().unlock();
@@ -71,11 +71,10 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
for (Map.Entry<String, LiteLoaderGameVersions> entry : root.getVersions().entrySet()) {
String gameVersion = entry.getKey();
LiteLoaderGameVersions liteLoader = entry.getValue();
Optional<String> gg = VersionNumber.parseVersion(gameVersion);
if (!gg.isPresent())
continue;
doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false);
doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true);
String gg = VersionNumber.normalize(gameVersion);
doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false);
doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true);
}
} finally {
lock.writeLock().unlock();
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more