Compare commits

..
152 Commits
Author SHA1 Message Date
huangyuhui 630f164877 Continue load old version if the newer version cannot launch 2018-06-10 00:10:17 +08:00
huangyuhui 38c6bce2c0 Change log folder to ~/.hmcl 2018-06-09 17:37:43 +08:00
huanghongxun 034734ec66 Merge pull request #348 from yushijinhun/fxgson
使用 FxGson
2018-06-09 01:12:50 +08:00
yushijinhun d875eeb22a 将Config中属性替换为JavaFX Property 2018-06-09 00:32:33 +08:00
yushijinhun 61db6b8fb2 添加FxGson依赖 2018-06-09 00:28:23 +08:00
huanghongxun 945d4cf20d Merge pull request #347 from yushijinhun/merge-head
重构
2018-06-08 21:34:36 +08:00
yushijinhun 631337c451 YggdrasilAccount记录角色uuid而不是名称
注:旧版本无法读取此版本配置(移除了character)。
2018-06-08 21:13:33 +08:00
yushijinhun 4f51e377ae 移除canPlayOffline() 2018-06-08 21:09:35 +08:00
yushijinhun e71c06a0d0 Arguments.addXXX改为链式调用 2018-06-08 21:09:35 +08:00
yushijinhun 13aabde5d9 完善Account文档 2018-06-08 21:09:35 +08:00
huanghongxun e3f371f457 Merge pull request #344 from yushijinhun/javafx
修复YggdrasilAccount读取到了错误的character
2018-06-08 16:36:50 +08:00
yushijinhun 6ee99b5736 修复YggdrasilAccount读取到了错误的character
引入:https://github.com/huanghongxun/HMCL/commit/6c4231a6b242c2a11af9768e424593cc6cc14bc5#diff-839dd42e51cd641828b00c5302c02b12R50
2018-06-08 16:31:56 +08:00
huangyuhui 895188b25f Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-06-08 16:06:31 +08:00
huanghongxun 9fba923a51 Merge pull request #343 from yushijinhun/javafx
修复 defaultJavaPath 不能保存的问题
2018-06-08 16:06:01 +08:00
huangyuhui 37a39098f8 Try to fix VersionNotFoundException. Closes #298 2018-06-08 16:05:33 +08:00
yushijinhun 6fb548e6c3 修复defaultJavaPath不能保存的问题(未添加监听器) 2018-06-08 15:19:47 +08:00
huangyuhui 110b7cb06f Should fix 'Never get to here' 2018-06-08 13:55:38 +08:00
huangyuhui e9070a77c3 Fixed java.lang.IllegalStateException: stream has already been operated upon or closed 2018-06-08 13:40:12 +08:00
huangyuhui 475e6e5511 Remove redundant URLs that download providers provide. Mentioned in #242 2018-06-08 13:13:42 +08:00
huangyuhui eed4c7c11a Load skin when added new account 2018-06-08 12:58:01 +08:00
huangyuhui d986d7ac03 Apply spinner instead of intermediate progress bar 2018-06-08 12:47:46 +08:00
huangyuhui f640922a50 Optimize imports 2018-06-08 12:47:34 +08:00
huangyuhui 118a6cf0d1 Fasten version item construction 2018-06-08 12:45:44 +08:00
huanghongxun 72871e6d9a Merge pull request #342 from yushijinhun/refactor-skin
Yggdrasil 部分重构及 bug 修复
2018-06-08 10:41:46 +08:00
yushijinhun 61acbcec0d 移除GameProfile类的TypeAdapter 2018-06-07 22:28:17 +08:00
yushijinhun 06af79e2b4 移除对legcay账户的支持 2018-06-07 22:25:31 +08:00
yushijinhun 6782602141 重构YggdrasilService相关代码 2018-06-07 21:12:38 +08:00
yushijinhun 8df17ae38a 使用静态方法pair()替代new Pair<>() 2018-06-07 21:08:25 +08:00
huanghongxun 1645a93adf Merge pull request #341 from yushijinhun/cleanup
清理冗余代码
2018-06-07 12:41:51 +08:00
yushijinhun 6a04ab37d7 清理Hex 2018-06-07 12:17:58 +08:00
yushijinhun 27c9abd9ed 清理DigestUtils 2018-06-07 12:16:34 +08:00
yushijinhun a08be2e11d 移除Charsets类 2018-06-07 11:47:36 +08:00
huanghongxun 17615b1791 Merge pull request #340 from yushijinhun/javafx
修复 Java 选择相关 bug
2018-06-07 01:15:44 +08:00
yushijinhun d75c3b584a 修复加载JREs时可能的并发修改异常 2018-06-07 01:06:13 +08:00
huangyuhui 20f919b93e Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-06-07 00:53:56 +08:00
huangyuhui 20244fe5cc Fasten versions loading 2018-06-07 00:50:15 +08:00
yushijinhun 6a89f0a661 将当前Java版本包含在getJREs()中 2018-06-07 00:28:33 +08:00
huanghongxun 3a9efdb03e Merge pull request #339 from yushijinhun/multiple-java
Linux下自动检测Java + 允许多个相同版本的JRE
2018-06-06 23:57:24 +08:00
huangyuhui 2ae00db94b Removed MultiFileItem tooltips 2018-06-06 23:54:19 +08:00
huangyuhui a9261b759b Shutdown Now to prevent NPE when task executed after app closed 2018-06-06 23:52:23 +08:00
yushijinhun 6b02dc3cd5 允许多个相同版本的Java共存 2018-06-06 23:43:58 +08:00
yushijinhun afa37c0d2f 自动检测Linux下的Java 2018-06-06 23:01:27 +08:00
huangyuhui 2575f10497 Fixed #336 2018-06-06 19:45:39 +08:00
huanghongxun 27c4a68a13 Merge pull request #338 from yushijinhun/openjdk
对 OpenJDK 更好的支持
2018-06-06 14:02:14 +08:00
yushijinhun f09cb54894 当JavaFX缺失时弹窗提示 2018-06-06 13:52:23 +08:00
yushijinhun 5431cfef79 对OpenJDK上出现的错误同样进行上报 2018-06-06 13:31:53 +08:00
huanghongxun 35c0f3cddc Merge pull request #337 from yushijinhun/javafx
完善 authlib-injector 功能
2018-06-03 03:12:19 +08:00
yushijinhun efe22ca613 防止执行平移动画时组件溢出面板 2018-06-02 20:09:36 +08:00
yushijinhun 3408a8ffcf 更改措辞:添加认证服务器->管理认证服务器 2018-06-02 19:59:00 +08:00
yushijinhun 60a140efb3 修复删除验证服务器时可能出现ConcurrentModificationException
初始化块执行时字段并不一定都完成了初始化,应当避免使用。
利用ConcurrentHashMap的弱一致性防止迭代中删除账户造成ConcurrentModificationException。
2018-06-02 19:38:03 +08:00
yushijinhun bbe584bd91 修复无法显示未设置皮肤的Yggdrasil账户的皮肤的问题 2018-06-02 19:21:41 +08:00
yushijinhun 3866377a28 使用UUID而不是角色名来标识皮肤 2018-06-02 18:15:41 +08:00
yushijinhun ebc0b4b2b0 移除AccountHelper未使用的单例 2018-06-02 18:06:32 +08:00
huanghongxun d6c65a320d Merge pull request #334 from yushijinhun/java9
移除对Unsafe的使用
2018-05-29 22:47:35 +08:00
yushijinhun 2e46d76165 移除Lang.get(Map,Object,Class) 2018-05-27 12:40:45 +08:00
yushijinhun 23037da406 移除对Unsafe的使用 2018-05-27 12:13:27 +08:00
yushijinhun 617261c74d 不使用ReflectionHelper来安装Tooltip 2018-05-27 12:09:04 +08:00
yushijinhun 6c0d6ee0cf 通过非反射方法获取总物理内存 2018-05-27 11:04:39 +08:00
huanghongxun 663e1ef7ab Merge pull request #332 from xfl03/javafx
Fix #325 #296
2018-05-20 19:40:39 +08:00
Alexander Xia 1a47d355f0 Fix #325 #296 2018-05-20 19:12:12 +08:00
huangyuhui e407994103 Fixed #331 2018-05-17 16:46:16 +08:00
huangyuhui bda2149c02 Fix #326 2018-04-30 01:59:33 +08:00
huangyuhui b925423d61 Fixded #324 2018-04-26 22:15:27 +08:00
huangyuhui f3906a37bf Fixed #321 2018-04-26 13:14:14 +08:00
huangyuhui a1e115ea6b Ignore some exceptions 2018-03-09 16:54:28 +08:00
huangyuhui 03383510ef Disable accept button in AddAccountPage 2018-03-09 16:45:12 +08:00
huangyuhui e8cf9644eb Fix IllegalArgumentException: Invalid UUID String 2018-03-09 16:31:39 +08:00
huangyuhui dbfcf1facc Try to fix empty stack of StackContainerPane 2018-03-09 16:24:02 +08:00
huangyuhui 44aff94937 Remove error 2018-03-06 11:25:15 +08:00
huangyuhui fb1fc92b7c Fix optifine 2018-03-06 11:19:21 +08:00
huangyuhui 71cfa64b16 Fix StackContainerPane 2018-03-06 10:19:37 +08:00
huangyuhui b200105a85 Fix Liteloader installing of BMCLAPI 2018-03-06 10:13:48 +08:00
huanghongxun ce83f3c5e1 Merge pull request #297 from andylizi/switch-to-https
Switch to Https for most links.
2018-03-05 20:57:30 +08:00
huanghongxun 57d3275856 Merge branch 'javafx' into switch-to-https 2018-03-05 20:56:26 +08:00
huangyuhui eb1a109afc Fix IllegalArgumentException: Folder parameter must be a valid folder 2018-03-05 20:36:38 +08:00
huangyuhui a03e3fea12 Logs stopApplication stack trace 2018-03-05 20:23:23 +08:00
huangyuhui c60ae111ef Progress bar in account page 2018-03-05 20:07:30 +08:00
huangyuhui 5eb46395ae Close popup when deleted an account 2018-03-05 19:13:58 +08:00
huangyuhui db08b61cfa Fix #301 2018-03-05 19:02:13 +08:00
huangyuhui 41d313baff Fix #303 2018-03-04 23:35:42 +08:00
huangyuhui 58c3ed9254 Fixed #302 2018-03-04 23:33:08 +08:00
huangyuhui affe36a568 Fix update again 2018-03-04 23:29:27 +08:00
huangyuhui 9ba6cddde7 Fix update 2018-03-04 19:09:11 +08:00
huangyuhui 5656c9f351 update update_link.php 2018-03-04 16:54:35 +08:00
huangyuhui ecf24a746e Fix crashing when unable to save version setting 2018-03-04 15:33:54 +08:00
huangyuhui 5b5a58a867 Fixed not terminating the process when closed window 2018-03-03 22:56:42 +08:00
huangyuhui c6ab7878e7 Optimize code 2018-03-02 23:42:38 +08:00
huangyuhui 99e85eba37 Fix NPE in LauncherHelper.executor.onFinish: executor.getRunningTasks 2018-03-02 23:11:17 +08:00
huangyuhui 3bf92e6f12 Fix process not exited when pressed Alt+F4 2018-03-02 22:54:36 +08:00
huangyuhui ba359305f6 Hide refresh and add button in MainPage 2018-03-02 19:22:10 +08:00
huangyuhui f826eb224c AccountPage maintainence 2018-03-02 10:41:25 +08:00
huangyuhui a675f4cb68 Upgrade message 2018-03-02 00:56:16 +08:00
huangyuhui 5e1e2c6047 Remove accountsPage 2018-03-02 00:43:33 +08:00
huangyuhui eba0990538 Collect log when launcher crashed 2018-03-01 01:02:05 +08:00
huangyuhui 1bcef2bab0 Added event logs. Cloese #244 2018-03-01 00:27:07 +08:00
huangyuhui f83777e1d2 Terminate when exclaimation mark is in the path 2018-02-28 23:34:31 +08:00
huangyuhui 9fa48090ce Show indeterminate progress bar when loading versions in MainPage 2018-02-28 22:15:46 +08:00
huangyuhui 7c8ac4fee8 more precise warning in authlib injector add server page 2018-02-28 20:36:32 +08:00
andylizi 76c254ee98 Switch the download links to HTTPS 2018-02-28 17:23:49 +08:00
huangyuhui e75ad64483 Fixed IllegalStateException when clicked the empty space of list in InstallTypePage 2018-02-27 23:27:26 +08:00
huangyuhui 17d1af181d Profile page title changed to display name 2018-02-27 23:25:10 +08:00
huangyuhui a72d27a431 May fix VersionNotFoundException when building main page version items 2018-02-27 23:18:19 +08:00
huangyuhui 4e9fc57aa4 Fix NullPointerException when launcher launched two game at the same time 2018-02-27 23:12:53 +08:00
huangyuhui 494b27cc22 May fix RemoteVersion not found exception 2018-02-27 23:05:05 +08:00
huangyuhui 3606186975 Fix NullPointerException when selected an version item in download.VersionsPage 2018-02-27 20:05:05 +08:00
huangyuhui 41dbdfc016 Fix NullPointerException when refreshing versions again 2018-02-27 19:58:43 +08:00
huangyuhui 655e5ca7e0 Fixed missed encoding in Log4jHandler 2018-02-27 11:13:07 +08:00
huangyuhui 3067d323de Fixed NullPointerException when switched profile 2018-02-26 20:16:14 +08:00
huangyuhui 1404a080ad library download optimization 2018-02-26 19:56:06 +08:00
huangyuhui cbb2a1b575 Unique duplicated libraries in minecraft json file 2018-02-26 11:21:34 +08:00
huangyuhui 30b276ec1a Forge xz library supported 2018-02-26 11:14:53 +08:00
huangyuhui 13a24d5b00 try to fix RejectedExecutionException 2018-02-25 11:10:31 +08:00
huangyuhui 40360cf18a Fixed installing game 2018-02-25 10:59:48 +08:00
huangyuhui 139e2ee78d try fix ConcurrentModificationException 2018-02-25 10:48:07 +08:00
huangyuhui 3ff466462c Better theme selection UI 2018-02-24 23:46:20 +08:00
huanghongxun 9ff2442789 Foreground text fill auto detected 2018-02-23 17:40:32 +08:00
huanghongxun b11e4ab1db Fixed font in dialog accept and cancel button. Closes #291 2018-02-23 14:53:08 +08:00
huanghongxun a52cc8c093 Fixed crash when launching 1.7.10 forge version 2018-02-23 11:36:44 +08:00
huanghongxun 506c409303 log4jHandler maintainence 2018-02-23 11:22:20 +08:00
huanghongxun 53a8446f4a Curse CDN download provider 2018-02-23 09:54:57 +08:00
huanghongxun ab5bb8ea6c customized theme 2018-02-22 21:54:20 +08:00
huanghongxun d097b84c82 Fixed #285 2018-02-22 16:26:47 +08:00
huanghongxun f285635e76 Add multiple mods. Closes #287 2018-02-22 16:16:25 +08:00
huanghongxun 0f7b5299e9 Fixed incorrect format of userProperties. Closes #286 2018-02-22 15:46:48 +08:00
huanghongxun 7a788d1e6e cleanup 2018-02-22 15:02:12 +08:00
huanghongxun 6c4231a6b2 authentication reconstruction 2018-02-22 14:12:55 +08:00
huanghongxun 6e202341ad clean up 2018-02-20 18:03:34 +08:00
huanghongxun 95a509b86c ToStringBuilder 2018-02-20 12:11:31 +08:00
huanghongxun 602114af55 UI maintainence 2018-02-20 10:48:59 +08:00
huanghongxun e5ccc40358 clear cached skin records when refrshing account state 2018-02-18 18:35:05 +08:00
huanghongxun 2310f6e1c0 Fix http warning not being hidden if https used 2018-02-18 18:29:22 +08:00
huanghongxun 9ae952bb78 Add comments 2018-02-18 18:26:01 +08:00
huanghongxun 57a7eaccf5 Optimize imports 2018-02-18 17:16:42 +08:00
huanghongxun 1934bfa962 Merge pull request #282 from yushijinhun/javafx
重构
2018-02-18 17:12:16 +08:00
yushijinhun a5f3ec7b9b fix indents 2018-02-18 17:07:54 +08:00
yushijinhun 3cfcf1e772 用定义的类型代替FXUtils.addListener中的Pair 2018-02-18 16:47:00 +08:00
yushijinhun b45907a9f6 Polish, 处理部分警告 2018-02-18 16:23:12 +08:00
yushijinhun 8fd9395e23 删除对SSL的干涉(出于安全考虑) 2018-02-18 16:03:13 +08:00
yushijinhun 126b2a19e7 ReflectionHelper中使用参数化类型 2018-02-18 16:01:21 +08:00
yushijinhun bde01af89d help compilers to infer types
ecc cannot infer it
2018-02-18 15:49:11 +08:00
yushijinhun 65491ef18e Remove 3-arg Lang.merge() 2018-02-18 15:44:32 +08:00
huanghongxun 44a9470613 Fixed same skin among characters in one account 2018-02-18 15:42:18 +08:00
yushijinhun 470045f228 Rename Lang.getOrPut() to computeIfAbsent()
See Map.computeIfAbsent() in java9
2018-02-18 15:33:58 +08:00
yushijinhun d081a6ee92 Remove Lang.requireJsonNonNull(), use JsonUtils.fromNonNullJson() instead 2018-02-18 15:32:00 +08:00
yushijinhun 95f404bb64 move emptyConsumer() to Constants 2018-02-18 15:20:00 +08:00
yushijinhun dcea76bcd3 Add missing @SafeVarargs in Lang 2018-02-18 15:17:17 +08:00
yushijinhun e9b816fb87 Change Lang.EMPTY_CONSUMER to emptyConsumer() 2018-02-18 15:15:28 +08:00
huanghongxun 20a7039f59 GC after game launched 2018-02-18 13:54:13 +08:00
huanghongxun 4ad172dda4 authlib-injector prefetch support 2018-02-18 09:39:56 +08:00
huanghongxun adf6c19996 Cache session profile response. Closes #280 2018-02-17 23:38:40 +08:00
huanghongxun 24ec0adacf Authlib Injector support 2018-02-17 23:28:05 +08:00
huanghongxun 83bc6748a3 HMCLCore authlib injector support 2018-02-17 11:01:08 +08:00
379 changed files with 7134 additions and 4832 deletions

No files matched your search

+11 -10
View File
@@ -1,3 +1,6 @@
import org.apache.tools.ant.filters.ReplaceTokens
import proguard.gradle.ProGuardTask
import java.security.MessageDigest
import java.util.jar.JarFile
import java.util.jar.Pack200
@@ -15,11 +18,9 @@ if (buildnumber == null)
def versionroot = System.getenv("VERSION_ROOT")
if (versionroot == null)
versionroot = "3.0"
versionroot = "3.1"
String mavenGroupId = 'HMCL'
String mavenVersion = versionroot + '.' + buildnumber
String bundleName = "Hello Minecraft! Launcher"
version = mavenVersion
dependencies {
@@ -30,7 +31,7 @@ dependencies {
task generateSources(type: Sync) {
from 'src/main/java'
into "$buildDir/generated-src"
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [
filter(ReplaceTokens, tokens: [
'HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING': mavenVersion
])
}
@@ -47,16 +48,16 @@ jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
manifest {
attributes 'Created-By' : 'Copyright(c) 2013-2017 huangyuhui.',
attributes 'Created-By' : 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class' : mainClass,
'Multi-Release': "true"
}
doLast {
new File("build/signed").mkdirs()
ant.signjar(signedjar: archivePath, jar: archivePath,
keystore: "HMCL.keystore", storepass: "123456",
alias: "HMCL")
//new File("build/signed").mkdirs()
//ant.signjar(signedjar: archivePath, jar: archivePath,
// keystore: "HMCL.keystore", storepass: "123456",
// alias: "HMCL")
def messageDigest = MessageDigest.getInstance("SHA1")
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
@@ -122,7 +123,7 @@ task makePackGZ(dependsOn: jar) doLast {
fileEx.append sha1Hex
}
task proguard(type: proguard.gradle.ProGuardTask, dependsOn: jar) {
task proguard(type: ProGuardTask, dependsOn: jar) {
ext {
def re = jar.classifier
injar = jar.archivePath
@@ -0,0 +1,166 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.AppDataUpgrader;
import org.jackhuang.hmcl.upgrade.IUpgrader;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public final class Launcher extends Application {
@Override
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UPDATE_CHECKER.process(false)
.then(Task.of(Schedulers.javafx(), () -> {
if (UPDATE_CHECKER.isOutOfDate())
Controllers.showUpdate();
}))
.start();
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);
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.LOG.info("*** " + TITLE + " ***");
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
}
public static void stopApplication() {
Logging.LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Platform.exit();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
for (URL u : urls)
try {
File f = new File(u.toURI());
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
result.add(f);
} catch (URISyntaxException e) {
return null;
}
}
if (result.isEmpty())
return null;
else
return result;
}
public static String i18n(String key) {
try {
return RESOURCE_BUNDLE.getString(key);
} catch (Exception e) {
Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
return key;
}
}
public static String i18n(String key, Object... formatArgs) {
return String.format(i18n(key), formatArgs);
}
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 String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
public static final IUpgrader UPGRADER = new AppDataUpgrader();
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final String UPDATE_SERVER = "http://47.101.47.110";
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
+27 -105
View File
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,119 +17,41 @@
*/
package org.jackhuang.hmcl;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.IUpgrader;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import javax.swing.*;
import java.io.File;
import java.util.Arrays;
import java.util.ResourceBundle;
import java.util.logging.Level;
public final class Main extends Application {
@Override
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
primaryStage.show();
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
}
public final class Main {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
checkJavaFX();
checkDirectoryPath();
Launcher.main(args);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDirectory);
}
}
private static void checkJavaFX() {
try {
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
IUpgrader.NOW_UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.LOG.info("*** " + TITLE + " ***");
UPDATE_CHECKER.process(false)
.then(Task.of(Schedulers.javafx(), () -> {
if (UPDATE_CHECKER.isOutOfDate())
Controllers.showUpdate();
}))
.start();
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit("JavaFX is missing.\n"
+ "If you are using Java 11 or above, please downgrade to Java 8 or 10.\n"
+ "If you are using OpenJDK, please ensure OpenJFX is included.");
}
}
public static void stopApplication() {
JFXUtilities.runInFX(() -> {
stopWithoutPlatform();
Platform.exit();
});
private static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
public static void stopWithoutPlatform() {
JFXUtilities.runInFX(() -> {
Controllers.getStage().close();
Logging.LOG.info("Shutting down executor services.");
Schedulers.shutdown();
});
}
public static String i18n(String key) {
try {
return RESOURCE_BUNDLE.getString(key);
} catch (Exception e) {
Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
return key;
}
}
public static String i18n(String key, Object... formatArgs) {
return String.format(i18n(key), formatArgs);
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
}
}
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final File APPDATA = getWorkingDirectory("hmcl");
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final String CONTACT = "http://huangyuhui.duapp.com/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when added accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountAddedEvent extends Event {
private final Account account;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountAddedEvent(Object source, Account account) {
super(source);
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("account", account)
.toString();
}
}
@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountLoadingEvent extends Event {
private final Collection<Account> accounts;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountLoadingEvent(Object source, Collection<Account> accounts) {
super(source);
this.accounts = Collections.unmodifiableCollection(accounts);
}
public Collection<Account> getAccounts() {
return accounts;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("accounts", accounts)
.toString();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.VersionNumber;
/**
@@ -42,4 +43,11 @@ public final class OutOfDateEvent extends Event {
return true;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", getSource())
.append("version", getVersion())
.toString();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when the selected profile changed.
@@ -44,4 +45,11 @@ public final class ProfileChangedEvent extends Event {
return profile;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("profile", profile)
.toString();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,6 +17,12 @@
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading profiles.
* <br>
@@ -26,14 +32,29 @@ package org.jackhuang.hmcl.event;
*/
public class ProfileLoadingEvent extends Event {
private final Collection<Profile> profiles;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public ProfileLoadingEvent(Object source) {
public ProfileLoadingEvent(Object source, Collection<Profile> profiles) {
super(source);
this.profiles = Collections.unmodifiableCollection(profiles);
}
public Collection<Profile> getProfiles() {
return profiles;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("profiles", profiles)
.toString();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -19,10 +19,10 @@ package org.jackhuang.hmcl.game;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.ProfileTexture;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
@@ -30,22 +30,17 @@ import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.io.IOException;
import java.net.Proxy;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.*;
public final class AccountHelper {
public static final AccountHelper INSTANCE = new AccountHelper();
private AccountHelper() {}
public static final File SKIN_DIR = new File(Main.APPDATA, "skins");
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
loadSkins(Proxy.NO_PROXY);
@@ -75,8 +70,8 @@ public final class AccountHelper {
return new SkinLoadTask(account, proxy, true);
}
private static File getSkinFile(String name) {
return new File(SKIN_DIR, name + ".png");
private static File getSkinFile(UUID uuid) {
return new File(SKIN_DIR, uuid + ".png");
}
public static Image getSkin(YggdrasilAccount account) {
@@ -84,12 +79,11 @@ public final class AccountHelper {
}
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
if (account.getSelectedProfile() == null)
return getDefaultSkin(account, scaleRatio);
String name = account.getSelectedProfile().getName();
if (name == null)
return getDefaultSkin(account, scaleRatio);
File file = getSkinFile(name);
UUID uuid = account.getUUID();
if (uuid == null)
return getSteveSkin(scaleRatio);
File file = getSkinFile(uuid);
if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath());
return new Image("file:" + file.getAbsolutePath(),
@@ -97,15 +91,14 @@ public final class AccountHelper {
original.getHeight() * scaleRatio,
false, false);
}
return getDefaultSkin(account, scaleRatio);
return getDefaultSkin(uuid, scaleRatio);
}
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio, Proxy proxy) throws Exception {
String name = profile.getName();
File file = getSkinFile(name);
File file = getSkinFile(profile.getId());
downloadSkin(account, profile, true, proxy);
if (!file.exists())
return getDefaultSkin(account, scaleRatio);
return getDefaultSkin(profile.getId(), scaleRatio);
String url = "file:" + file.getAbsolutePath();
return scale(url, scaleRatio);
@@ -144,21 +137,33 @@ public final class AccountHelper {
@Override
public void execute() throws Exception {
if (account.canLogIn() && (account.getSelectedProfile() == null || refresh))
if (!account.isLoggedIn() && (account.getCharacter() == null || refresh))
DialogController.logIn(account);
downloadSkin(account, account.getSelectedProfile(), refresh, proxy);
downloadSkin(account, refresh, proxy);
}
}
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh, Proxy proxy) throws Exception {
if (profile == null) return;
String name = profile.getName();
if (name == null) return;
Optional<ProfileTexture> texture = account.getSkin(profile);
account.clearCache();
Optional<Texture> texture = account.getSkin(profile);
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(name);
File file = getSkinFile(profile.getId());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
}
private static void downloadSkin(YggdrasilAccount account, boolean refresh, Proxy proxy) throws Exception {
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;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
@@ -180,11 +185,8 @@ public final class AccountHelper {
return scale("/assets/img/alex.png", 4);
}
public static Image getDefaultSkin(Account account, double scaleRatio) {
if (account == null)
return getSteveSkin(scaleRatio);
int type = account.getUUID().hashCode() & 1;
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
int type = uuid.hashCode() & 1;
if (type == 1)
return getAlexSkin(scaleRatio);
else
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DefaultGameBuilder;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
@@ -55,7 +54,7 @@ public class HMCLGameDownloadTask extends Task {
}
@Override
public void execute() throws IOException {
public void execute() {
File jar = profile.getRepository().getVersionJar(version);
File cache = new File(Settings.INSTANCE.getCommonPath(), "jars/" + gameVersion + ".jar");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
@@ -45,7 +45,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
protected void appendJvmArgs(List<String> result) {
super.appendJvmArgs(result);
result.add("-Dminecraft.launcher.version=" + Main.VERSION);
result.add("-Dminecraft.launcher.brand=" + Main.NAME);
result.add("-Dminecraft.launcher.version=" + Launcher.VERSION);
result.add("-Dminecraft.launcher.brand=" + Launcher.NAME);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -26,10 +26,7 @@ import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
@@ -175,10 +172,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
return new File(getVersionRoot(id), "icon.png");
}
public void saveVersionSetting(String id) {
public boolean saveVersionSetting(String id) {
if (!versionSettings.containsKey(id))
return;
Lang.invoke(() -> FileUtils.writeText(getVersionSettingFile(id), GSON.toJson(versionSettings.get(id))));
return false;
File file = getVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
try {
FileUtils.writeText(file, GSON.toJson(versionSettings.get(id)));
return true;
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
return false;
}
}
public boolean forbidsVersion(String id) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -85,7 +85,7 @@ public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
return null;
});
Version mv = repository.getVersion(version).resolve(repository);
Version mv = repository.getResolvedVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
zip.putTextFile(Constants.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,7 +21,10 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CompressingUtils;
@@ -39,7 +42,6 @@ public final class HMCLModpackInstallTask extends Task {
private final String name;
private final HMCLGameRepository repository;
private final Modpack modpack;
private final File run;
private final List<Task> dependencies = new LinkedList<>();
private final List<Task> dependents = new LinkedList<>();
@@ -49,8 +51,8 @@ public final class HMCLModpackInstallTask extends Task {
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
this.run = repository.getRunDirectory(name);
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,12 +20,12 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
@@ -33,7 +33,7 @@ import java.util.List;
*/
public final class HMCLModpackManager {
public static final List<String> MODPACK_BLACK_LIST = Arrays.asList(
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
@@ -43,7 +43,7 @@ public final class HMCLModpackManager {
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
);
public static final List<String> MODPACK_SUGGESTED_BLACK_LIST = Arrays.asList(
public static final List<String> MODPACK_SUGGESTED_BLACK_LIST = Lang.immutableListOf(
"fonts", // BetterFonts
"saves", "servers.dat", "options.txt", // Minecraft
"blueprints" /* BuildCraft */,
@@ -59,7 +59,7 @@ public final class HMCLModpackManager {
return ModAdviser.ModSuggestion.NORMAL;
else
return ModAdviser.ModSuggestion.SUGGESTED;
};
}
private static boolean match(List<String> l, String fileName, boolean isDirectory) {
for (String s : l)
@@ -81,13 +81,9 @@ public final class HMCLModpackManager {
*/
public static Modpack readHMCLModpackManifest(File file) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = Constants.GSON.fromJson(manifestJson, Modpack.class);
if (manifest == null)
throw new JsonParseException("`modpack.json` not found. " + file + " is not a valid HMCL modpack.");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
Version game = Constants.GSON.fromJson(gameJson, Version.class);
if (game == null)
throw new JsonParseException("`minecraft/pack.json` not found. " + file + " iot a valid HMCL modpack.");
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,66 +17,72 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import java.io.*;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class LauncherHelper {
public static final LauncherHelper INSTANCE = new LauncherHelper();
private LauncherHelper(){}
private TaskExecutor executor;
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(() -> {});
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
public void launch(String selectedVersion, File scriptFile) {
Profile profile = Settings.INSTANCE.getSelectedProfile();
GameRepository repository = profile.getRepository();
Account account = Settings.INSTANCE.getSelectedAccount();
public void launch(Profile profile, Account account, String selectedVersion, File scriptFile) {
if (account == null)
throw new IllegalStateException("No account");
throw new IllegalArgumentException("No account");
Version version = repository.getVersion(selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> Schedulers.newThread().schedule(() -> launch0(selectedVersion, scriptFile)));
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile));
});
} catch (InterruptedException ignore) {
}
});
}
private void launch0(String selectedVersion, File scriptFile) {
Profile profile = Settings.INSTANCE.getSelectedProfile();
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
GameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = repository.getVersion(selectedVersion);
Account account = Settings.INSTANCE.getSelectedAccount();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
executor = Task.of(Schedulers.javafx(), () -> Controllers.dialog(launchingStepsPane))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES)))
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
if (setting.isNotCheckGame())
return null;
@@ -86,36 +92,39 @@ public final class LauncherHelper {
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(new CurseCompletionTask(dependencyManager, selectedVersion))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(Main.i18n("account.methods"), variables -> {
.then(Task.of(Launcher.i18n("account.methods"), variables -> {
try {
try {
variables.set("account", account.logIn(new SpecificCharacterSelector(Accounts.getCurrentCharacter(account)), Settings.INSTANCE.getProxy()));
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
if (account.canPlayOffline())
variables.set("account", account.playOffline());
else
throw e;
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
} catch (AuthenticationException e) {
variables.set("account", DialogController.logIn(account));
JFXUtilities.runInFX(() -> Controllers.dialog(launchingStepsPane));
}
}))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository, selectedVersion, variables.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
repository,
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
setting.getLauncherVisibility() == 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())
));
}))
.then(variables -> {
DefaultLauncher launcher = variables.<DefaultLauncher>get("launcher");
DefaultLauncher launcher = variables.get("launcher");
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(Main.i18n("version.launch"));
return new LaunchTask<>(launcher::launch).setName(Launcher.i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(Main.i18n("version.launch_script"));
}).setName(Launcher.i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
@@ -123,41 +132,44 @@ public final class LauncherHelper {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
Main.stopApplication();
Launcher.stopApplication();
else
launchingStepsPane.setCancel(() -> {
launchingStepsPane.setCancel(it -> {
process.stop();
Controllers.closeDialog();
Controllers.closeDialog(it);
});
} else
Platform.runLater(() ->
Controllers.dialog(Main.i18n("version.launch_script.success", scriptFile.getAbsolutePath())));
Platform.runLater(() -> {
Controllers.closeDialog(launchingStepsPane);
Controllers.dialog(Launcher.i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
.executor();
launchingStepsPane.setExecutor(executor);
executor.addTaskListener(new TaskListener() {
AtomicInteger finished = new AtomicInteger(0);
final AtomicInteger finished = new AtomicInteger(0);
@Override
public void onFinished(Task task) {
finished.incrementAndGet();
Platform.runLater(() -> {
launchingStepsPane.setProgress(1.0 * finished.get() / executor.getRunningTasks());
});
int runningTasks = executor.getRunningTasks();
Platform.runLater(() -> launchingStepsPane.setProgress(1.0 * finished.get() / runningTasks));
}
@Override
public void onTerminate() {
Platform.runLater(() -> {
if (executor.getLastException() != null)
Controllers.dialog(I18nException.getStackTrace(executor.getLastException()),
scriptFile == null ? Main.i18n("launch.failed") : Main.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE, Controllers::closeDialog);
else
Controllers.closeDialog();
});
public void onStop(boolean success, TaskExecutor executor) {
if (!success) {
Platform.runLater(() -> {
Controllers.closeDialog(launchingStepsPane);
if (executor.getLastException() != null)
Controllers.dialog(I18nException.getStackTrace(executor.getLastException()),
scriptFile == null ? Launcher.i18n("launch.failed") : Launcher.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
});
}
launchingStepsPane.setExecutor(null);
}
});
@@ -165,64 +177,58 @@ public final class LauncherHelper {
}
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
boolean flag = false, suggest = true;
boolean flag = false;
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(Main.i18n("launch.wrong_javadir"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(Launcher.i18n("launch.wrong_javadir"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
flag = true;
}
if (java.getParsedVersion() < JavaVersion.JAVA_8) {
Controllers.dialog(Main.i18n("launch.advice.newer_java"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
Controllers.dialog(Launcher.i18n("launch.advice.newer_java"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
Controllers.dialog(Main.i18n("launch.advice.java9"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
suggest = false;
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
Controllers.dialog(Launcher.i18n("launch.advice.java9"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
flag = true;
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
org.jackhuang.hmcl.util.Platform.IS_64_BIT) {
Controllers.dialog(Main.i18n("launch.advice.different_platform"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(Launcher.i18n("launch.advice.different_platform"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
Controllers.dialog(Main.i18n("launch.advice.too_large_memory_for_32bit"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(Launcher.i18n("launch.advice.too_large_memory_for_32bit"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(Main.i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(Launcher.i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (flag) {
if (suggest && Controllers.getDialogContent() instanceof MessageDialogPane)
((MessageDialogPane) Controllers.getDialogContent()).disableClosingDialog();
} else
if (!flag)
onAccept.run();
}
public static void stopManagedProcesses() {
synchronized (PROCESSES) {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE)
Controllers.closeDialog();
Controllers.closeDialog(launchingStepsPane);
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
Controllers.dialog(launchingStepsPane);
}
private void checkExit(LauncherVisibility v) {
@@ -239,14 +245,14 @@ public final class LauncherHelper {
Platform.runLater(() -> {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
// If we use Main.stop(), log window will be halt immediately.
Main.stopWithoutPlatform();
// If we use Launcher.stop(), log window will be halt immediately.
Launcher.stopWithoutPlatform();
});
break;
}
}
class LaunchTask<T> extends TaskResult<T> {
private static class LaunchTask<T> extends TaskResult<T> {
private final ExceptionalSupplier<T, Exception> supplier;
public LaunchTask(ExceptionalSupplier<T, Exception> supplier) {
@@ -258,11 +264,11 @@ public final class LauncherHelper {
try {
setResult(supplier.get());
} catch (PermissionException e) {
throw new I18nException(Main.i18n("launch.failed.executable_permission"), e);
throw new I18nException(Launcher.i18n("launch.failed.executable_permission"), e);
} catch (ProcessCreationException e) {
throw new I18nException(Main.i18n("launch.failed.creating_process") + e.getLocalizedMessage(), e);
throw new I18nException(Launcher.i18n("launch.failed.creating_process") + e.getLocalizedMessage(), e);
} catch (NotDecompressingNativesException e) {
throw new I18nException(Main.i18n("launch.failed.decompressing_natives") + e.getLocalizedMessage(), e);
throw new I18nException(Launcher.i18n("launch.failed.decompressing_natives") + e.getLocalizedMessage(), e);
}
}
@@ -298,10 +304,10 @@ public final class LauncherHelper {
if (authInfo == null)
forbiddenTokens = Collections.emptyMap();
else
forbiddenTokens = Lang.mapOf(
new Pair<>(authInfo.getAuthToken(), "<access token>"),
new Pair<>(authInfo.getUserId(), "<uuid>"),
new Pair<>(authInfo.getUsername(), "<player>")
forbiddenTokens = mapOf(
pair(authInfo.getAccessToken(), "<access token>"),
pair(UUIDTypeAdapter.fromUUID(authInfo.getUUID()), "<uuid>"),
pair(authInfo.getUsername(), "<player>")
);
visibility = setting.getLauncherVisibility();
@@ -331,7 +337,7 @@ public final class LauncherHelper {
else
System.out.print(log);
logs.add(new Pair<>(log, level));
logs.add(pair(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
logs.removeFirst();
@@ -354,9 +360,12 @@ public final class LauncherHelper {
});
break;
case CLOSE:
throw new Error("Never come to here");
// Never come to here.
break;
case KEEP:
// No operations here
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
break;
case HIDE:
Platform.runLater(() -> {
@@ -379,10 +388,10 @@ public final class LauncherHelper {
switch (exitType) {
case JVM_ERROR:
logWindow.setTitle(Main.i18n("launch.failed.cannot_create_jvm"));
logWindow.setTitle(Launcher.i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
logWindow.setTitle(Main.i18n("launch.failed.exited_abnormally"));
logWindow.setTitle(Launcher.i18n("launch.failed.exited_abnormally"));
break;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
public enum LoadingState {
DEPENDENCIES("launch.state.dependencies"),
@@ -33,6 +33,6 @@ public enum LoadingState {
}
public String getLocalizedMessage() {
return Main.i18n(key);
return Launcher.i18n(key);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -106,7 +106,7 @@ public final class ModpackHelper {
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
public static Task getUpdateTask(Profile profile, File zipFile, String name, ModpackConfiguration configuration) throws UnsupportedModpackException, MismatchedModpackTypeException, IOException {
public static Task getUpdateTask(Profile profile, File zipFile, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile);
switch (configuration.getType()) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,17 +17,33 @@
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.OfflineAccount;
import org.jackhuang.hmcl.auth.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorBuildInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerResponse;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
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.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author huangyuhui
@@ -37,42 +53,58 @@ public final class Accounts {
public static final String OFFLINE_ACCOUNT_KEY = "offline";
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf(
new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
new Pair<>(YGGDRASIL_ACCOUNT_KEY, YggdrasilAccountFactory.INSTANCE)
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf(
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
);
private static final Map<String, String> AUTHLIB_INJECTOR_SERVER_NAMES = new HashMap<>();
public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
else return YGGDRASIL_ACCOUNT_KEY;
}
public static void setCurrentCharacter(Account account, String character) {
account.getProperties().put("character", character);
}
public static boolean hasCurrentCharacter(Account account) {
return Lang.get(account.getProperties(), "character", String.class, null) != null;
}
public static String getCurrentCharacter(Account account) {
return Lang.get(account.getProperties(), "character", String.class)
.orElseThrow(() -> new IllegalArgumentException("Account " + account + " has not set current character."));
}
public static Optional<String> getCurrentCharacter(Map<Object, Object> storage) {
Optional<Map> properties = Lang.get(storage, "properties", Map.class);
if (!properties.isPresent()) return Optional.empty();
return Lang.get(properties.get(), "character", String.class);
}
static String getAccountId(Account account) {
return getAccountId(account.getUsername(), getCurrentCharacter(account));
return getAccountId(account.getUsername(), account.getCharacter());
}
static String getAccountId(String username, String character) {
return username + ":" + character;
}
private static String downloadAuthlibInjector() throws Exception {
AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo();
File jar = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.jar");
File local = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.txt");
int buildNumber = 0;
try {
buildNumber = Integer.parseInt(FileUtils.readText(local));
} catch (IOException | NumberFormatException ignore) {
}
if (buildNumber < buildInfo.getBuildNumber()) {
new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run();
FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber()));
}
return jar.getAbsolutePath();
}
public static String getAuthlibInjectorServerName(String serverIp) throws Exception {
if (AUTHLIB_INJECTOR_SERVER_NAMES.containsKey(serverIp))
return AUTHLIB_INJECTOR_SERVER_NAMES.get(serverIp);
else {
AuthlibInjectorServerResponse response = Constants.GSON.fromJson(NetworkUtils.doGet(NetworkUtils.toURL(serverIp)), AuthlibInjectorServerResponse.class);
AUTHLIB_INJECTOR_SERVER_NAMES.put(serverIp, response.getMeta().getServerName());
return response.getMeta().getServerName();
}
}
public static TaskResult<String> getAuthlibInjectorServerNameAsync(AuthlibInjectorAccount account) {
return Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(account.getServerBaseURL()));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,265 +17,93 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.util.JavaVersion;
import java.util.LinkedList;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
public final class Config {
import org.jackhuang.hmcl.Launcher;
import com.google.gson.annotations.SerializedName;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
public final class Config implements Cloneable {
@SerializedName("last")
private String selectedProfile = "";
public final StringProperty selectedProfile = new SimpleStringProperty("");
@SerializedName("backgroundType")
private int backgroundImageType = 0;
public final IntegerProperty backgroundImageType = new SimpleIntegerProperty(0);
@SerializedName("bgpath")
private String backgroundImage = null;
public final StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("commonpath")
private String commonDirectory = Main.MINECRAFT_DIRECTORY.getAbsolutePath();
public final StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
@SerializedName("hasProxy")
private boolean hasProxy = false;
public final BooleanProperty hasProxy = new SimpleBooleanProperty();
@SerializedName("hasProxyAuth")
private boolean hasProxyAuth = false;
public final BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
@SerializedName("proxyType")
private int proxyType = 0;
public final IntegerProperty proxyType = new SimpleIntegerProperty();
@SerializedName("proxyHost")
private String proxyHost = null;
public final StringProperty proxyHost = new SimpleStringProperty();
@SerializedName("proxyPort")
private String proxyPort = null;
public final StringProperty proxyPort = new SimpleStringProperty();
@SerializedName("proxyUserName")
private String proxyUser = null;
public final StringProperty proxyUser = new SimpleStringProperty();
@SerializedName("proxyPassword")
private String proxyPass = null;
public final StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("theme")
private String theme = null;
@SerializedName("java")
private List<JavaVersion> java = null;
public final StringProperty theme = new SimpleStringProperty();
@SerializedName("localization")
private String localization;
public final StringProperty localization = new SimpleStringProperty();
@SerializedName("downloadtype")
private int downloadType = 0;
public final IntegerProperty downloadType = new SimpleIntegerProperty();
@SerializedName("configurations")
private Map<String, Profile> configurations = new TreeMap<>();
public final ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private List<Map<Object, Object>> accounts = new LinkedList<>();
public final ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private String selectedAccount = "";
public final StringProperty selectedAccount = new SimpleStringProperty("");
@SerializedName("fontFamily")
private String fontFamily = "Consolas";
public final StringProperty fontFamily = new SimpleStringProperty("Consolas");
@SerializedName("fontSize")
private double fontSize = 12;
public final DoubleProperty fontSize = new SimpleDoubleProperty(12);
@SerializedName("logLines")
private int logLines = 100;
public final IntegerProperty logLines = new SimpleIntegerProperty(100);
public String getSelectedProfile() {
return selectedProfile;
}
@SerializedName("authlibInjectorServerURLs")
public final ObservableSet<String> authlibInjectorServerURLs = FXCollections.observableSet(new HashSet<>());
public void setSelectedProfile(String selectedProfile) {
this.selectedProfile = selectedProfile;
Settings.INSTANCE.save();
}
public String getBackgroundImage() {
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage = backgroundImage;
Settings.INSTANCE.save();
}
public int getBackgroundImageType() {
return backgroundImageType;
}
public void setBackgroundImageType(int backgroundImageType) {
this.backgroundImageType = backgroundImageType;
Settings.INSTANCE.save();
}
public String getCommonDirectory() {
return commonDirectory;
}
public void setCommonDirectory(String commonDirectory) {
this.commonDirectory = commonDirectory;
Settings.INSTANCE.save();
}
public boolean isHasProxy() {
return hasProxy;
}
public void setHasProxy(boolean hasProxy) {
this.hasProxy = hasProxy;
Settings.INSTANCE.save();
}
public boolean isHasProxyAuth() {
return hasProxyAuth;
}
public void setHasProxyAuth(boolean hasProxyAuth) {
this.hasProxyAuth = hasProxyAuth;
Settings.INSTANCE.save();
}
public int getProxyType() {
return proxyType;
}
public void setProxyType(int proxyType) {
this.proxyType = proxyType;
Settings.INSTANCE.save();
}
public String getProxyHost() {
return proxyHost;
}
public void setProxyHost(String proxyHost) {
this.proxyHost = proxyHost;
Settings.INSTANCE.save();
}
public String getProxyPort() {
return proxyPort;
}
public void setProxyPort(String proxyPort) {
this.proxyPort = proxyPort;
Settings.INSTANCE.save();
}
public String getProxyUser() {
return proxyUser;
}
public void setProxyUser(String proxyUser) {
this.proxyUser = proxyUser;
Settings.INSTANCE.save();
}
public String getProxyPass() {
return proxyPass;
}
public void setProxyPass(String proxyPass) {
this.proxyPass = proxyPass;
Settings.INSTANCE.save();
}
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
Settings.INSTANCE.save();
}
public List<JavaVersion> getJava() {
return java;
}
public void setJava(List<JavaVersion> java) {
this.java = java;
Settings.INSTANCE.save();
}
public String getLocalization() {
return localization;
}
public void setLocalization(String localization) {
this.localization = localization;
Settings.INSTANCE.save();
}
public int getDownloadType() {
return downloadType;
}
public void setDownloadType(int downloadType) {
this.downloadType = downloadType;
Settings.INSTANCE.save();
}
public Map<String, Profile> getConfigurations() {
return configurations;
}
public void setConfigurations(Map<String, Profile> configurations) {
this.configurations = configurations;
Settings.INSTANCE.save();
}
public List<Map<Object, Object>> getAccounts() {
return accounts;
}
public void setAccounts(List<Map<Object, Object>> accounts) {
this.accounts = accounts;
Settings.INSTANCE.save();
}
public String getSelectedAccount() {
return selectedAccount;
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount = selectedAccount;
Settings.INSTANCE.save();
}
public String getFontFamily() {
return fontFamily;
}
public void setFontFamily(String fontFamily) {
this.fontFamily = fontFamily;
Settings.INSTANCE.save();
}
public double getFontSize() {
return fontSize;
}
public void setFontSize(double fontSize) {
this.fontSize = fontSize;
Settings.INSTANCE.save();
}
public int getLogLines() {
return logLines;
}
public void setLogLines(int logLines) {
this.logLines = logLines;
@Override
public Config clone() {
return Settings.GSON.fromJson(Settings.GSON.toJson(this), Config.class);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,19 +18,19 @@
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.CurseCDNDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import org.jackhuang.hmcl.util.Lang;
import java.util.Arrays;
import java.util.List;
public final class DownloadProviders {
private DownloadProviders() {}
public static List<DownloadProvider> DOWNLOAD_PROVIDERS = Arrays.asList(MojangDownloadProvider.INSTANCE, BMCLAPIDownloadProvider.INSTANCE);
public static final List<DownloadProvider> DOWNLOAD_PROVIDERS = Lang.immutableListOf(new MojangDownloadProvider(), BMCLAPIDownloadProvider.INSTANCE, CurseCDNDownloadProvider.INSTANCE);
public static DownloadProvider getDownloadProvider(int index) {
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(MojangDownloadProvider.INSTANCE);
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(DOWNLOAD_PROVIDERS.get(0));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.ui.construct.UTF8Control;
import org.jackhuang.hmcl.util.Lang;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@@ -54,7 +53,7 @@ public final class Locales {
*/
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
public static final List<SupportedLocale> LOCALES = Arrays.asList(DEFAULT, EN, ZH_CN);
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN);
public static SupportedLocale getLocale(int index) {
return Lang.get(LOCALES, index).orElse(DEFAULT);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2013 huangyuhui
* Copyright (C) 2018 huangyuhui
*
* 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
@@ -19,12 +19,12 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.HMCLDependencyManager;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.util.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.ImmediateStringProperty;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.io.File;
import java.lang.reflect.Type;
@@ -147,6 +147,14 @@ public final class Profile {
vs.setUsesGlobal(true);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("gameDir", getGameDir())
.append("name", getName())
.toString();
}
public void addPropertyChangedListener(InvalidationListener listener) {
nameProperty.addListener(listener);
globalProperty.addListener(listener);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
public final class Profiles {
private Profiles() {
@@ -26,9 +26,9 @@ public final class Profiles {
public static String getProfileDisplayName(Profile profile) {
switch (profile.getName()) {
case Settings.DEFAULT_PROFILE:
return Main.i18n("profile.default");
return Launcher.i18n("profile.default");
case Settings.HOME_PROFILE:
return Main.i18n("profile.home");
return Launcher.i18n("profile.home");
default:
return profile.getName();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.util.Lang;
import java.net.Proxy;
import java.util.Arrays;
import java.util.List;
/**
@@ -29,7 +28,7 @@ import java.util.List;
public final class Proxies {
private Proxies() {}
public static final List<Proxy.Type> PROXIES = Arrays.asList(Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS);
public static final List<Proxy.Type> PROXIES = Lang.immutableListOf(Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS);
public static Proxy.Type getProxyType(int index) {
return Lang.get(PROXIES, index).orElse(null);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -19,21 +19,24 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.image.Image;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Main;
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.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
@@ -44,68 +47,72 @@ import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class Settings {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.setPrettyPrinting().create();
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()
.create();
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
public static final File SETTINGS_FILE = new File("hmcl.json").getAbsoluteFile();
public static final String SETTINGS_FILE_NAME = "hmcl.json";
public static final File SETTINGS_FILE = new File(SETTINGS_FILE_NAME).getAbsoluteFile();
public static final Settings INSTANCE = new Settings();
private Settings() {}
private final Config SETTINGS = initSettings();
private Map<String, Account> accounts = new HashMap<>();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
{
for (Map<Object, Object> settings : SETTINGS.getAccounts()) {
Optional<String> characterName = Accounts.getCurrentCharacter(settings);
AccountFactory factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class, ""));
if (factory == null || !characterName.isPresent()) {
private Settings() {
loadProxy();
for (Iterator<Map<Object, Object>> iterator = SETTINGS.accounts.iterator(); iterator.hasNext();) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse(""));
if (factory == null) {
// unrecognized account type, so remove it.
SETTINGS.getAccounts().remove(settings);
iterator.remove();
continue;
}
Account account;
try {
account = factory.fromStorage(settings);
account = factory.fromStorage(settings, getProxy());
} catch (Exception e) {
SETTINGS.getAccounts().remove(settings);
// storage is malformed, delete.
iterator.remove();
continue;
}
accounts.put(Accounts.getAccountId(account), account);
}
save();
checkAuthlibInjectorAccounts();
checkProfileMap();
save();
for (Map.Entry<String, Profile> entry2 : getProfileMap().entrySet()) {
entry2.getValue().setName(entry2.getKey());
entry2.getValue().nameProperty().setChangedListener(this::profileNameChanged);
entry2.getValue().addPropertyChangedListener(e -> {
save();
});
entry2.getValue().addPropertyChangedListener(e -> save());
}
Lang.ignoringException(() -> {
Runtime.getRuntime().addShutdownHook(new Thread(this::save));
});
loadProxy();
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
}
private Config initSettings() {
@@ -129,11 +136,11 @@ public class Settings {
public void save() {
try {
SETTINGS.getAccounts().clear();
SETTINGS.accounts.clear();
for (Account account : accounts.values()) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
SETTINGS.getAccounts().add(storage);
SETTINGS.accounts.add(storage);
}
FileUtils.writeText(SETTINGS_FILE, GSON.toJson(SETTINGS));
@@ -142,12 +149,13 @@ public class Settings {
}
}
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.getCommonDirectory()) {
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.commonDirectory.get()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setCommonDirectory(get());
SETTINGS.commonDirectory.set(get());
save();
}
};
@@ -163,7 +171,7 @@ public class Settings {
this.commonPath.set(commonPath);
}
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.getLocalization());
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.localization.get());
public Locales.SupportedLocale getLocale() {
return locale;
@@ -171,7 +179,8 @@ public class Settings {
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
SETTINGS.setLocalization(Locales.getNameByLocale(locale));
SETTINGS.localization.set(Locales.getNameByLocale(locale));
save();
}
private Proxy proxy = Proxy.NO_PROXY;
@@ -180,7 +189,7 @@ public class Settings {
return proxy;
}
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.getProxyType());
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.proxyType.get());
public Proxy.Type getProxyType() {
return proxyType;
@@ -188,49 +197,64 @@ public class Settings {
public void setProxyType(Proxy.Type proxyType) {
this.proxyType = proxyType;
SETTINGS.setProxyType(Proxies.PROXIES.indexOf(proxyType));
SETTINGS.proxyType.set(Proxies.PROXIES.indexOf(proxyType));
save();
loadProxy();
}
public String getProxyHost() {
return SETTINGS.getProxyHost();
return SETTINGS.proxyHost.get();
}
public void setProxyHost(String proxyHost) {
SETTINGS.setProxyHost(proxyHost);
SETTINGS.proxyHost.set(proxyHost);
save();
}
public String getProxyPort() {
return SETTINGS.getProxyPort();
return SETTINGS.proxyPort.get();
}
public void setProxyPort(String proxyPort) {
SETTINGS.setProxyPort(proxyPort);
SETTINGS.proxyPort.set(proxyPort);
save();
}
public String getProxyUser() {
return SETTINGS.getProxyUser();
return SETTINGS.proxyUser.get();
}
public void setProxyUser(String proxyUser) {
SETTINGS.setProxyUser(proxyUser);
SETTINGS.proxyUser.set(proxyUser);
save();
}
public String getProxyPass() {
return SETTINGS.getProxyPass();
return SETTINGS.proxyPass.get();
}
public void setProxyPass(String proxyPass) {
SETTINGS.setProxyPass(proxyPass);
SETTINGS.proxyPass.set(proxyPass);
save();
}
public boolean hasProxy() { return SETTINGS.isHasProxy(); }
public boolean hasProxy() {
return SETTINGS.hasProxy.get();
}
public void setHasProxy(boolean hasProxy) { SETTINGS.setHasProxy(hasProxy); }
public void setHasProxy(boolean hasProxy) {
SETTINGS.hasProxy.set(hasProxy);
save();
}
public boolean hasProxyAuth() { return SETTINGS.isHasProxyAuth(); }
public boolean hasProxyAuth() {
return SETTINGS.hasProxyAuth.get();
}
public void setHasProxyAuth(boolean hasProxyAuth) { SETTINGS.setHasProxyAuth(hasProxyAuth); }
public void setHasProxyAuth(boolean hasProxyAuth) {
SETTINGS.hasProxyAuth.set(hasProxyAuth);
save();
}
private void loadProxy() {
String host = getProxyHost();
@@ -259,47 +283,75 @@ public class Settings {
}
public Font getFont() {
return Font.font(SETTINGS.getFontFamily(), SETTINGS.getFontSize());
return Font.font(SETTINGS.fontFamily.get(), SETTINGS.fontSize.get());
}
public void setFont(Font font) {
SETTINGS.setFontFamily(font.getFamily());
SETTINGS.setFontSize(font.getSize());
SETTINGS.fontFamily.set(font.getFamily());
SETTINGS.fontSize.set(font.getSize());
save();
}
public int getLogLines() {
return Math.max(SETTINGS.getLogLines(), 100);
return Math.max(SETTINGS.logLines.get(), 100);
}
public void setLogLines(int logLines) {
SETTINGS.setLogLines(logLines);
SETTINGS.logLines.set(logLines);
save();
}
public DownloadProvider getDownloadProvider() {
switch (SETTINGS.getDownloadType()) {
case 0:
return MojangDownloadProvider.INSTANCE;
case 1:
return BMCLAPIDownloadProvider.INSTANCE;
default:
return MojangDownloadProvider.INSTANCE;
/****************************************
* AUTHLIB INJECTORS *
****************************************/
public Set<String> getAuthlibInjectorServerURLs() {
return SETTINGS.authlibInjectorServerURLs;
}
public void removeAuthlibInjectorServerURL(String serverURL) {
SETTINGS.authlibInjectorServerURLs.remove(serverURL);
checkAuthlibInjectorAccounts();
save();
}
public void addAuthlibInjectorServerURL(String serverURL) {
SETTINGS.authlibInjectorServerURLs.add(serverURL);
save();
}
private void checkAuthlibInjectorAccounts() {
for (Account account : getAccounts()) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount injectorAccount = (AuthlibInjectorAccount) account;
if (!SETTINGS.authlibInjectorServerURLs.contains(injectorAccount.getServerBaseURL()))
deleteAccount(account);
}
}
}
/****************************************
* DOWNLOAD PROVIDERS *
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(SETTINGS.downloadType.get());
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
if (downloadProvider == MojangDownloadProvider.INSTANCE)
SETTINGS.setDownloadType(0);
else if (downloadProvider == BMCLAPIDownloadProvider.INSTANCE)
SETTINGS.setDownloadType(1);
else
int index = DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(downloadProvider);
if (index == -1)
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
SETTINGS.downloadType.set(index);
save();
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.getSelectedAccount())) {
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.selectedAccount.get())) {
@Override
public Account get() {
Account a = super.get();
@@ -321,7 +373,8 @@ public class Settings {
public void invalidated() {
super.invalidated();
SETTINGS.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
SETTINGS.selectedAccount.set(getValue() == null ? "" : Accounts.getAccountId(getValue()));
save();
}
};
@@ -339,6 +392,9 @@ public class Settings {
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
}
public Account getAccount(String name, String character) {
@@ -352,12 +408,14 @@ public class Settings {
public void deleteAccount(String name, String character) {
accounts.remove(Accounts.getAccountId(name, character));
onAccountLoading();
selectedAccount.get();
}
public void deleteAccount(Account account) {
accounts.remove(Accounts.getAccountId(account));
onAccountLoading();
selectedAccount.get();
}
@@ -365,12 +423,13 @@ public class Settings {
* BACKGROUND *
****************************************/
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.getBackgroundImage()) {
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.backgroundImage.get()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setBackgroundImage(get());
SETTINGS.backgroundImage.set(get());
save();
}
};
@@ -386,12 +445,13 @@ public class Settings {
this.backgroundImage.set(backgroundImage);
}
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.getBackgroundImageType())) {
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.backgroundImageType.get())) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setBackgroundImageType(get().ordinal());
SETTINGS.backgroundImageType.set(get().ordinal());
save();
}
};
@@ -411,12 +471,13 @@ public class Settings {
* THEME *
****************************************/
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.getTheme()).orElse(Theme.BLUE)) {
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.theme.get()).orElse(Theme.BLUE)) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setTheme(get().name().toLowerCase());
SETTINGS.theme.set(get().getName().toLowerCase());
save();
}
};
@@ -436,21 +497,23 @@ public class Settings {
* PROFILES *
****************************************/
private Profile selectedProfile;
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(getProfileMap().keySet().stream().findFirst().get());
if (!hasProfile(SETTINGS.selectedProfile.get())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
SETTINGS.selectedProfile.set(selectedProfile);
save();
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(SETTINGS.getSelectedProfile());
return getProfile(SETTINGS.selectedProfile.get());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(selectedProfile.getName());
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.selectedProfile.get())) {
SETTINGS.selectedProfile.set(selectedProfile.getName());
save();
Schedulers.computation().schedule(this::onProfileChanged);
}
}
@@ -467,7 +530,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.getConfigurations();
return SETTINGS.configurations;
}
public Collection<Profile> getProfiles() {
@@ -479,8 +542,11 @@ public class Settings {
throw new IllegalArgumentException("Profile's name is empty");
getProfileMap().put(ver.getName(), ver);
Schedulers.computation().schedule(this::onProfileLoading);
ver.nameProperty().setChangedListener(this::profileNameChanged);
save();
}
public void deleteProfile(Profile profile) {
@@ -496,13 +562,13 @@ public class Settings {
private void checkProfileMap() {
if (getProfileMap().isEmpty()) {
getProfileMap().put(DEFAULT_PROFILE, new Profile(DEFAULT_PROFILE));
getProfileMap().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.MINECRAFT_DIRECTORY));
getProfileMap().put(HOME_PROFILE, new Profile(HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY));
}
}
private void onProfileChanged() {
EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(this, getSelectedProfile()));
getSelectedProfile().getRepository().refreshVersionsAsync().start();
EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(SETTINGS, getSelectedProfile()));
}
private void profileNameChanged(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {
@@ -514,7 +580,15 @@ public class Settings {
* Invoked by loading GUI phase.
*/
public void onProfileLoading() {
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(SETTINGS));
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
onProfileChanged();
}
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
public Config getRawConfig() {
return SETTINGS.clone();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,29 +17,114 @@
*/
package org.jackhuang.hmcl.setting;
import java.util.Optional;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
public enum Theme {
BLUE,
DARK_BLUE,
GREEN,
ORANGE,
PURPLE,
RED;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.logging.Level;
public class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
public static final Theme DARK_BLUE = new Theme("dark_blue", "#283593");
public static final Theme GREEN = new Theme("green", "#43A047");
public static final Theme ORANGE = new Theme("orange", "#E67E22");
public static final Theme PURPLE = new Theme("purple", "#9C27B0");
public static final Theme RED = new Theme("red", "#F44336");
public static final Theme[] VALUES = new Theme[]{BLUE, DARK_BLUE, GREEN, ORANGE, PURPLE, RED};
private final String color;
private final String name;
Theme(String name, String color) {
this.name = name;
this.color = color;
}
public String getName() {
return name;
}
public String getColor() {
return color;
}
public boolean isCustom() {
return name.startsWith("#");
}
public boolean isLight() {
return Color.web(color).grayscale().getRed() >= 0.5;
}
public Color getForegroundColor() {
return isLight() ? Color.BLACK : Color.WHITE;
}
public String[] getStylesheets() {
String name = isCustom() ? BLUE.getName() : this.name;
String css = Theme.class.getResource("/assets/css/" + name + ".css").toExternalForm();
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet", e);
}
return new String[]{
Theme.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
Theme.class.getResource("/css/jfoenix-design.css").toExternalForm(),
Theme.class.getResource("/assets/css/" + name().toLowerCase() + ".css").toExternalForm(),
css,
Theme.class.getResource("/assets/css/root.css").toExternalForm()
};
}
public static Theme custom(String color) {
if (!color.startsWith("#"))
throw new IllegalArgumentException();
return new Theme(color, color);
}
public static Optional<Theme> getTheme(String name) {
for (Theme theme : values())
if (theme.name().equalsIgnoreCase(name))
if (name == null)
return Optional.empty();
for (Theme theme : VALUES)
if (theme.name.equalsIgnoreCase(name))
return Optional.of(theme);
if (name.startsWith("#"))
try {
Color.web(name);
return Optional.of(custom(name));
} catch (IllegalArgumentException ignore) {
}
return Optional.empty();
}
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> Settings.INSTANCE.getTheme().getForegroundColor(), Settings.INSTANCE.themeProperty());
}
public static ObjectBinding<Color> blackFillBinding() {
return Bindings.createObjectBinding(() -> Color.BLACK);
}
public static ObjectBinding<Color> whiteFillBinding() {
return Bindings.createObjectBinding(() -> Color.WHITE);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2013 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -19,14 +19,16 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
*
@@ -87,6 +89,20 @@ public final class VersionSetting {
javaProperty.set(java);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
* It's used to determine which JRE to use when multiple JREs match the selected Java version.
*/
public String getDefaultJavaPath() {
return defaultJavaPathProperty.get();
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
@@ -138,7 +154,7 @@ public final class VersionSetting {
permSizeProperty.set(permSize);
}
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", (int) OperatingSystem.SUGGESTED_MEMORY);
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
public ImmediateIntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
@@ -444,12 +460,18 @@ public final class VersionSetting {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
JavaVersion c = JavaVersion.getJREs().get(getJava());
if (c == null) {
List<JavaVersion> matchedJava = JavaVersion.getJREs().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
setJava("Default");
return JavaVersion.fromCurrentEnvironment();
} else
return c;
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
}
@@ -475,6 +497,7 @@ public final class VersionSetting {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
@@ -482,8 +505,8 @@ public final class VersionSetting {
return new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Main.TITLE)
.setProfileName(Main.TITLE)
.setVersionName(Launcher.TITLE)
.setProfileName(Launcher.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
@@ -535,6 +558,7 @@ public final class VersionSetting {
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
return obj;
}
@@ -571,6 +595,7 @@ public final class VersionSetting {
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)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
return vs;
}
@@ -1,147 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
public final class AccountItem extends StackPane {
private final Account account;
@FXML
private Pane icon;
@FXML private VBox content;
@FXML private StackPane header;
@FXML private StackPane body;
@FXML private JFXButton btnDelete;
@FXML private JFXButton btnRefresh;
@FXML private Label lblUser;
@FXML private Label lblType;
@FXML private Label lblEmail;
@FXML private Label lblCurrentAccount;
@FXML private JFXRadioButton chkSelected;
@FXML private JFXProgressBar pgsSkin;
@FXML private ImageView portraitView;
@FXML private HBox buttonPane;
public AccountItem(int i, Account account, ToggleGroup toggleGroup) {
this.account = account;
FXUtils.loadFXML(this, "/assets/fxml/account-item.fxml");
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -0.5, 1.0));
chkSelected.setToggleGroup(toggleGroup);
btnDelete.setGraphic(SVG.delete("black", 15, 15));
btnRefresh.setGraphic(SVG.refresh("black", 15, 15));
// create content
String headerColor = getDefaultColor(i % 12);
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor);
// create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 32.0, header.boundsInParentProperty(), icon.heightProperty()));
chkSelected.getProperties().put("account", account);
setSelected(Settings.INSTANCE.getSelectedAccount() == account);
lblUser.setText(Accounts.getCurrentCharacter(account));
lblType.setText(AccountsPage.accountType(account));
lblEmail.setText(account.getUsername());
if (account instanceof YggdrasilAccount) {
btnRefresh.setOnMouseClicked(e -> {
pgsSkin.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), this::loadSkin);
});
AccountHelper.loadSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), this::loadSkin);
} else
loadSkin();
if (account instanceof OfflineAccount) { // Offline Account cannot be refreshed,
buttonPane.getChildren().remove(btnRefresh);
}
}
private void loadSkin() {
pgsSkin.setVisible(false);
portraitView.setViewport(AccountHelper.getViewport(4));
if (account instanceof YggdrasilAccount)
portraitView.setImage(AccountHelper.getSkin((YggdrasilAccount) account, 4));
else
portraitView.setImage(AccountHelper.getDefaultSkin(account, 4));
FXUtils.limitSize(portraitView, 32, 32);
}
private String getDefaultColor(int i) {
switch (i) {
case 0: return "#8F3F7E";
case 1: return "#B5305F";
case 2: return "#CE584A";
case 3: return "#DB8D5C";
case 4: return "#DA854E";
case 5: return "#E9AB44";
case 6: return "#FEE435";
case 7: return "#99C286";
case 8: return "#01A05E";
case 9: return "#4A8895";
case 10: return "#16669B";
case 11: return "#2F65A5";
case 12: return "#4E6A9C";
default: return "#FFFFFF";
}
}
public Account getAccount() {
return account;
}
public void setSelected(boolean selected) {
lblCurrentAccount.setVisible(selected);
chkSelected.setSelected(selected);
}
public void setOnDeleteButtonMouseClicked(EventHandler<? super MouseEvent> eventHandler) {
btnDelete.setOnMouseClicked(eventHandler);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,41 +17,39 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.function.Consumer;
public class YggdrasilAccountLoginPane extends StackPane {
private final YggdrasilAccount oldAccount;
public class AccountLoginPane extends StackPane {
private final Account oldAccount;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final Consumer<Region> closeConsumer;
@FXML
private Label lblUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
private JFXDialog dialog;
public YggdrasilAccountLoginPane(YggdrasilAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) {
public AccountLoginPane(Account oldAccount, Consumer<Region> closeConsumer, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;
this.closeConsumer = closeConsumer;
FXUtils.loadFXML(this, "/assets/fxml/yggdrasil-account-login.fxml");
FXUtils.loadFXML(this, "/assets/fxml/account-login.fxml");
lblUsername.setText(oldAccount.getUsername());
txtPassword.setOnAction(e -> onAccept());
@@ -59,14 +57,12 @@ public class YggdrasilAccountLoginPane extends StackPane {
@FXML
private void onAccept() {
String username = oldAccount.getUsername();
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("login", () -> {
try {
Account account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password);
return account.logIn(new SpecificCharacterSelector(Accounts.getCurrentCharacter(oldAccount)), Settings.INSTANCE.getProxy());
return oldAccount.logInWithPassword(password);
} catch (Exception e) {
return e;
}
@@ -74,11 +70,11 @@ public class YggdrasilAccountLoginPane extends StackPane {
Object account = variable.get("login");
if (account instanceof AuthInfo) {
success.accept(((AuthInfo) account));
dialog.close();
closeConsumer.accept(this);
} else if (account instanceof NoSelectedCharacterException) {
dialog.close();
closeConsumer.accept(this);
} else if (account instanceof Exception) {
lblCreationWarning.setText(AccountsPage.accountException((Exception) account));
lblCreationWarning.setText(AddAccountPane.accountException((Exception) account));
}
progressBar.setVisible(false);
@@ -88,10 +84,6 @@ public class YggdrasilAccountLoginPane extends StackPane {
@FXML
private void onCancel() {
failed.run();
dialog.close();
}
public void setDialog(JFXDialog dialog) {
this.dialog = dialog;
closeConsumer.accept(this);
}
}
@@ -0,0 +1,152 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
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.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.Optional;
public class AccountPage extends StackPane implements DecoratorPage {
private final StringProperty title;
private final ObjectProperty<Runnable> onDelete = new SimpleObjectProperty<>(this, "onDelete");
private final VersionListItem item;
private final Account account;
@FXML
private Label lblType;
@FXML
private Label lblServer;
@FXML
private Label lblCharacter;
@FXML
private Label lblEmail;
@FXML
private BorderPane paneServer;
@FXML
private BorderPane paneEmail;
@FXML
private ComponentList componentList;
@FXML
private JFXButton btnDelete;
@FXML
private JFXButton btnRefresh;
@FXML
private JFXProgressBar progressBar;
public AccountPage(Account account, VersionListItem item) {
this.account = account;
this.item = item;
title = new SimpleStringProperty(this, "title", Launcher.i18n("account") + " - " + account.getCharacter());
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
FXUtils.setLimitWidth(this, 300);
if (account instanceof AuthlibInjectorAccount) {
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
.subscribe(Schedulers.javafx(), variables -> lblServer.setText(variables.get("serverName")));
FXUtils.setLimitHeight(this, 182);
} else {
componentList.removeChildren(paneServer);
if (account instanceof OfflineAccount) {
componentList.removeChildren(paneEmail);
FXUtils.setLimitHeight(this, 110);
} else
FXUtils.setLimitHeight(this, 145);
}
btnDelete.setGraphic(SVG.delete(Theme.blackFillBinding(), 15, 15));
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
lblCharacter.setText(account.getCharacter());
lblType.setText(AddAccountPane.accountType(account));
lblEmail.setText(account.getUsername());
btnRefresh.setVisible(account instanceof YggdrasilAccount);
}
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
}
@FXML
private void onRefresh() {
if (account instanceof YggdrasilAccount) {
progressBar.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
if (isDependentsSucceeded) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
}
}).start();
}
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public Runnable getOnDelete() {
return onDelete.get();
}
public ObjectProperty<Runnable> onDeleteProperty() {
return onDelete;
}
public void setOnDelete(Runnable onDelete) {
this.onDelete.set(onDelete);
}
}
@@ -1,281 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
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.Main;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.InvalidCredentialsException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public final class AccountsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("account"));
@FXML
private ScrollPane scrollPane;
@FXML private JFXMasonryPane masonryPane;
@FXML private JFXDialog dialog;
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXProgressBar progressBar;
{
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
getChildren().remove(dialog);
dialog.setDialogContainer(this);
FXUtils.smoothScrolling(scrollPane);
cboType.getItems().setAll(Main.i18n("account.methods.offline"), Main.i18n("account.methods.yggdrasil"));
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
txtPassword.setVisible(newValue.intValue() != 0);
});
cboType.getSelectionModel().select(0);
txtPassword.setOnAction(e -> onCreationAccept());
txtUsername.setOnAction(e -> onCreationAccept());
txtUsername.getValidators().add(new Validator(Main.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), account -> {
for (Node node : masonryPane.getChildren())
if (node instanceof AccountItem)
((AccountItem) node).setSelected(account == ((AccountItem) node).getAccount());
});
loadAccounts();
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
public void loadAccounts() {
List<Node> children = new LinkedList<>();
int i = 0;
ToggleGroup group = new ToggleGroup();
for (Account account : Settings.INSTANCE.getAccounts()) {
children.add(buildNode(++i, account, group));
}
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (newValue != null)
Settings.INSTANCE.setSelectedAccount((Account) newValue.getProperties().get("account"));
});
FXUtils.resetChildren(masonryPane, children);
Platform.runLater(() -> {
masonryPane.requestLayout();
scrollPane.requestLayout();
});
}
private Node buildNode(int i, Account account, ToggleGroup group) {
AccountItem item = new AccountItem(i, account, group);
item.setOnDeleteButtonMouseClicked(e -> {
Settings.INSTANCE.deleteAccount(account);
Platform.runLater(this::loadAccounts);
});
return item;
}
@FXML
private void addNewAccount() {
txtUsername.setText("");
txtPassword.setText("");
dialog.show();
}
@FXML
private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText();
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
try {
Account account;
switch (type) {
case 0: account = OfflineAccountFactory.INSTANCE.fromUsername(username); break;
case 1: account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password); break;
default: throw new Error();
}
AuthInfo info = account.logIn(new CharacterSelector(), Settings.INSTANCE.getProxy());
Accounts.setCurrentCharacter(account, info.getUsername());
return account;
} catch (Exception e) {
return e;
}
}).subscribe(Schedulers.javafx(), variables -> {
Object account = variables.get("create_account");
if (account instanceof Account) {
Settings.INSTANCE.addAccount((Account) account);
dialog.close();
loadAccounts();
} else if (account instanceof NoSelectedCharacterException) {
dialog.close();
} else if (account instanceof Exception) {
lblCreationWarning.setText(accountException((Exception) account));
}
progressBar.setVisible(false);
});
}
@FXML
private void onCreationCancel() {
dialog.close();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public static String accountException(Exception account) {
if (account instanceof InvalidCredentialsException) {
return Main.i18n("account.failed.invalid_credentials");
} else if (account instanceof NoCharacterException) {
return Main.i18n("account.failed.no_charactor");
} else if (account instanceof ServerDisconnectException) {
return Main.i18n("account.failed.connect_authentication_server");
} else if (account instanceof InvalidTokenException) {
return Main.i18n("account.failed.invalid_token");
} else if (account instanceof InvalidPasswordException) {
return Main.i18n("account.failed.invalid_password");
} else {
return account.getClass() + ": " + ((Exception) account).getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return Main.i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return Main.i18n("account.methods.yggdrasil");
else throw new Error(Main.i18n("account.methods.no_method") + ": " + account);
}
class CharacterSelector extends BorderPane implements MultiCharacterSelector {
private AdvancedListBox listBox = new AdvancedListBox();
private JFXButton cancel = new JFXButton();
private CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
{
setStyle("-fx-padding: 8px;");
cancel.setText(Main.i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(Main.i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount))
return MultiCharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
image = FXUtils.DEFAULT_ICON;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == FXUtils.DEFAULT_ICON)
portraitView.setImage(FXUtils.DEFAULT_ICON);
else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> Controllers.dialog(this));
try {
latch.await();
JFXUtilities.runInFX(Controllers::closeDialog);
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account);
}
}
}
}
@@ -0,0 +1,286 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
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.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Logging;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept;
@FXML private StackPane acceptPane;
@FXML private JFXSpinner spinnerAccept;
private final Consumer<Region> finalization;
private final TransitionHandler transitionHandler;
public AddAccountPane(Consumer<Region> finalization) {
this.finalization = finalization;
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
transitionHandler = new TransitionHandler(acceptPane);
acceptPane.getChildren().setAll(btnAccept);
loadServers();
cboType.getItems().setAll(Launcher.i18n("account.methods.offline"), Launcher.i18n("account.methods.yggdrasil"), Launcher.i18n("account.methods.authlib_injector"));
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
txtPassword.setVisible(newValue.intValue() != 0);
lblPassword.setVisible(newValue.intValue() != 0);
cboServers.setVisible(newValue.intValue() == 2);
linkManageInjectorServers.setVisible(newValue.intValue() == 2);
lblInjectorServer.setVisible(newValue.intValue() == 2);
validateAcceptButton();
});
cboType.getSelectionModel().select(0);
// These two lines can eliminate black, don't know why.
cboServers.getItems().setAll(new TwoLineListItem("", ""));
cboServers.getSelectionModel().select(0);
txtPassword.setOnAction(e -> onCreationAccept());
txtUsername.setOnAction(e -> onCreationAccept());
txtUsername.getValidators().add(new Validator(Launcher.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
txtUsername.textProperty().addListener(it -> validateAcceptButton());
txtPassword.textProperty().addListener(it -> validateAcceptButton());
}
private void validateAcceptButton() {
btnAccept.setDisable(!txtUsername.validate() || (cboType.getSelectionModel().getSelectedIndex() != 0 && !txtPassword.validate()));
}
private void loadServers() {
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.flatMap(serverURL -> {
try {
return Stream.of(new TwoLineListItem(Accounts.getAuthlibInjectorServerName(serverURL), serverURL));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Authlib-injector server root " + serverURL + " cannot be recognized.", e);
return Stream.empty();
}
})
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
cboServers.getItems().setAll(variables.<Collection<TwoLineListItem>>get("list"));
if (!cboServers.getItems().isEmpty())
cboServers.getSelectionModel().select(0);
}));
}
private void showSpinner() {
transitionHandler.setContent(spinnerAccept, ContainerAnimations.FADE.getAnimationProducer());
}
private void hideSpinner() {
transitionHandler.setContent(btnAccept, ContainerAnimations.FADE.getAnimationProducer());
}
@FXML
private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText();
String password = txtPassword.getText();
String apiRoot = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).map(TwoLineListItem::getSubtitle).orElse(null);
showSpinner();
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
AccountFactory<?> factory;
switch (type) {
case 0: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY); break;
case 1: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY); break;
case 2: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY); break;
default: throw new Error();
}
return factory.create(new Selector(), username, password, apiRoot, Settings.INSTANCE.getProxy());
}).finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
hideSpinner();
finalization.accept(this);
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
finalization.accept(this);
} else {
lblCreationWarning.setText(accountException(exception));
}
hideSpinner();
}).start();
}
@FXML
private void onCreationCancel() {
finalization.accept(this);
}
@FXML
private void onManageInjecterServers() {
finalization.accept(this);
Controllers.navigate(Controllers.getServersPage());
}
private void showSelector(Node node) {
getChildren().setAll(node);
}
private void closeSelector() {
getChildren().setAll(layout);
}
private class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
{
setStyle("-fx-padding: 8px;");
cancel.setText(Launcher.i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(Launcher.i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount))
return CharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = FXUtils.DEFAULT_ICON;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == FXUtils.DEFAULT_ICON)
portraitView.setImage(FXUtils.DEFAULT_ICON);
else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> showSelector(this));
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
JFXUtilities.runInFX(AddAccountPane.this::closeSelector);
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account);
}
}
}
public static String accountException(Exception exception) {
if (exception instanceof InvalidCredentialsException) {
return Launcher.i18n("account.failed.invalid_credentials");
} else if (exception instanceof NoCharacterException) {
return Launcher.i18n("account.failed.no_charactor");
} else if (exception instanceof ServerDisconnectException) {
return Launcher.i18n("account.failed.connect_authentication_server");
} else if (exception instanceof InvalidTokenException) {
return Launcher.i18n("account.failed.invalid_token");
} else if (exception instanceof InvalidPasswordException) {
return Launcher.i18n("account.failed.invalid_password");
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return Launcher.i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return Launcher.i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return Launcher.i18n("account.methods.yggdrasil");
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
}
}
@@ -0,0 +1,64 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.setting.Theme;
import java.util.function.Consumer;
public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServerInfo info;
private final Label lblServerName = new Label();
private final Label lblServerIp = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServerInfo info, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.info = info;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;");
VBox center = new VBox();
BorderPane.setAlignment(center, Pos.CENTER);
center.getChildren().addAll(lblServerName, lblServerIp);
setCenter(center);
JFXButton right = new JFXButton();
right.setOnMouseClicked(e -> deleteCallback.accept(this));
right.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(right, Pos.CENTER);
right.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(right);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(info.getServerName());
lblServerIp.setText(info.getServerIp());
}
public AuthlibInjectorServerInfo getInfo() {
return info;
}
}
@@ -0,0 +1,163 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
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.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Collection;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("account.injector.server"));
@FXML private ScrollPane scrollPane;
@FXML private StackPane addServerContainer;
@FXML private Label lblServerIp;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@FXML private Label lblServerWarning;
@FXML private VBox listPane;
@FXML private JFXTextField txtServerIp;
@FXML private JFXDialogLayout addServerPane;
@FXML private JFXDialogLayout confirmServerPane;
@FXML private JFXDialog dialog;
@FXML private StackPane contentPane;
@FXML private JFXSpinner spinner;
@FXML private JFXProgressBar progressBar;
@FXML private JFXButton btnAddNext;
private final TransitionHandler transitionHandler;
{
FXUtils.loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
FXUtils.smoothScrolling(scrollPane);
transitionHandler = new TransitionHandler(addServerContainer);
getChildren().remove(dialog);
dialog.setDialogContainer(this);
txtServerIp.textProperty().addListener((a, b, newValue) ->
btnAddNext.setDisable(!txtServerIp.validate()));
loading();
}
private void removeServer(AuthlibInjectorServerItem item) {
Settings.INSTANCE.removeAuthlibInjectorServerURL(item.getInfo().getServerIp());
loading();
}
private void loading() {
getChildren().remove(contentPane);
spinner.setVisible(true);
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.flatMap(serverURL -> {
try {
return Stream.of(new AuthlibInjectorServerItem(new AuthlibInjectorServerInfo(serverURL, Accounts.getAuthlibInjectorServerName(serverURL)), this::removeServer));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Authlib-injector server root " + serverURL + " cannot be recognized.", e);
return Stream.empty();
}
})
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
listPane.getChildren().setAll(variables.<Collection<? extends Node>>get("list"));
loadingCompleted();
}));
}
private void loadingCompleted() {
getChildren().add(contentPane);
spinner.setVisible(false);
if (Settings.INSTANCE.getAuthlibInjectorServerURLs().isEmpty())
onAdd();
}
@FXML
private void onAdd() {
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
txtServerIp.setText("");
addServerPane.setDisable(false);
progressBar.setVisible(false);
dialog.show();
}
@FXML
private void onAddCancel() {
dialog.close();
}
@FXML
private void onAddNext() {
String serverIp = txtServerIp.getText();
progressBar.setVisible(true);
addServerPane.setDisable(true);
Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(serverIp))
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
addServerPane.setDisable(false);
if (isDependentsSucceeded) {
lblServerName.setText(variables.get("serverName"));
lblServerIp.setText(txtServerIp.getText());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverIp).getProtocol()));
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else
lblCreationWarning.setText(variables.<Exception>get("lastException").getLocalizedMessage());
}).start();
}
@FXML
private void onAddPrev() {
transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
}
@FXML
private void onAddFinish() {
String ip = txtServerIp.getText();
if (!ip.endsWith("/"))
ip += "/";
Settings.INSTANCE.addAuthlibInjectorServerURL(ip);
loading();
dialog.close();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,13 +17,12 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -39,9 +38,10 @@ public final class Controllers {
private static Scene scene;
private static Stage stage;
private static MainPage mainPage = new MainPage();
private static MainPage mainPage = null;
private static SettingsPage settingsPage = null;
private static VersionPage versionPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static LeftPaneController leftPaneController;
private static Decorator decorator;
@@ -67,11 +67,21 @@ public final class Controllers {
return versionPage;
}
// FXThread
public static AuthlibInjectorServersPage getServersPage() {
if (serversPage == null)
serversPage = new AuthlibInjectorServersPage();
return serversPage;
}
// FXThread
public static Decorator getDecorator() {
return decorator;
}
public static MainPage getMainPage() {
if (mainPage == null)
mainPage = new MainPage();
return mainPage;
}
@@ -82,7 +92,9 @@ public final class Controllers {
public static void initialize(Stage stage) {
Controllers.stage = stage;
decorator = new Decorator(stage, mainPage, Main.TITLE, false, true);
stage.setOnCloseRequest(e -> Launcher.stopApplication());
decorator = new Decorator(stage, getMainPage(), Launcher.TITLE, false, true);
decorator.showPage(null);
leftPaneController = new LeftPaneController(decorator.getLeftPane());
@@ -92,24 +104,19 @@ public final class Controllers {
decorator.setCustomMaximize(false);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
scene.getStylesheets().setAll(Settings.INSTANCE.getTheme().getStylesheets());
stage.setMinWidth(804);
stage.setMaxWidth(804);
stage.setMinHeight(521);
stage.setMaxHeight(521);
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.setTitle(Main.TITLE);
stage.setTitle(Launcher.TITLE);
}
public static Region getDialogContent() {
return decorator.getDialog().getContent();
}
public static JFXDialog dialog(Region content) {
// TODO: temp fix
decorator.showDialog(new Region());
return decorator.showDialog(content);
public static void dialog(Region content) {
if (decorator != null)
decorator.showDialog(content);
}
public static void dialog(String text) {
@@ -125,28 +132,33 @@ public final class Controllers {
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, decorator.getDialog(), type, onAccept));
dialog(new MessageDialogPane(text, title, Controllers::closeDialog, type, onAccept));
}
public static void confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, decorator.getDialog(), onAccept, onCancel));
dialog(new MessageDialogPane(text, title, Controllers::closeDialog, onAccept, onCancel));
}
public static void inputDialog(String text, Consumer<String> onResult) {
dialog(new InputDialogPane(text, decorator.getDialog(), onResult));
dialog(new InputDialogPane(text, Controllers::closeDialog, onResult));
}
public static void taskDialog(TaskExecutor executor, String title, String subtitle, Runnable onCancel) {
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setSubtitle(subtitle);
pane.setExecutor(executor);
executor.start();
dialog(pane);
return pane;
}
public static void closeDialog() {
decorator.getDialog().close();
public static void closeDialog(Region content) {
decorator.closeDialog(content);
}
public static void navigate(Node node) {
@@ -157,6 +169,18 @@ public final class Controllers {
}
public static void showUpdate() {
getDecorator().showUpdate();
if (stage == null) // shut down
return;
getLeftPaneController().showUpdate();
}
public static void shutdown() {
mainPage = null;
settingsPage = null;
versionPage = null;
serversPage = null;
decorator = null;
stage = null;
scene = null;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -27,29 +27,28 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
/**
* @author huangyuhui
*/
public class CrashWindow extends Stage {
private Label lblCrash = new Label();
private Button btnContact = new Button();
private TextArea textArea = new TextArea();
public CrashWindow(String text) {
if (Main.UPDATE_CHECKER.isOutOfDate())
lblCrash.setText(Main.i18n("launcher.crash_out_dated"));
Label lblCrash = new Label();
if (Launcher.UPDATE_CHECKER.isOutOfDate())
lblCrash.setText(Launcher.i18n("launcher.crash_out_dated"));
else
lblCrash.setText(Main.i18n("launcher.crash"));
lblCrash.setText(Launcher.i18n("launcher.crash"));
lblCrash.setWrapText(true);
TextArea textArea = new TextArea();
textArea.setText(text);
textArea.setEditable(false);
btnContact.setText(Main.i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Main.CONTACT));
Button btnContact = new Button();
btnContact.setText(Launcher.i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
@@ -66,11 +65,9 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(new Image("/assets/img/icon.png"));
setTitle(Main.i18n("message.error"));
setTitle(Launcher.i18n("message.error"));
setOnCloseRequest(e -> {
System.exit(1);
});
setOnCloseRequest(e -> System.exit(1));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.svg.SVGGlyph;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -46,13 +45,15 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.FileUtils;
@@ -60,8 +61,6 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -76,7 +75,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private static final SVGGlyph close = Lang.apply(new SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>(Main::stopApplication);
private final ObjectProperty<Runnable> onCloseButtonAction;
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
private final Stage primaryStage;
@@ -84,11 +83,13 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private final boolean max, min;
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private final JFXDialog dialog;
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging, dialogShown, maximized;
private boolean allowMove, isDragging, maximized;
private BoundingBox originalBox, maximizedBox;
private TransitionHandler animationHandler;
private final TransitionHandler animationHandler;
private final StackContainerPane dialogPane = new StackContainerPane();
@FXML
private StackPane contentPlaceHolder;
@@ -121,16 +122,12 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
@FXML
private JFXHamburger titleBurger;
@FXML
private JFXDialog dialog;
@FXML
private JFXButton btnMin;
@FXML
private JFXButton btnMax;
@FXML
private JFXButton btnClose;
@FXML
private HBox updatePane;
@FXML
private HBox navLeft;
public Decorator(Stage primaryStage, Node mainPage, String title) {
@@ -145,14 +142,22 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml");
updatePane.setCursor(Cursor.HAND);
updatePane.setOnMouseClicked(event -> Main.UPDATE_CHECKER.checkOutdate());
onCloseButtonAction = new SimpleObjectProperty<>(this, "onCloseButtonAction", Launcher::stopApplication);
primaryStage.initStyle(StageStyle.UNDECORATED);
btnClose.setGraphic(close);
btnMin.setGraphic(minus);
btnMax.setGraphic(resizeMax);
close.fillProperty().bind(Theme.foregroundFillBinding());
minus.fillProperty().bind(Theme.foregroundFillBinding());
resizeMax.fillProperty().bind(Theme.foregroundFillBinding());
resizeMin.fillProperty().bind(Theme.foregroundFillBinding());
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), 15, 15));
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), 15, 15));
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), 15, 15));
lblTitle.setText(title);
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
@@ -161,9 +166,11 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
btnMax.fire();
});
dialog = new JFXDialog();
dialog.setOverlayClose(false);
drawerWrapper.getChildren().add(0, dialog);
dialog.setDialogContainer(drawerWrapper);
dialog.setOnDialogClosed(e -> dialogShown = false);
dialog.setOnDialogOpened(e -> dialogShown = true);
dialog.setContent(dialogPane);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
@@ -184,14 +191,17 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
private void loadBackground() {
Image background;
try {
Image background;
if (Settings.INSTANCE.getBackgroundImageType() == EnumBackgroundImage.DEFAULT)
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), "");
else
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), Settings.INSTANCE.getBackgroundImage());
if (Settings.INSTANCE.getBackgroundImageType() == EnumBackgroundImage.DEFAULT)
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), "");
else
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), Settings.INSTANCE.getBackgroundImage());
drawerWrapper.setBackground(new Background(new BackgroundImage(background, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))));
drawerWrapper.setBackground(new Background(new BackgroundImage(background, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))));
} catch (IllegalArgumentException ignore) {
}
}
private static Image searchBackgroundImage(Image def, String customPath) {
@@ -208,7 +218,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
// bgskin
// images in ./bg
if (!loaded) {
File backgroundImageFile = new File("bg");
if (backgroundImageFile.isDirectory()) {
@@ -305,52 +315,52 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltax = this.newX - this.initX;
double deltay = this.newY - this.initY;
double deltaX = this.newX - this.initX;
double deltaY = this.newY - this.initY;
Cursor cursor = this.getCursor();
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltax);
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
this.primaryStage.setY(this.primaryStage.getY() + deltay);
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
this.setStageWidth(this.primaryStage.getWidth() + deltax);
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltax);
this.setStageHeight(this.primaryStage.getHeight() + deltay);
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.getHeight() + deltay);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
this.primaryStage.setX(this.primaryStage.getX() + deltax);
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
mouseEvent.consume();
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
this.primaryStage.setX(this.primaryStage.getX() + deltax);
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
this.setStageHeight(this.primaryStage.getHeight() + deltay);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltax)) {
this.primaryStage.setX(this.primaryStage.getX() + deltax);
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
this.primaryStage.setY(this.primaryStage.getY() + deltay);
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltay)) {
this.primaryStage.setY(this.primaryStage.getY() + deltay);
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
@@ -467,10 +477,6 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
public void showUpdate() {
updatePane.setVisible(true);
}
private void showCloseNavButton() {
navLeft.getChildren().add(closeNavButton);
}
@@ -480,6 +486,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
private void setContent(Node content, AnimationProducer animation) {
isWizardPageNow = false;
animationHandler.setContent(content, animation);
if (content instanceof Region) {
@@ -487,11 +494,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
FXUtils.setOverflowHidden((Region) content, true);
}
if (content instanceof Refreshable)
refreshNavButton.setVisible(true);
if (content != mainPage)
backNavButton.setVisible(true);
refreshNavButton.setVisible(content instanceof Refreshable);
backNavButton.setVisible(content != mainPage);
String prefix = category == null ? "" : category + " - ";
@@ -506,12 +510,15 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private String category;
private Node nowPage;
private boolean isWizardPageNow;
public Node getNowPage() {
return nowPage;
}
public void showPage(Node content) {
FXUtils.checkFxUserThread();
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
if (content != null)
contentPlaceHolder.getStyleClass().add("gray-background");
@@ -532,11 +539,23 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
public JFXDialog showDialog(Region content) {
dialog.setContent(content);
if (!dialogShown)
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialogPane.isEmpty())
dialog.show();
return dialog;
dialogPane.push(node);
}
public void closeDialog(Node node) {
FXUtils.checkFxUserThread();
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty())
Platform.runLater(() -> {
if (dialogPane.getChildren().isEmpty())
dialog.close();
});
}
public void startWizard(WizardProvider wizardProvider) {
@@ -544,6 +563,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
this.category = category;
wizardController.setProvider(wizardProvider);
wizardController.onStart();
@@ -569,6 +590,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
contentPlaceHolder.getStyleClass().add("white-background");
setContent(page, nav.getAnimation().getAnimationProducer());
isWizardPageNow = true;
}
@FXML
@@ -584,7 +606,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
@FXML
private void onBack() {
if (wizardController.canPrev())
if (isWizardPageNow && wizardController.canPrev())
wizardController.onPrev(true);
else
onCloseNav();
@@ -624,10 +646,6 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
return wizardController;
}
public JFXDialog getDialog() {
return dialog;
}
public AdvancedListBox getLeftPane() {
return leftPane;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.MultiCharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.task.SilentException;
@@ -35,19 +34,15 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
JFXUtilities.runInFX(() -> {
YggdrasilAccountLoginPane pane = new YggdrasilAccountLoginPane((YggdrasilAccount) account, it -> {
AccountLoginPane pane = new AccountLoginPane(account, Controllers::closeDialog, it -> {
res.set(it);
latch.countDown();
Controllers.closeDialog();
}, () -> {
latch.countDown();
Controllers.closeDialog();
});
pane.setDialog(Controllers.dialog(pane));
}, latch::countDown);
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
}
return account.logIn(MultiCharacterSelector.DEFAULT);
return account.logIn();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,12 +17,12 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -30,39 +30,44 @@ import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.ReflectionHelper.call;
import static org.jackhuang.hmcl.util.ReflectionHelper.construct;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class FXUtils {
private FXUtils() {
}
public static void checkFxUserThread() {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX application thread; currentThread = "
+ Thread.currentThread().getName());
}
}
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener((a, b, c) -> consumer.accept(c));
}
@@ -94,19 +99,36 @@ public final class FXUtils {
});
}
public static <T> void addListener(Node node, String key, ObservableValue<T> value, Consumer<T> callback) {
ChangeListener<T> listener = (a, b, newValue) -> callback.accept(newValue);
node.getProperties().put(key, new Pair<>(callback, listener));
value.addListener(listener);
private static class ListenerPair<T> {
private final ObservableValue<T> value;
private final ChangeListener<? super T> listener;
ListenerPair(ObservableValue<T> value, ChangeListener<? super T> listener) {
this.value = value;
this.listener = listener;
}
void bind() {
value.addListener(listener);
}
void unbind() {
value.removeListener(listener);
}
}
public static <T> void removeListener(Node node, String key) {
if (node.getProperties().get(key) instanceof Pair) {
Pair pair = (Pair) node.getProperties().get(key);
if (pair.getValue() instanceof ObservableValue && pair.getKey() instanceof ChangeListener) {
((ObservableValue) pair.getValue()).removeListener((ChangeListener) pair.getKey());
}
}
public static <T> void addListener(Node node, String key, ObservableValue<T> value, Consumer<? super T> callback) {
ListenerPair<T> pair = new ListenerPair<>(value, (a, b, newValue) -> callback.accept(newValue));
node.getProperties().put(key, pair);
pair.bind();
}
public static void removeListener(Node node, String key) {
tryCast(node.getProperties().get(key), ListenerPair.class)
.ifPresent(info -> {
info.unbind();
node.getProperties().remove(key);
});
}
public static void setValidateWhileTextChanged(Node field, boolean validate) {
@@ -172,68 +194,46 @@ public final class FXUtils {
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Main.RESOURCE_BUNDLE);
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Launcher.RESOURCE_BUNDLE);
loader.setRoot(node);
loader.setController(node);
Lang.invoke(() -> loader.load());
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
}
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
pane.getChildren().setAll(children);
public static void installTooltip(Node node, String tooltip) {
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
try {
// Java 8
call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false),
"install", node, tooltip);
} catch (Throwable e) {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
try {
// Java 9
call(tooltip, "setShowDelay", new Duration(openDelay));
call(tooltip, "setShowDuration", new Duration(visibleDelay));
call(tooltip, "setHideDelay", new Duration(closeDelay));
} catch (Throwable e2) {
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
}
public static boolean alert(Alert.AlertType type, String title, String contentText) {
return alert(type, title, contentText, null);
}
public static boolean alert(Alert.AlertType type, String title, String contentText, String headerText) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(headerText);
alert.setContentText(contentText);
Optional<ButtonType> result = alert.showAndWait();
return result.isPresent() && result.get() == ButtonType.OK;
}
public static Optional<String> inputDialog(String title, String contentText) {
return inputDialog(title, contentText, null);
}
public static Optional<String> inputDialog(String title, String contentText, String headerText) {
return inputDialog(title, contentText, headerText, "");
}
public static Optional<String> inputDialog(String title, String contentText, String headerText, String defaultValue) {
TextInputDialog dialog = new TextInputDialog(defaultValue);
dialog.setTitle(title);
dialog.setHeaderText(headerText);
dialog.setContentText(contentText);
return dialog.showAndWait();
}
public static void openFolder(File file) {
file.mkdirs();
if (!FileUtils.makeDirectory(file)) {
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
return;
}
String path = file.getAbsolutePath();
switch (OperatingSystem.CURRENT_OS) {
@@ -306,18 +306,32 @@ public final class FXUtils {
checkBox.selectedProperty().unbindBidirectional(property);
}
/**
* Bind combo box selection with given enum property bidirectionally.
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
*/
@SuppressWarnings("unchecked")
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
unbindEnum(comboBox);
ChangeListener<Number> listener = (a, b, newValue) -> {
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
};
ChangeListener<Number> listener = (a, b, newValue) ->
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
comboBox.getSelectionModel().select(property.getValue().ordinal());
comboBox.getProperties().put("listener", listener);
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
}
/**
* Unbind combo box selection with given enum property bidirectionally.
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
* @see #bindEnum(JFXComboBox, Property)
*/
@SuppressWarnings("unchecked")
public static void unbindEnum(JFXComboBox<?> comboBox) {
ChangeListener listener = Lang.get(comboBox.getProperties(), "listener", ChangeListener.class, null);
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
@@ -363,6 +377,31 @@ public final class FXUtils {
});
}
public static <T> T runInUIThread(Supplier<T> supplier) {
if (javafx.application.Platform.isFxApplicationThread()) {
return supplier.get();
} else {
CountDownLatch doneLatch = new CountDownLatch(1);
AtomicReference<T> reference = new AtomicReference<>();
Platform.runLater(() -> {
try {
reference.set(supplier.get());
} finally {
doneLatch.countDown();
}
});
try {
doneLatch.await();
} catch (InterruptedException var3) {
Thread.currentThread().interrupt();
}
return reference.get();
}
}
public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png");
public static final Interpolator SINE = new Interpolator() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.game.GameVersion;
@@ -53,7 +54,7 @@ public class InstallerController {
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
this.version = profile.getRepository().getVersion(versionId).resolve(profile.getRepository());
this.version = profile.getRepository().getResolvedVersion(versionId);
contentPane.getChildren().clear();
forge = liteLoader = optiFine = null;
@@ -62,7 +63,7 @@ public class InstallerController {
Consumer<InstallerItem> removeAction = x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(profile.getRepository(), version.setLibraries(newList))
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
@@ -89,7 +90,7 @@ public class InstallerController {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
Controllers.dialog("version.cannot_read");
Controllers.dialog(Launcher.i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,7 +21,7 @@ import com.jfoenix.effects.JFXDepthManager;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import java.util.function.Consumer;
@@ -44,7 +44,7 @@ public class InstallerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblInstallerArtifact.setText(artifact);
lblInstallerVersion.setText(Main.i18n("archive.version") + ": " + version);
lblInstallerVersion.setText(Launcher.i18n("archive.version") + ": " + version);
}
@FXML
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -19,90 +19,98 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Paint;
import javafx.scene.text.Text;
import org.jackhuang.hmcl.Main;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
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.Settings;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
private final VBox profilePane = new VBox();
private final VersionListItem accountItem = new VersionListItem("", "");
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(Launcher.i18n("account.missing"), Launcher.i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
leftPane.startCategory(Main.i18n("account").toUpperCase())
.add(Lang.apply(new RipplerContainer(accountItem), rippler -> {
rippler.setOnMouseClicked(e -> Controllers.navigate(new AccountsPage()));
accountItem.setOnSettingsButtonClicked(() -> Controllers.navigate(new AccountsPage()));
}))
.startCategory(Main.i18n("launcher").toUpperCase())
.add(Lang.apply(new IconedItem(SVG.gear("black", 20, 20), Main.i18n("settings.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
}))
.add(new ClassTitle(Lang.apply(new BorderPane(), borderPane -> {
borderPane.setLeft(Lang.apply(new VBox(), vBox -> {
vBox.getChildren().setAll(new Text(Main.i18n("profile.title").toUpperCase()));
}));
JFXButton addProfileButton = new JFXButton();
addProfileButton.setGraphic(SVG.plus("black", 10, 10));
addProfileButton.getStyleClass().add("toggle-icon-tiny");
addProfileButton.setOnMouseClicked(e ->
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), Launcher.i18n("settings.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
});
leftPane
.add(new ClassTitle(Launcher.i18n("account").toUpperCase(), Lang.apply(new JFXButton(), button -> {
button.setGraphic(SVG.plus(Theme.blackFillBinding(), 10, 10));
button.getStyleClass().add("toggle-icon-tiny");
button.setOnMouseClicked(e -> addNewAccount());
})))
.add(accountPane)
.startCategory(Launcher.i18n("launcher").toUpperCase())
.add(launcherSettingsItem)
.add(new ClassTitle(Launcher.i18n("profile.title").toUpperCase(), Lang.apply(new JFXButton(), button -> {
button.setGraphic(SVG.plus(Theme.blackFillBinding(), 10, 10));
button.getStyleClass().add("toggle-icon-tiny");
button.setOnMouseClicked(e ->
Controllers.getDecorator().showPage(new ProfilePage(null)));
borderPane.setRight(addProfileButton);
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), it -> {
if (it == null) {
accountItem.setVersionName(Main.i18n("account.missing"));
accountItem.setGameVersion(Main.i18n("message.unknown"));
} else {
accountItem.setVersionName(Accounts.getCurrentCharacter(it));
accountItem.setGameVersion(AccountsPage.accountType(it));
}
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
onAccountsLoading();
}
if (it instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) it, 4);
accountItem.setImage(image, AccountHelper.getViewport(4));
} else
accountItem.setImage(AccountHelper.getDefaultSkin(it, 4), AccountHelper.getViewport(4));
private void addNewAccount() {
Controllers.dialog(new AddAccountPane(Controllers::closeDialog));
}
private void onSelectedAccountChanged(Account newAccount) {
Platform.runLater(() -> {
for (Node node : accountPane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) {
boolean current = Objects.equals(node.getProperties().get("account"), newAccount);
((RipplerContainer) node).setSelected(current);
}
}
});
}
@@ -111,10 +119,10 @@ public final class LeftPaneController {
Platform.runLater(() -> {
for (Node node : profilePane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof Pair<?, ?>) {
boolean current = Objects.equals(((Pair) node.getProperties().get("profile")).getKey(), profile.getName());
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof String) {
boolean current = Objects.equals(node.getProperties().get("profile"), profile.getName());
((RipplerContainer) node).setSelected(current);
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? Main.i18n("profile.selected") : "");
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? Launcher.i18n("profile.selected") : "");
}
}
});
@@ -125,20 +133,89 @@ public final class LeftPaneController {
for (Profile profile : Settings.INSTANCE.getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(() -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setOnMouseClicked(e -> Settings.INSTANCE.setSelectedProfile(profile));
ripplerContainer.getProperties().put("profile", new Pair<>(profile.getName(), item));
ripplerContainer.getProperties().put("profile", profile.getName());
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
list.add(ripplerContainer);
}
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return Launcher.i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
Account account = event.getAccount();
VersionListItem item = items.get(account);
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
items.clear();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
items.put(account, item);
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
ripplerContainer.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
Settings.INSTANCE.setSelectedAccount(account);
});
ripplerContainer.getProperties().put("account", account);
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount)
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
.subscribe(Schedulers.javafx(), variables -> FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(variables.get("serverName"))));
if (selectedAccount == account)
ripplerContainer.setSelected(true);
list.add(ripplerContainer);
}
if (Settings.INSTANCE.getAccounts().isEmpty()) {
RipplerContainer container = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
container.setOnMouseClicked(e -> addNewAccount());
list.add(container);
}
Platform.runLater(() -> accountPane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(Launcher.i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
}
private boolean checkedModpack = false;
private static boolean showNewAccount = true;
private void onRefreshedVersions(RefreshedVersionsEvent event) {
JFXUtilities.runInFX(() -> {
boolean flag = true;
HMCLGameRepository repository = (HMCLGameRepository) event.getSource();
if (!checkedModpack) {
checkedModpack = true;
@@ -147,27 +224,31 @@ public final class LeftPaneController {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
try {
AtomicReference<Region> region = new AtomicReference<>();
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
Controllers.taskDialog(ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
Controllers.closeDialog();
checkAccount();
})).executor(),
Main.i18n("modpack.installing"), "", null);
flag = false;
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
Controllers.closeDialog(region.get());
checkAccount();
})).executor();
region.set(Controllers.taskDialog(executor, Launcher.i18n("modpack.installing"), ""));
executor.start();
showNewAccount = false;
} catch (UnsupportedModpackException ignore) {
}
}
}
}
if (flag)
if (showNewAccount) {
showNewAccount = false;
checkAccount();
}
});
}
private void checkAccount() {
public void checkAccount() {
if (Settings.INSTANCE.getAccounts().isEmpty())
Controllers.navigate(new AccountsPage());
addNewAccount();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2013 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -30,7 +30,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
@@ -63,16 +63,14 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
setTitle(Main.i18n("logwindow.title"));
setTitle(Launcher.i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
public LogWindow(String text) {
this();
onDone.register(() -> {
logLine(text, Log4jLevel.INFO);
});
onDone.register(() -> logLine(text, Log4jLevel.INFO));
}
public ReadOnlyIntegerProperty fatalProperty() {
@@ -140,6 +138,9 @@ public final class LogWindow extends Stage {
case DEBUG:
debug.set(debug.get() + 1);
break;
default:
// ignore
break;
}
}
@@ -164,7 +165,7 @@ public final class LogWindow extends Stage {
@FXML
private ComboBox<String> cboLines;
WebEngine engine;
final WebEngine engine;
Node body;
Document document;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,27 +17,23 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXMasonryPane;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -47,181 +43,203 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
public final class MainPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("main_page"));
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("main_page"));
private Profile profile;
private String rightClickedVersion;
@FXML
private JFXButton btnRefresh;
@FXML
private StackPane contentPane;
@FXML
private JFXButton btnAdd;
@FXML
private JFXSpinner spinner;
@FXML
private JFXMasonryPane masonryPane;
@FXML
private JFXListView versionList;
private JFXPopup versionPopup;
private ScrollPane scrollPane;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(() -> Platform.runLater(this::loadVersions));
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
loadingVersions();
versionPopup = new JFXPopup(versionList);
getChildren().remove(versionList);
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())
// This will occupy 0.5s. Too slow!
JFXUtilities.runInFX(this::loadingVersions);
});
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(event -> {
this.profile = event.getProfile();
});
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), Main.i18n("install")));
FXUtils.installTooltip(btnAdd, 0, 5000, 0, new Tooltip(Main.i18n("install")));
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), Launcher.i18n("install")));
FXUtils.installTooltip(btnAdd, Launcher.i18n("install"));
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersionsAsync().start());
FXUtils.installTooltip(btnRefresh, 0, 5000, 0, new Tooltip(Main.i18n("button.refresh")));
FXUtils.installTooltip(btnRefresh, Launcher.i18n("button.refresh"));
}
private Node buildNode(Profile profile, String version, String game) {
private String modifyVersion(String gameVersion, String version) {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
}
private Node buildNode(HMCLGameRepository repository, Version version, Callable<String> gameCallable) {
Profile profile = repository.getProfile();
String id = version.getId();
VersionItem item = new VersionItem();
item.setUpdate(profile.getRepository().isModpack(version));
item.setGameVersion(game);
item.setVersionName(version);
item.setUpdate(repository.isModpack(id));
Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> {
String game = vars.get("game");
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : profile.getRepository().getVersion(version).getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Main.i18n("install.installer.forge")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)forge", "").replace(game, "").trim(), "-"), "-")).append("\n");
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Launcher.i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(Launcher.i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(Launcher.i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(Main.i18n("install.installer.liteloader")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)liteloader", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(Main.i18n("install.installer.optifine")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)optifine", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
}
item.setLibraries(libraries.toString());
item.setLibraries(libraries.toString());
});
item.setVersionName(id);
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.empty_username"));
Controllers.getLeftPaneController().checkAccount();
else
LauncherHelper.INSTANCE.launch(version, null);
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.empty_username"));
Controllers.dialog(Launcher.i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
chooser.setInitialDirectory(profile.getRepository().getRunDirectory(version));
chooser.setTitle(Main.i18n("version.launch_script.save"));
if (repository.getRunDirectory(id).isDirectory())
chooser.setInitialDirectory(repository.getRunDirectory(id));
chooser.setTitle(Launcher.i18n("version.launch_script.save"));
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(Main.i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(Main.i18n("extension.sh"), "*.sh"));
? new FileChooser.ExtensionFilter(Launcher.i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(Launcher.i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(version, file);
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
}
});
item.setOnSettingsButtonClicked(e -> {
Controllers.getDecorator().showPage(Controllers.getVersionPage());
Controllers.getVersionPage().load(version, profile);
Controllers.getVersionPage().load(id, profile);
});
item.setOnUpdateButtonClicked(event -> {
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("modpack"), "*.zip"));
chooser.setTitle(Launcher.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(null);
AtomicReference<Region> region = new AtomicReference<>();
try {
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, version, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(version)))
.then(Task.of(Schedulers.javafx(), Controllers::closeDialog)).executor();
pane.setExecutor(executor);
pane.setTitle(Main.i18n("modpack.update"));
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id)))
.then(Task.of(Schedulers.javafx(), () -> Controllers.closeDialog(region.get()))).executor();
region.set(Controllers.taskDialog(executor, Launcher.i18n("modpack.update"), ""));
executor.start();
Controllers.dialog(pane);
} catch (UnsupportedModpackException e) {
Controllers.dialog(Main.i18n("modpack.unsupported"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.unsupported"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(Main.i18n("modpack.mismatched_type"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
Controllers.dialog(Main.i18n("modpack.invalid"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.mismatched_type"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.invalid"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
}
});
item.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.SECONDARY) {
rightClickedVersion = version;
versionList.getSelectionModel().select(-1);
JFXListView<String> versionList = new JFXListView<>();
JFXPopup versionPopup = new JFXPopup(versionList);
versionList.getStyleClass().add("option-list-view");
FXUtils.setLimitWidth(versionList, 150);
versionList.getItems().setAll(Lang.immutableListOf(
Launcher.i18n("version.manage.rename"),
Launcher.i18n("version.manage.remove"),
Launcher.i18n("modpack.export"),
Launcher.i18n("folder.game")
));
versionList.setOnMouseClicked(e -> {
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
VersionPage.renameVersion(profile, id);
break;
case 1:
VersionPage.deleteVersion(profile, id);
break;
case 2:
VersionPage.exportVersion(profile, id);
break;
case 3:
FXUtils.openFolder(repository.getRunDirectory(id));
break;
default:
break;
}
});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.empty_username"));
Controllers.dialog(Launcher.i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(version, null);
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
}
});
File iconFile = profile.getRepository().getVersionIcon(version);
File iconFile = repository.getVersionIcon(id);
if (iconFile.exists())
item.setImage(new Image("file:" + iconFile.getAbsolutePath()));
return item;
}
public void onProfilesLoading() {
// TODO: Profiles
private void loadingVersions() {
getChildren().setAll(spinner);
masonryPane.getChildren().clear();
}
public void onProfileChanged(ProfileChangedEvent event) {
Platform.runLater(() -> loadVersions(event.getProfile()));
}
private void loadVersions() {
loadVersions(Settings.INSTANCE.getSelectedProfile());
}
private void loadVersions(Profile profile) {
this.profile = profile;
List<Node> children = new LinkedList<>();
List<Version> versions = new LinkedList<>(profile.getRepository().getVersions());
for (Version version : versions) {
children.add(buildNode(profile, version.getId(), GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version.getId())).orElse("Unknown")));
}
FXUtils.resetChildren(masonryPane, children);
}
@FXML
private void onVersionManagement() {
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
VersionPage.renameVersion(profile, rightClickedVersion);
break;
case 1:
VersionPage.deleteVersion(profile, rightClickedVersion);
break;
case 2:
VersionPage.exportVersion(profile, rightClickedVersion);
break;
case 3:
FXUtils.openFolder(profile.getRepository().getRunDirectory(rightClickedVersion));
break;
}
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile()) {
masonryPane.getChildren().setAll(children);
getChildren().setAll(contentPane);
}
});
}
public String getTitle() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,16 +17,16 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.controls.JFXTabPane;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.Schedulers;
@@ -37,9 +37,11 @@ import org.jackhuang.hmcl.util.Logging;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class ModController {
@@ -68,21 +70,22 @@ public final class ModController {
});
rootPane.setOnDragDropped(event -> {
List<File> mods = event.getDragboard().getFiles();
Stream<File> stream = null;
if (mods != null)
stream = mods.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)));
if (stream != null && stream.findAny().isPresent()) {
stream.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
List<File> files = event.getDragboard().getFiles();
if (files != null) {
Collection<File> mods = files.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)))
.collect(Collectors.toList());
if (!mods.isEmpty()) {
mods.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
}
}
event.consume();
});
@@ -93,7 +96,7 @@ public final class ModController {
this.versionId = versionId;
Task.of(variables -> {
synchronized (ModController.this) {
Platform.runLater(() -> {
JFXUtilities.runInFX(() -> {
rootPane.getChildren().remove(contentPane);
spinner.setVisible(true);
});
@@ -121,30 +124,51 @@ public final class ModController {
list.add(item);
}
Platform.runLater(() -> {
rootPane.getChildren().add(contentPane);
spinner.setVisible(false);
});
variables.set("list", list);
}
}).subscribe(Schedulers.javafx(), variables -> {
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModController.this)
modPane.getChildren().setAll(variables.<List<ModItem>>get("list"));
});
});
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
rootPane.getChildren().add(contentPane);
spinner.setVisible(false);
if (isDependentsSucceeded)
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModController.this)
modPane.getChildren().setAll(variables.<List<ModItem>>get("list"));
});
}).start();
}
@FXML
private void onAdd() {
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(Main.i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
File res = chooser.showOpenDialog(Controllers.getStage());
chooser.setTitle(Launcher.i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(Launcher.i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
// It's guaranteed that succeeded and failed are thread safe here.
List<String> succeeded = new LinkedList<>();
List<String> failed = new LinkedList<>();
if (res == null) return;
Task.of(() -> modManager.addMod(versionId, res))
.subscribe(Task.of(Schedulers.javafx(), () -> loadMods(modManager, versionId)));
Task.of(variables -> {
for (File file : res) {
try {
modManager.addMod(versionId, file);
succeeded.add(file.getName());
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to add mod " + file, e);
failed.add(file.getName());
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
}
}
}).with(Task.of(Schedulers.javafx(), variables -> {
List<String> prompt = new LinkedList<>();
if (!succeeded.isEmpty())
prompt.add(Launcher.i18n("mods.add.success", String.join(", ", succeeded)));
if (!failed.isEmpty())
prompt.add(Launcher.i18n("mods.add.failed", String.join(", ", failed)));
Controllers.dialog(String.join("\n", prompt), Launcher.i18n("mods.add"));
loadMods(modManager, versionId);
})).start();
}
public void setParentTab(JFXTabPane parentTab) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,44 +21,38 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import java.util.function.Consumer;
public final class ModItem extends BorderPane {
private final Label lblModFileName = new Label();
private final Label lblModAuthor = new Label();
private final JFXCheckBox chkEnabled = new JFXCheckBox();
public ModItem(ModInfo info, Consumer<ModItem> deleteCallback) {
lblModFileName.setStyle("-fx-font-size: 15;");
lblModAuthor.setStyle("-fx-font-size: 10;");
JFXCheckBox chkEnabled = new JFXCheckBox();
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
setLeft(chkEnabled);
VBox center = new VBox();
BorderPane.setAlignment(center, Pos.CENTER);
center.getChildren().addAll(lblModFileName, lblModAuthor);
setCenter(center);
TwoLineListItem modItem = new TwoLineListItem();
BorderPane.setAlignment(modItem, Pos.CENTER);
setCenter(modItem);
JFXButton right = new JFXButton();
right.setOnMouseClicked(e -> deleteCallback.accept(this));
right.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(right, Pos.CENTER);
right.setGraphic(SVG.close("black", 15, 15));
setRight(right);
JFXButton btnRemove = new JFXButton();
FXUtils.installTooltip(btnRemove, Launcher.i18n("mods.remove"));
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(btnRemove);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblModFileName.setText(info.getFileName());
lblModAuthor.setText(info.getName() + ", " + Main.i18n("archive.version") + ": " + info.getVersion() + ", " + Main.i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + Main.i18n("archive.author") + ": " + info.getAuthors());
modItem.setTitle(info.getFileName());
modItem.setSubtitle(info.getName() + ", " + Launcher.i18n("archive.version") + ": " + info.getVersion() + ", " + Launcher.i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + Launcher.i18n("archive.author") + ": " + info.getAuthors());
chkEnabled.setSelected(info.isActive());
chkEnabled.selectedProperty().addListener((a, b, newValue) -> {
info.activeProperty().set(newValue);
});
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
info.activeProperty().set(newValue));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -23,7 +23,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Settings;
@@ -51,15 +51,16 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
*/
public ProfilePage(Profile profile) {
this.profile = profile;
String profileDisplayName = Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse("");
title = new SimpleStringProperty(this, "title",
profile == null ? Main.i18n("profile.new") : Main.i18n("profile") + " - " + profile.getName());
profile == null ? Launcher.i18n("profile.new") : Launcher.i18n("profile") + " - " + profileDisplayName);
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(""));
FXUtils.loadFXML(this, "/assets/fxml/profile.fxml");
txtProfileName.setText(Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse(""));
txtProfileName.setText(profileDisplayName);
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,19 +17,21 @@
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
public final class SVG {
private SVG() {
}
private static Node createSVGPath(String d, String fill, double width, double height) {
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
path.setStyle("-fx-fill: " + fill + ";");
path.fillProperty().bind(fill);
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
@@ -41,87 +43,87 @@ public final class SVG {
// default fill: white, width: 20, height 20
public static Node gear(String fill, double width, double height) {
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
}
public static Node back(String fill, double width, double height) {
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
}
public static Node close(String fill, double width, double height) {
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
}
public static Node dotsVertical(String fill, double width, double height) {
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
}
public static Node delete(String fill, double width, double height) {
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
public static Node accountEdit(String fill, double width, double height) {
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
}
public static Node expand(String fill, double width, double height) {
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
}
public static Node collapse(String fill, double width, double height) {
public static Node collapse(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
}
public static Node navigate(String fill, double width, double height) {
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node launch(String fill, double width, double height) {
public static Node launch(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
}
public static Node script(String fill, double width, double height) {
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z", fill, width, height);
}
public static Node pencil(String fill, double width, double height) {
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
}
public static Node refresh(String fill, double width, double height) {
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
}
public static Node folderOpen(String fill, double width, double height) {
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
}
public static Node update(String fill, double width, double height) {
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
}
public static Node closeCircle(String fill, double width, double height) {
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z", fill, width, height);
}
public static Node checkCircle(String fill, double width, double height) {
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z", fill, width, height);
}
public static Node infoCircle(String fill, double width, double height) {
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", fill, width, height);
}
public static Node helpCircle(String fill, double width, double height) {
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z", fill, width, height);
}
public static Node alert(String fill, double width, double height) {
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
}
public static Node plus(String fill, double width, double height) {
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,18 +18,23 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.FontComboBox;
@@ -41,9 +46,10 @@ import org.jackhuang.hmcl.util.Lang;
import java.net.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("settings.launcher"));
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("settings.launcher"));
@FXML
private JFXTextField txtProxyHost;
@@ -76,7 +82,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private MultiFileItem backgroundItem;
@FXML
private MultiFileItem themeItem;
private StackPane themeColorPickerContainer;
@FXML
private JFXRadioButton chkNoProxy;
@FXML
@@ -159,13 +165,11 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
chkManualProxy.setSelected(true);
proxyPane.disableProperty().bind(chkNoProxy.selectedProperty());
hasProxyGroup.selectedToggleProperty().addListener((a, b, newValue) -> {
Settings.INSTANCE.setHasProxy(newValue != chkNoProxy);
});
hasProxyGroup.selectedToggleProperty().addListener((a, b, newValue) ->
Settings.INSTANCE.setHasProxy(newValue != chkNoProxy));
proxyConfigurationGroup.selectedToggleProperty().addListener((a, b, newValue) -> {
Settings.INSTANCE.setProxyType((Proxy.Type) newValue.getUserData());
});
proxyConfigurationGroup.selectedToggleProperty().addListener((a, b, newValue) ->
Settings.INSTANCE.setProxyType((Proxy.Type) newValue.getUserData()));
chkProxyAuthentication.setSelected(Settings.INSTANCE.hasProxyAuth());
chkProxyAuthentication.selectedProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setHasProxyAuth(newValue));
@@ -173,12 +177,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty());
FXUtils.installTooltip(btnUpdate, 0, 5000, 0, new Tooltip(Main.i18n("update.tooltip")));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("update.tooltip"));
checkUpdate();
// background
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(Main.i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
backgroundItem.createChildren(Launcher.i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
));
FXUtils.bindString(backgroundItem.getTxtCustom(), Settings.INSTANCE.backgroundImageProperty());
@@ -190,30 +194,27 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
Settings.INSTANCE.backgroundImageTypeProperty().setChangedListener(it -> initBackgroundItemSubtitle());
initBackgroundItemSubtitle();
backgroundItem.setToggleSelectedListener(newValue -> {
Settings.INSTANCE.setBackgroundImageType((EnumBackgroundImage) newValue.getUserData());
});
backgroundItem.setToggleSelectedListener(newValue ->
Settings.INSTANCE.setBackgroundImageType((EnumBackgroundImage) newValue.getUserData()));
// theme
themeItem.loadChildren(Arrays.asList(
themeItem.createChildren(Main.i18n("color.blue"), Theme.BLUE),
themeItem.createChildren(Main.i18n("color.dark_blue"), Theme.DARK_BLUE),
themeItem.createChildren(Main.i18n("color.green"), Theme.GREEN),
themeItem.createChildren(Main.i18n("color.orange"), Theme.ORANGE),
themeItem.createChildren(Main.i18n("color.purple"), Theme.PURPLE),
themeItem.createChildren(Main.i18n("color.red"), Theme.RED)
));
themeItem.getGroup().getToggles().stream().filter(it -> Settings.INSTANCE.getTheme() == it.getUserData()).findFirst().ifPresent(it -> it.setSelected(true));
themeItem.setToggleSelectedListener(newValue -> Settings.INSTANCE.setTheme((Theme) newValue.getUserData()));
Settings.INSTANCE.themeProperty().setChangedListenerAndOperate(it ->
themeItem.setSubtitle(Main.i18n("color." + it.name().toLowerCase())));
JFXColorPicker picker = new JFXColorPicker(Color.web(Settings.INSTANCE.getTheme().getColor()), null);
picker.setCustomColorText(Launcher.i18n("color.custom"));
picker.setRecentColorsText(Launcher.i18n("color.recent"));
picker.getCustomColors().setAll(Arrays.stream(Theme.VALUES).map(Theme::getColor).map(Color::web).collect(Collectors.toList()));
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
Settings.INSTANCE.setTheme(theme);
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
});
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
}
private void initBackgroundItemSubtitle() {
switch (Settings.INSTANCE.getBackgroundImageType()) {
case DEFAULT:
backgroundItem.setSubtitle(Main.i18n("launcher.background.default"));
backgroundItem.setSubtitle(Launcher.i18n("launcher.background.default"));
break;
case CUSTOM:
backgroundItem.setSubtitle(Settings.INSTANCE.getBackgroundImage());
@@ -235,25 +236,25 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}
public void checkUpdate() {
btnUpdate.setVisible(Main.UPDATE_CHECKER.isOutOfDate());
btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate());
if (Main.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(Main.i18n("update.newest_version", Main.UPDATE_CHECKER.getNewVersion().toString()));
if (Launcher.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(Launcher.i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(Main.i18n("update.found"));
lblUpdate.setText(Launcher.i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else {
lblUpdateSub.setText(Main.i18n("update.latest"));
lblUpdateSub.setText(Launcher.i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(Main.i18n("update"));
lblUpdate.setText(Launcher.i18n("update"));
lblUpdate.getStyleClass().setAll();
}
}
@FXML
private void onUpdate() {
Main.UPDATE_CHECKER.checkOutdate();
Launcher.UPDATE_CHECKER.checkOutdate();
}
}
@@ -0,0 +1,200 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.css.*;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
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 {
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final StyleableObjectProperty<Font> titleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FONT, this, "title-font", Font.font(15));
private final StyleableObjectProperty<Font> subtitleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FONT, this, "subtitle-font", Font.getDefault());
private final StyleableObjectProperty<Paint> titleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FILL, this, "title-fill", Color.BLACK);
private final StyleableObjectProperty<Paint> subtitleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FILL, this, "subtitle-fill", Color.GRAY);
public TwoLineListItem(String titleString, String subtitleString) {
this();
title.set(titleString);
subtitle.set(subtitleString);
}
public TwoLineListItem() {
Label lblTitle = new Label();
lblTitle.textFillProperty().bind(titleFill);
lblTitle.fontProperty().bind(titleFont);
lblTitle.textProperty().bind(title);
Label lblSubtitle = new Label();
lblSubtitle.textFillProperty().bind(subtitleFill);
lblSubtitle.fontProperty().bind(subtitleFont);
lblSubtitle.textProperty().bind(subtitle);
VBox vbox = new VBox();
vbox.getChildren().setAll(lblTitle, lblSubtitle);
getChildren().setAll(vbox);
}
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 Font getTitleFont() {
return titleFont.get();
}
public StyleableObjectProperty<Font> titleFontProperty() {
return titleFont;
}
public void setTitleFont(Font titleFont) {
this.titleFont.set(titleFont);
}
public Font getSubtitleFont() {
return subtitleFont.get();
}
public StyleableObjectProperty<Font> subtitleFontProperty() {
return subtitleFont;
}
public void setSubtitleFont(Font subtitleFont) {
this.subtitleFont.set(subtitleFont);
}
public Paint getTitleFill() {
return titleFill.get();
}
public StyleableObjectProperty<Paint> titleFillProperty() {
return titleFill;
}
public void setTitleFill(Paint titleFill) {
this.titleFill.set(titleFill);
}
public Paint getSubtitleFill() {
return subtitleFill.get();
}
public StyleableObjectProperty<Paint> subtitleFillProperty() {
return subtitleFill;
}
public void setSubtitleFill(Paint subtitleFill) {
this.subtitleFill.set(subtitleFill);
}
@Override
public String toString() {
return getTitle();
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
private static class StyleableProperties {
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);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,19 +21,20 @@ import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Main;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import java.util.Optional;
@@ -65,18 +66,117 @@ public final class VersionItem extends StackPane {
private EventHandler<? super MouseEvent> launchClickedHandler = null;
public VersionItem() {
FXUtils.loadFXML(this, "/assets/fxml/version-item.fxml");
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear("black", 15, 15));
btnUpdate.setGraphic(SVG.update("black", 15, 15));
btnLaunch.setGraphic(SVG.launch("black", 15, 15));
btnScript.setGraphic(SVG.script("black", 15, 15));
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 156);
FXUtils.installTooltip(btnSettings, 0, 5000, 0, new Tooltip(Main.i18n("version.settings")));
FXUtils.installTooltip(btnUpdate, 0, 5000, 0, new Tooltip(Main.i18n("version.update")));
FXUtils.installTooltip(btnLaunch, 0, 5000, 0, new Tooltip(Main.i18n("version.launch")));
FXUtils.installTooltip(btnScript, 0, 5000, 0, new Tooltip(Main.i18n("version.launch_script")));
content = new VBox();
{
header = new StackPane();
VBox.setVgrow(header, Priority.ALWAYS);
header.setPickOnBounds(false);
header.setPadding(new Insets(8));
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87);");
{
VBox headerContent = new VBox();
headerContent.setPadding(new Insets(8, 8, 0, 8));
{
lblVersionName = new Label();
lblVersionName.setStyle("-fx-font-size: 15;");
lblVersionName.setTextAlignment(TextAlignment.JUSTIFY);
lblVersionName.setWrapText(true);
headerContent.getChildren().add(lblVersionName);
lblGameVersion = new Label();
lblGameVersion.setStyle("-fx-font-size: 11;");
lblGameVersion.setTextAlignment(TextAlignment.JUSTIFY);
lblGameVersion.setWrapText(true);
headerContent.getChildren().add(lblGameVersion);
lblLibraries = new Label();
lblLibraries.setStyle("-fx-font-size: 10; -fx-text-fill: gray;");
lblLibraries.setTextAlignment(TextAlignment.JUSTIFY);
lblLibraries.setWrapText(true);
headerContent.getChildren().add(lblLibraries);
}
header.getChildren().add(headerContent);
}
content.getChildren().add(header);
body = new StackPane();
body.setStyle("-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;");
body.setMinHeight(40);
body.setPickOnBounds(false);
{
BorderPane bodyContent = new BorderPane();
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnSettings = new JFXButton();
btnSettings.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnSettings, 30);
FXUtils.setLimitHeight(btnSettings, 30);
hbox.getChildren().add(btnSettings);
btnUpdate = new JFXButton();
btnUpdate.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnUpdate, 30);
FXUtils.setLimitHeight(btnUpdate, 30);
hbox.getChildren().add(btnUpdate);
}
bodyContent.setLeft(hbox);
}
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnScript = new JFXButton();
btnScript.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnScript, 30);
FXUtils.setLimitHeight(btnScript, 30);
hbox.getChildren().add(btnScript);
btnLaunch = new JFXButton();
btnLaunch.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnLaunch, 30);
FXUtils.setLimitHeight(btnLaunch, 30);
hbox.getChildren().add(btnLaunch);
}
bodyContent.setRight(hbox);
}
body.getChildren().setAll(bodyContent);
}
content.getChildren().add(body);
}
getChildren().add(content);
icon = new StackPane();
StackPane.setAlignment(icon, Pos.TOP_RIGHT);
icon.setPickOnBounds(false);
{
iconView = new ImageView();
StackPane.setAlignment(iconView, Pos.CENTER_RIGHT);
StackPane.setMargin(iconView, new Insets(0, 12, 0, 0));
iconView.setImage(new Image("/assets/img/icon.png"));
icon.getChildren().add(iconView);
}
getChildren().add(icon);
}
public VersionItem() {
initializeComponents();
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear(Theme.blackFillBinding(), 15, 15));
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 15, 15));
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 15, 15));
btnScript.setGraphic(SVG.script(Theme.blackFillBinding(), 15, 15));
FXUtils.installTooltip(btnSettings, Launcher.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Launcher.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Launcher.i18n("version.launch_script"));
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
FXUtils.limitSize(iconView, 32, 32);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,11 +17,14 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
public final class VersionListItem extends StackPane {
@@ -33,7 +36,7 @@ public final class VersionListItem extends StackPane {
private Label lblGameVersion;
@FXML
private ImageView imageView;
private Runnable handler;
@FXML private JFXButton btnSettings;
public VersionListItem(String versionName) {
this(versionName, "");
@@ -48,13 +51,8 @@ public final class VersionListItem extends StackPane {
FXUtils.limitSize(imageView, 32, 32);
}
@FXML
private void onSettings() {
handler.run();
}
public void setOnSettingsButtonClicked(Runnable handler) {
this.handler = handler;
public void setOnSettingsButtonClicked(EventHandler<? super MouseEvent> handler) {
btnSettings.setOnMouseClicked(handler);
}
public void setVersionName(String versionName) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -25,9 +25,8 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
@@ -66,8 +65,8 @@ public final class VersionPage extends StackPane implements DecoratorPage {
@FXML
private JFXTabPane tabPane;
private JFXPopup browsePopup;
private JFXPopup managementPopup;
private final JFXPopup browsePopup;
private final JFXPopup managementPopup;
private Profile profile;
private String version;
@@ -80,17 +79,17 @@ public final class VersionPage extends StackPane implements DecoratorPage {
browsePopup = new JFXPopup(browseList);
managementPopup = new JFXPopup(managementList);
FXUtils.installTooltip(btnDelete, 0, 5000, 0, new Tooltip(Main.i18n("version.manage.remove")));
FXUtils.installTooltip(btnBrowseMenu, 0, 5000, 0, new Tooltip(Main.i18n("settings.game.exploration")));
FXUtils.installTooltip(btnManagementMenu, 0, 5000, 0, new Tooltip(Main.i18n("settings.game.management")));
FXUtils.installTooltip(btnExport, 0, 5000, 0, new Tooltip(Main.i18n("modpack.export")));
FXUtils.installTooltip(btnDelete, Launcher.i18n("version.manage.remove"));
FXUtils.installTooltip(btnBrowseMenu, Launcher.i18n("settings.game.exploration"));
FXUtils.installTooltip(btnManagementMenu, Launcher.i18n("settings.game.management"));
FXUtils.installTooltip(btnExport, Launcher.i18n("modpack.export"));
}
public void load(String id, Profile profile) {
this.version = id;
this.profile = profile;
title.set(Main.i18n("settings.game") + " - " + id);
title.set(Launcher.i18n("settings.game") + " - " + id);
versionSettingsController.loadVersionSetting(profile, id);
modController.setParentTab(tabPane);
@@ -162,7 +161,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
deleteVersion(profile, version);
break;
case 2: // redownload asset index
new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version).resolve(profile.getRepository())).start();
new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getResolvedVersion(version)).start();
break;
case 3: // delete libraries
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
@@ -186,7 +185,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void deleteVersion(Profile profile, String version) {
Controllers.confirmDialog(Main.i18n("version.manage.remove.confirm", version), Main.i18n("message.confirm"), () -> {
Controllers.confirmDialog(Launcher.i18n("version.manage.remove.confirm", version), Launcher.i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
@@ -195,7 +194,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void renameVersion(Profile profile, String version) {
Controllers.inputDialog(Main.i18n("version.manage.rename.message"), res -> {
Controllers.inputDialog(Launcher.i18n("version.manage.rename.message"), res -> {
if (profile.getRepository().renameVersion(version, res)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
@@ -204,6 +203,6 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void exportVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), Main.i18n("modpack.wizard"));
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), Launcher.i18n("modpack.wizard"));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -17,34 +17,33 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ImagePickerItem;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.NumberValidator;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -76,37 +75,35 @@ public final class VersionSettingsController {
@FXML private MultiFileItem javaItem;
@FXML private MultiFileItem gameDirItem;
@FXML private JFXToggleButton chkShowLogs;
@FXML private JFXButton btnIconSelection;
@FXML private ImageView iconView;
@FXML private ImagePickerItem iconPickerItem;
@FXML
private void initialize() {
lblPhysicalMemory.setText(Main.i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
lblPhysicalMemory.setText(Launcher.i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
FXUtils.smoothScrolling(scroll);
Task.of(variables -> {
variables.set("list", JavaVersion.getJREs().values().stream().map(javaVersion ->
javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion)
).collect(Collectors.toList()));
}).subscribe(Schedulers.javafx(), variables ->
javaItem.loadChildren(variables.<Collection<Node>>get("list"))
);
Task.of(variables -> variables.set("list", JavaVersion.getJREs()))
.subscribe(Schedulers.javafx(), variables -> {
javaItem.loadChildren(
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.collect(Collectors.toList()));
initializeSelectedJava();
});
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
gameDirItem.loadChildren(Arrays.asList(
gameDirItem.createChildren(Main.i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(Main.i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
gameDirItem.createChildren(Launcher.i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(Launcher.i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
));
globalItem.loadChildren(Arrays.asList(
globalItem.createChildren(Main.i18n("settings.type.global"), true),
globalItem.createChildren(Main.i18n("settings.type.special"), false)
globalItem.createChildren(Launcher.i18n("settings.type.global"), true),
globalItem.createChildren(Launcher.i18n("settings.type.special"), false)
));
FXUtils.installTooltip(btnIconSelection, 0, 5000, 0, new Tooltip(Main.i18n("button.edit")));
}
public void loadVersionSetting(Profile profile, String versionId) {
@@ -156,29 +153,17 @@ public final class VersionSettingsController {
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(versionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.isCustomToggle(newValue)) {
versionSetting.setJava("Custom");
versionSetting.setDefaultJavaPath(null);
} else {
versionSetting.setJava(((JavaVersion) newValue.getUserData()).getVersion());
JavaVersion java = (JavaVersion) newValue.getUserData();
versionSetting.setJava(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
}
});
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
@@ -197,16 +182,14 @@ public final class VersionSettingsController {
});
versionSetting.usesGlobalProperty().setChangedListenerAndOperate(it ->
globalItem.setSubtitle(Main.i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
globalItem.setSubtitle(Launcher.i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
gameDirItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == versionSetting.getGameDirType())
.findFirst().ifPresent(toggle -> toggle.setSelected(true));
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
gameDirItem.setToggleSelectedListener(newValue -> {
versionSetting.setGameDirType((EnumGameDirectory) newValue.getUserData());
});
gameDirItem.setToggleSelectedListener(newValue -> versionSetting.setGameDirType((EnumGameDirectory) newValue.getUserData()));
versionSetting.gameDirProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
versionSetting.gameDirTypeProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
@@ -214,9 +197,33 @@ public final class VersionSettingsController {
lastVersionSetting = versionSetting;
initializeSelectedJava();
loadIcon();
}
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !JavaVersion.getJREsImmediately().isPresent() /* JREs are still being loaded */) {
return;
}
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(lastVersionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
}
private void initJavaSubtitle(VersionSetting versionSetting) {
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
@@ -239,7 +246,7 @@ public final class VersionSettingsController {
@FXML
private void onExploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("extension.png"), "*.png"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("extension.png"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIcon(versionId);
@@ -255,9 +262,9 @@ public final class VersionSettingsController {
private void loadIcon() {
File iconFile = profile.getRepository().getVersionIcon(versionId);
if (iconFile.exists())
iconView.setImage(new Image("file:" + iconFile.getAbsolutePath()));
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconView.setImage(FXUtils.DEFAULT_ICON);
FXUtils.limitSize(iconView, 32, 32);
iconPickerItem.setImage(FXUtils.DEFAULT_ICON);
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright c 2017 huangyuhui <[email protected]>
* Copyright c 2018 huangyuhui <[email protected]>
*
* 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
@@ -83,7 +83,7 @@ public enum ContainerAnimations {
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))));
private AnimationProducer animationProducer;
private final AnimationProducer animationProducer;
ContainerAnimations(AnimationProducer animationProducer) {
this.animationProducer = animationProducer;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,12 +21,9 @@ import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
public final class TransitionHandler implements AnimationHandler {
private final StackPane view;
@@ -40,6 +37,12 @@ public final class TransitionHandler implements AnimationHandler {
public TransitionHandler(StackPane view) {
this.view = view;
currentNode = view.getChildren().stream().findFirst().orElse(null);
// prevent content overflow
Rectangle clip = new Rectangle();
clip.widthProperty().bind(view.widthProperty());
clip.heightProperty().bind(view.heightProperty());
view.setClip(clip);
}
@Override
@@ -77,9 +80,7 @@ public final class TransitionHandler implements AnimationHandler {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(transition.animate(this));
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
view.getChildren().remove(previousNode);
}));
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> view.getChildren().remove(previousNode)));
nowAnimation.play();
animation = nowAnimation;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,11 +18,13 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import org.jackhuang.hmcl.util.Lang;
/**
* @author huangyuhui
@@ -48,6 +50,13 @@ public class ClassTitle extends StackPane {
getStyleClass().add("class-title");
}
public ClassTitle(String text, Node rightNode) {
this(Lang.apply(new BorderPane(), borderPane -> {
borderPane.setLeft(Lang.apply(new VBox(), vBox -> vBox.getChildren().setAll(new Text(text))));
borderPane.setRight(rightNode);
}));
}
public Node getContent() {
return content;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -60,9 +60,14 @@ public class ComponentList extends StackPane {
child.getStyleClass().add("options-list-item-ahead");
else
child.getStyleClass().add("options-list-item");
child.getProperties().put("node", node);
vbox.getChildren().add(child);
}
public void removeChildren(Node node) {
vbox.getChildren().removeIf(node1 -> node1.getProperties().get("node") == node);
}
public String getTitle() {
return title.get();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -31,17 +31,17 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
/**
* @author
* @author huangyuhui
*/
public class ComponentListCell extends StackPane {
private final Node content;
private Animation expandAnimation;
private Rectangle clipRect;
private double animatedHeight;
private final BooleanProperty expanded = new SimpleBooleanProperty(this, "expanded", false);
public ComponentListCell(Node content) {
@@ -78,7 +78,7 @@ public class ComponentListCell extends StackPane {
StackPane groupNode = new StackPane();
groupNode.getStyleClass().add("options-list-item-header");
Node expandIcon = SVG.expand("black", 10, 10);
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
@@ -129,26 +129,20 @@ public class ComponentListCell extends StackPane {
updateClip(newHeight);
}
animatedHeight = newAnimatedHeight;
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
));
if (!isExpanded()) {
expandAnimation.setOnFinished(e2 -> {
updateClip(newHeight);
animatedHeight = 0.0;
});
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
}
expandAnimation.play();
});
expandedProperty().addListener((a, b, newValue) -> {
expandIcon.setRotate(newValue ? 180 : 0);
});
expandedProperty().addListener((a, b, newValue) ->
expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(holder);
} else
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -26,7 +26,8 @@ import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -50,10 +51,10 @@ public class FileItem extends BorderPane {
setLeft(left);
JFXButton right = new JFXButton();
right.setGraphic(SVG.pencil("black", 15, 15));
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
FXUtils.installTooltip(right, 0, 5000, 0, new Tooltip(Main.i18n("button.edit")));
FXUtils.installTooltip(right, Launcher.i18n("button.edit"));
setRight(right);
Tooltip tip = new Tooltip();
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,21 +21,19 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Paint;
public class IconedItem extends RipplerContainer {
private final Node icon;
private final String text;
public IconedItem(Node icon, String text) {
super(createHBox(icon, text));
this.icon = icon;
this.text = text;
}
private static HBox createHBox(Node icon, String text) {
HBox hBox = new HBox();
icon.setMouseTransparent(true);
Label textLabel = new Label(text);
textLabel.setId("label");
textLabel.setAlignment(Pos.CENTER);
textLabel.setMouseTransparent(true);
hBox.getChildren().addAll(icon, textLabel);
@@ -43,4 +41,12 @@ public class IconedItem extends RipplerContainer {
hBox.setAlignment(Pos.CENTER_LEFT);
return hBox;
}
public void setText(String text) {
((Label) lookup("#label")).setText(text);
}
public void setTextFill(Paint paint) {
((Label) lookup("#label")).setTextFill(paint);
}
}
@@ -0,0 +1,101 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@DefaultProperty("image")
public final class ImagePickerItem extends BorderPane {
private final ImageView imageView;
private final JFXButton selectButton;
private final Label label;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
public ImagePickerItem() {
imageView = new ImageView();
imageView.setSmooth(false);
imageView.setPreserveRatio(true);
selectButton = new JFXButton();
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, Launcher.i18n("button.edit"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setSpacing(8);
setRight(hBox);
VBox vBox = new VBox();
label = new Label();
label.textProperty().bind(title);
vBox.getChildren().setAll(label);
vBox.setAlignment(Pos.CENTER_LEFT);
setLeft(vBox);
imageView.imageProperty().bind(image);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public EventHandler<? super MouseEvent> getOnSelectButtonClicked() {
return onSelectButtonClicked.get();
}
public ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClickedProperty() {
return onSelectButtonClicked;
}
public void setOnSelectButtonClicked(EventHandler<? super MouseEvent> onSelectButtonClicked) {
this.onSelectButtonClicked.set(onSelectButtonClicked);
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void setImage(Image image) {
this.image.set(image);
}
public ImageView getImageView() {
return imageView;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,18 +18,16 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
public class InputDialogPane extends StackPane {
private final String text;
private final JFXDialog dialog;
@FXML
private JFXButton acceptButton;
@@ -40,16 +38,13 @@ public class InputDialogPane extends StackPane {
@FXML
private Label content;
public InputDialogPane(String text, JFXDialog dialog, Consumer<String> onResult) {
this.text = text;
this.dialog = dialog;
public InputDialogPane(String text, Consumer<Region> closeConsumer, Consumer<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
cancelButton.setOnMouseClicked(e -> dialog.close());
cancelButton.setOnMouseClicked(e -> closeConsumer.accept(this));
acceptButton.setOnMouseClicked(e -> {
onResult.accept(textField.getText());
dialog.close();
closeConsumer.accept(this);
});
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import javax.swing.*;
import java.util.Optional;
@@ -29,7 +30,7 @@ public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = Main.i18n("message.info");
private static final String TITLE = Launcher.i18n("message.info");
/**
* User Operation: Yes
@@ -106,12 +107,12 @@ public final class MessageBox {
* @param title The title of MsgBox.
* @return user operation.
*/
public static void show(String message, String title) {
public static Optional<ButtonType> show(String message, String title) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
alert.showAndWait();
return alert.showAndWait();
}
public static int confirm(String message, String title) {
@@ -123,28 +124,32 @@ public final class MessageBox {
}
public static int confirm(String message, String title, int option) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unrecognized button type:" + buttonType.get());
return FXUtils.runInUIThread(() -> {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
default:
throw new IllegalArgumentException("Unrecognized message box option " + option);
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unexpected button type:" + buttonType.get());
});
}
public static Optional<String> input(String message) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 4017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,16 +18,18 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.util.Optional;
import java.util.function.Consumer;
public final class MessageDialogPane extends StackPane {
private boolean closingDialog = true;
@@ -45,7 +47,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, JFXDialog dialog, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, Consumer<Region> closeConsumer, int type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -53,8 +55,7 @@ public final class MessageDialogPane extends StackPane {
content.setText(text);
acceptButton.setOnMouseClicked(e -> {
if (closingDialog)
dialog.close();
closeConsumer.accept(MessageDialogPane.this);
Optional.ofNullable(onAccept).ifPresent(Runnable::run);
});
@@ -62,35 +63,36 @@ public final class MessageDialogPane extends StackPane {
switch (type) {
case MessageBox.INFORMATION_MESSAGE:
graphic.setGraphic(SVG.infoCircle("black", 40, 40));
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.ERROR_MESSAGE:
graphic.setGraphic(SVG.closeCircle("black", 40, 40));
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.FINE_MESSAGE:
graphic.setGraphic(SVG.checkCircle("black", 40, 40));
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.WARNING_MESSAGE:
graphic.setGraphic(SVG.alert("black", 40, 40));
graphic.setGraphic(SVG.alert(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.QUESTION_MESSAGE:
graphic.setGraphic(SVG.helpCircle("black", 40, 40));
graphic.setGraphic(SVG.helpCircle(Theme.blackFillBinding(), 40, 40));
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
}
public MessageDialogPane(String text, String title, JFXDialog dialog, Runnable onAccept, Runnable onCancel) {
this(text, title, dialog, MessageBox.QUESTION_MESSAGE, onAccept);
public MessageDialogPane(String text, String title, Consumer<Region> closeConsumer, Runnable onAccept, Runnable onCancel) {
this(text, title, closeConsumer, MessageBox.QUESTION_MESSAGE, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
if (closingDialog)
dialog.close();
closeConsumer.accept(MessageDialogPane.this);
Optional.ofNullable(onCancel).ifPresent(Runnable::run);
});
acceptButton.setText(Main.i18n("button.yes"));
cancelButton.setText(Main.i18n("button.no"));
acceptButton.setText(Launcher.i18n("button.yes"));
cancelButton.setText(Launcher.i18n("button.no"));
actions.getChildren().add(cancelButton);
}
@@ -0,0 +1,146 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXColorPicker;
import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
public class MultiColorItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Launcher.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Launcher.i18n("selector.choose_file"));
private final ToggleGroup group = new ToggleGroup();
private final JFXColorPicker colorPicker = new JFXColorPicker();
private final JFXRadioButton radioCustom = new JFXRadioButton();
private final BorderPane custom = new BorderPane();
private final VBox pane = new VBox();
private final boolean hasCustom;
private Consumer<Toggle> toggleSelectedListener;
private Consumer<Color> colorConsumer;
public MultiColorItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
this.hasCustom = hasCustom;
radioCustom.textProperty().bind(customTitleProperty());
radioCustom.setToggleGroup(group);
colorPicker.disableProperty().bind(radioCustom.selectedProperty().not());
colorPicker.setOnAction(e -> Optional.ofNullable(colorConsumer).ifPresent(c -> c.accept(colorPicker.getValue())));
custom.setLeft(radioCustom);
custom.setStyle("-fx-padding: 3;");
HBox right = new HBox();
right.setSpacing(3);
right.getChildren().addAll(colorPicker);
custom.setRight(right);
FXUtils.setLimitHeight(custom, 40);
pane.setStyle("-fx-padding: 0 0 10 0;");
pane.setSpacing(8);
if (hasCustom)
pane.getChildren().add(custom);
addChildren(pane);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
});
}
public Node createChildren(String title) {
return createChildren(title, null);
}
public Node createChildren(String title, Object userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, Object userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
JFXRadioButton left = new JFXRadioButton(title);
left.setToggleGroup(group);
left.setUserData(userData);
pane.setLeft(left);
Label right = new Label(subtitle);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
return pane;
}
public void loadChildren(Collection<Node> list) {
pane.getChildren().setAll(list);
if (hasCustom)
pane.getChildren().add(custom);
}
public ToggleGroup getGroup() {
return group;
}
public String getCustomTitle() {
return customTitle.get();
}
public StringProperty customTitleProperty() {
return customTitle;
}
public void setCustomTitle(String customTitle) {
this.customTitle.set(customTitle);
}
public String getChooserTitle() {
return chooserTitle.get();
}
public StringProperty chooserTitleProperty() {
return chooserTitle;
}
public void setChooserTitle(String chooserTitle) {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(Object userData) {
radioCustom.setUserData(userData);
}
public boolean isCustomToggle(Toggle toggle) {
return radioCustom == toggle;
}
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
toggleSelectedListener = consumer;
}
public void setOnColorPickerChanged(Consumer<Color> consumer) {
colorConsumer = consumer;
}
public void setColor(Color color) {
colorPicker.setValue(color);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -37,7 +37,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -47,10 +48,10 @@ import java.util.Collection;
import java.util.function.Consumer;
public class MultiFileItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Main.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Main.i18n("selector.choose_file"));
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Launcher.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Launcher.i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
private ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
private final JFXTextField txtCustom = new JFXTextField();
@@ -67,7 +68,7 @@ public class MultiFileItem extends ComponentList {
BorderPane.setAlignment(txtCustom, Pos.CENTER_RIGHT);
btnSelect.setGraphic(SVG.folderOpen("black", 15, 15));
btnSelect.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
btnSelect.setOnMouseClicked(e -> {
if (isDirectory()) {
DirectoryChooser chooser = new DirectoryChooser();
@@ -146,7 +147,7 @@ public class MultiFileItem extends ComponentList {
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(Main.i18n(getChooserTitle()));
chooser.setTitle(Launcher.i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -21,13 +21,9 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.MultipleSelectionModel;
public final class NoneMultipleSelectionModel<T> extends MultipleSelectionModel<T> {
private static final NoneMultipleSelectionModel INSTANCE = new NoneMultipleSelectionModel();
private NoneMultipleSelectionModel() {}
public class NoneMultipleSelectionModel<T> extends MultipleSelectionModel<T> {
@SuppressWarnings("unchecked")
public static <T> NoneMultipleSelectionModel<T> getInstance() {
return (NoneMultipleSelectionModel<T>) INSTANCE;
public NoneMultipleSelectionModel() {
}
@Override
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,10 +20,11 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
public class NumberValidator extends ValidatorBase {
private boolean nullable;
private final boolean nullable;
public NumberValidator() {
this(false);
@@ -49,13 +50,8 @@ public class NumberValidator extends ValidatorBase {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(nullable);
hasErrors.set(!nullable);
else
try {
Integer.parseInt(textField.getText());
hasErrors.set(false);
} catch (NumberFormatException e) {
hasErrors.set(true);
}
hasErrors.set(Lang.toIntOrNull(textField.getText()) == null);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -30,7 +30,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
@@ -217,10 +216,12 @@ public class RipplerContainer extends StackPane {
private static class StyleableProperties {
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();
}
@@ -232,7 +233,7 @@ public class RipplerContainer extends StackPane {
}
static {
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Parent.getClassCssMetaData());
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Node.getClassCssMetaData());
Collections.addAll(styleables, RIPPLER_FILL);
STYLEABLES = Collections.unmodifiableList(styleables);
}
@@ -0,0 +1,49 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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 javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Stack;
public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
public void push(Node node) {
stack.push(node);
getChildren().setAll(node);
Logging.LOG.info(this + " " + stack);
}
public void pop(Node node) {
boolean flag = stack.remove(node);
if (stack.isEmpty())
getChildren().setAll();
else
getChildren().setAll(stack.peek());
Logging.LOG.info(this + " " + stack + ", removed: " + flag + ", object: " + node);
}
public boolean isEmpty() {
return stack.isEmpty();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -23,15 +23,17 @@ import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
import java.util.function.Consumer;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Runnable onCancel;
private Consumer<Region> onCancel;
@FXML
private JFXProgressBar progressBar;
@@ -44,20 +46,22 @@ public class TaskExecutorDialogPane extends StackPane {
@FXML
private TaskListPane taskListPane;
public TaskExecutorDialogPane(Runnable cancel) {
public TaskExecutorDialogPane(Consumer<Region> cancel) {
FXUtils.loadFXML(this, "/assets/fxml/task-dialog.fxml");
setCancel(cancel);
btnCancel.setOnMouseClicked(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
onCancel.run();
onCancel.accept(this);
});
}
public void setExecutor(TaskExecutor executor) {
this.executor = executor;
taskListPane.setExecutor(executor);
if (executor != null)
taskListPane.setExecutor(executor);
}
public StringProperty titleProperty() {
@@ -91,7 +95,7 @@ public class TaskExecutorDialogPane extends StackPane {
progressBar.setProgress(progress);
}
public void setCancel(Runnable onCancel) {
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;
JFXUtilities.runInFX(() -> btnCancel.setDisable(onCancel == null));
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
@@ -33,12 +33,12 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
@Override
default void handleTask(Map<String, Object> settings, Task task) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(() -> {
Controllers.closeDialog();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(it -> {
Controllers.closeDialog(it);
Controllers.navigate(null);
});
pane.setTitle(Main.i18n("message.doing"));
pane.setTitle(Launcher.i18n("message.doing"));
pane.setProgress(Double.MAX_VALUE);
if (settings.containsKey("title")) {
Object title = settings.get("title");
@@ -57,24 +57,27 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
}
JFXUtilities.runInFX(() -> {
TaskExecutor executor = task.executor(e -> new TaskListener() {
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onSucceed() {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
JFXUtilities.runInFX(() -> Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null)));
else if (!settings.containsKey("forbid_success_message"))
JFXUtilities.runInFX(() -> Controllers.dialog(Main.i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null)));
}
public void onStop(boolean success, TaskExecutor executor) {
JFXUtilities.runInFX(() -> {
Controllers.closeDialog(pane);
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null));
else if (!settings.containsKey("forbid_success_message"))
Controllers.dialog(Launcher.i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null));
} else {
if (executor.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(executor.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, Launcher.i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
}
@Override
public void onTerminate() {
if (e.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(e.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
JFXUtilities.runInFX(() -> Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null)));
else if (!settings.containsKey("forbid_failure_message"))
JFXUtilities.runInFX(() -> Controllers.dialog(appendix, Main.i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null)));
});
}
});
pane.setExecutor(executor);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -23,7 +23,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameAssetRefreshTask;
@@ -31,10 +31,7 @@ import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.CurseInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
@@ -60,32 +57,34 @@ public final class TaskListPane extends StackPane {
}
@Override
public void onReady(Task task) {
public void onRunning(Task task) {
if (!task.getSignificance().shouldShow())
return;
if (task instanceof GameAssetRefreshTask) {
task.setName(Main.i18n("assets.download"));
task.setName(Launcher.i18n("assets.download"));
} else if (task instanceof GameAssetDownloadTask) {
task.setName(Main.i18n("assets.download_all"));
task.setName(Launcher.i18n("assets.download_all"));
} else if (task instanceof ForgeInstallTask) {
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.forge")));
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.liteloader")));
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.liteloader")));
} else if (task instanceof OptiFineInstallTask) {
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.optifine")));
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.optifine")));
} else if (task instanceof CurseCompletionTask) {
task.setName(Main.i18n("modpack.type.curse.completion"));
task.setName(Launcher.i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
task.setName(Launcher.i18n("modpack.installing"));
} else if (task instanceof CurseInstallTask) {
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.curse")));
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.multimc")));
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.hmcl")));
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.hmcl")));
} else if (task instanceof HMCLModpackExportTask) {
task.setName(Main.i18n("modpack.export"));
task.setName(Launcher.i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(Main.i18n("modpack.scan"));
task.setName(Launcher.i18n("modpack.scan"));
}
ProgressListNode node = new ProgressListNode(task);
@@ -0,0 +1,37 @@
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);
}
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -22,8 +22,9 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -31,8 +32,9 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Lang;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.Main.i18n;
import static org.jackhuang.hmcl.Launcher.i18n;
class AdditionalInstallersPage extends StackPane implements WizardPage {
private final InstallerWizardProvider provider;
@@ -98,29 +100,33 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Main.i18n("settings.tabs.installers");
return Launcher.i18n("settings.tabs.installers");
}
private String getVersion(String id) {
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion<?>) it).map(RemoteVersion::getSelfVersion).orElse(null);
}
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(Main.i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
lblGameVersion.setText(Launcher.i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
btnForge.setDisable(provider.getForge() != null);
if (provider.getForge() != null || controller.getSettings().containsKey("forge"))
lblForge.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), controller.getSettings().get("forge")));
lblForge.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), getVersion("forge")));
else
lblForge.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.forge")));
lblForge.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.forge")));
btnLiteLoader.setDisable(provider.getLiteLoader() != null);
if (provider.getLiteLoader() != null || controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), controller.getSettings().get("liteloader")));
lblLiteLoader.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), getVersion("liteloader")));
else
lblLiteLoader.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.liteloader")));
lblLiteLoader.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.liteloader")));
btnOptiFine.setDisable(provider.getOptiFine() != null);
if (provider.getOptiFine() != null || controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), controller.getSettings().get("optifine")));
lblOptiFine.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), getVersion("optifine")));
else
lblOptiFine.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.optifine")));
lblOptiFine.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.optifine")));
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,18 +18,14 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackManifest;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.MultiMCInstallVersionSettingTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
@@ -38,7 +34,8 @@ import org.jackhuang.hmcl.util.Lang;
import java.io.File;
import java.util.Map;
import static org.jackhuang.hmcl.Main.i18n;
import static org.jackhuang.hmcl.Launcher.i18n;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class DownloadWizardProvider implements WizardProvider {
private Profile profile;
@@ -53,16 +50,16 @@ public final class DownloadWizardProvider implements WizardProvider {
GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name"));
builder.gameVersion((String) settings.get("game"));
builder.gameVersion(((RemoteVersion<?>) settings.get("game")).getGameVersion());
if (settings.containsKey("forge"))
builder.version("forge", (String) settings.get("forge"));
builder.version((RemoteVersion<?>) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version("liteloader", (String) settings.get("liteloader"));
builder.version((RemoteVersion<?>) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version("optifine", (String) settings.get("optifine"));
builder.version((RemoteVersion<?>) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions());
}
@@ -71,9 +68,9 @@ public final class DownloadWizardProvider implements WizardProvider {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null;
File selected = Lang.get(settings, ModpackPage.MODPACK_FILE, File.class, null);
Modpack modpack = Lang.get(settings, ModpackPage.MODPACK_CURSEFORGE_MANIFEST, Modpack.class, null);
String name = Lang.get(settings, ModpackPage.MODPACK_NAME, String.class, null);
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_CURSEFORGE_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);
@@ -81,8 +78,8 @@ public final class DownloadWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", Main.i18n("install.success"));
settings.put("failure_message", Main.i18n("install.failed"));
settings.put("success_message", Launcher.i18n("install.success"));
settings.put("failure_message", Launcher.i18n("install.failed"));
switch (Lang.parseInt(settings.get(InstallTypePage.INSTALL_TYPE), -1)) {
case 0: return finishVersionDownloadingAsync(settings);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXListView;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -28,15 +28,15 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
public final class InstallTypePage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML private JFXListView<Object> list;
public InstallTypePage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/dltype.fxml");
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
controller.getSettings().put(INSTALL_TYPE, list.getSelectionModel().getSelectedIndex());
controller.onNext();
});
@@ -49,7 +49,7 @@ public final class InstallTypePage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Main.i18n("install.select");
return Launcher.i18n("install.select");
}
public static final String INSTALL_TYPE = "INSTALL_TYPE";
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -18,8 +18,9 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
@@ -79,19 +80,19 @@ public final class InstallerWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", Main.i18n("install.success"));
settings.put("failure_message", Main.i18n("install.failed"));
settings.put("success_message", Launcher.i18n("install.success"));
settings.put("failure_message", Launcher.i18n("install.failed"));
Task ret = Task.empty();
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync(gameVersion, version, "forge", (String) settings.get("forge")));
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync(gameVersion, version, "liteloader", (String) settings.get("liteloader")));
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync(gameVersion, version, "optifine", (String) settings.get("optifine")));
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -23,8 +23,9 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -36,8 +37,6 @@ import java.util.Map;
public class InstallersPage extends StackPane implements WizardPage {
private final WizardController controller;
private final GameRepository repository;
private final DownloadProvider downloadProvider;
@FXML
private VBox list;
@@ -71,56 +70,58 @@ public class InstallersPage extends StackPane implements WizardPage {
public InstallersPage(WizardController controller, GameRepository repository, DownloadProvider downloadProvider) {
this.controller = controller;
this.repository = repository;
this.downloadProvider = downloadProvider;
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
String gameVersion = (String) controller.getSettings().get("game");
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(Main.i18n("install.new_game.already_exists"));
hasVersion.setMessage(Launcher.i18n("install.new_game.already_exists"));
txtName.getValidators().add(hasVersion);
txtName.textProperty().addListener(e -> btnInstall.setDisable(!txtName.validate()));
txtName.setText(gameVersion);
btnForge.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 0);
controller.onNext(new VersionsPage(controller, Main.i18n("install.installer.choose", Main.i18n("install.installer.forge")), gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, Launcher.i18n("install.installer.choose", Launcher.i18n("install.installer.forge")), gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
});
btnLiteLoader.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 1);
controller.onNext(new VersionsPage(controller, Main.i18n("install.installer.choose", Main.i18n("install.installer.liteloader")), gameVersion, downloadProvider, "liteloader", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, Launcher.i18n("install.installer.choose", Launcher.i18n("install.installer.liteloader")), gameVersion, downloadProvider, "liteloader", () -> controller.onPrev(false)));
});
btnOptiFine.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 2);
controller.onNext(new VersionsPage(controller, Main.i18n("install.installer.choose", Main.i18n("install.installer.optifine")), gameVersion, downloadProvider, "optifine", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, Launcher.i18n("install.installer.choose", Launcher.i18n("install.installer.optifine")), gameVersion, downloadProvider, "optifine", () -> controller.onPrev(false)));
});
}
@Override
public String getTitle() {
return Main.i18n("install.new_game");
return Launcher.i18n("install.new_game");
}
private String getVersion(String id) {
return ((RemoteVersion<?>) controller.getSettings().get(id)).getSelfVersion();
}
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(Main.i18n("install.new_game.current_game_version") + ": " + controller.getSettings().get("game"));
lblGameVersion.setText(Launcher.i18n("install.new_game.current_game_version") + ": " + getVersion("game"));
if (controller.getSettings().containsKey("forge"))
lblForge.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.forge")) + ": " + controller.getSettings().get("forge"));
lblForge.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.forge")) + ": " + getVersion("forge"));
else
lblForge.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.forge")));
lblForge.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.forge")));
if (controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.liteloader")) + ": " + controller.getSettings().get("liteloader"));
lblLiteLoader.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.liteloader")) + ": " + getVersion("liteloader"));
else
lblLiteLoader.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.liteloader")));
lblLiteLoader.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.liteloader")));
if (controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.optifine")) + ": " + controller.getSettings().get("optifine"));
lblOptiFine.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.optifine")) + ": " + getVersion("optifine"));
else
lblOptiFine.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.optifine")));
lblOptiFine.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.optifine")));
}
@Override
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -25,7 +25,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -75,16 +75,16 @@ public final class ModpackPage extends StackPane implements WizardPage {
Profile profile = (Profile) controller.getSettings().get("PROFILE");
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("modpack"), "*.zip"));
chooser.setTitle(Launcher.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) Platform.runLater(() -> Controllers.navigate(null));
else {
controller.getSettings().put(MODPACK_FILE, selectedFile);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
txtModpackName.getValidators().addAll(
new Validator(Main.i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(Main.i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
new Validator(Launcher.i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(Launcher.i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
@@ -96,7 +96,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
lblAuthor.setText(manifest.getAuthor());
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
} catch (UnsupportedModpackException e) {
txtModpackName.setText(Main.i18n("modpack.task.install.error"));
txtModpackName.setText(Launcher.i18n("modpack.task.install.error"));
}
}
}
@@ -118,14 +118,14 @@ public final class ModpackPage extends StackPane implements WizardPage {
if (manifest != null) {
WebStage stage = new WebStage();
stage.getWebView().getEngine().loadContent(manifest.getDescription());
stage.setTitle(Main.i18n("modpack.wizard.step.3"));
stage.setTitle(Launcher.i18n("modpack.wizard.step.3"));
stage.showAndWait();
}
}
@Override
public String getTitle() {
return Main.i18n("modpack.task.install");
return Launcher.i18n("modpack.task.install");
}
public static final String MODPACK_FILE = "MODPACK_FILE";
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -27,7 +27,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.game.GameVersionList;
@@ -41,15 +40,13 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class VersionsPage extends StackPane implements WizardPage, Refreshable {
private final WizardController controller;
private final String gameVersion;
private final DownloadProvider downloadProvider;
private final String libraryId;
private final Runnable callback;
private final String title;
@FXML
@@ -74,12 +71,10 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private TaskExecutor executor;
public VersionsPage(WizardController controller, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.controller = controller;
this.title = title;
this.gameVersion = gameVersion;
this.downloadProvider = downloadProvider;
this.libraryId = libraryId;
this.callback = callback;
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
@@ -94,8 +89,10 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
chkSnapshot.selectedProperty().addListener(listener);
chkOld.selectedProperty().addListener(listener);
list.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
controller.getSettings().put(libraryId, newValue.getRemoteVersion().getSelfVersion());
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
controller.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem().getRemoteVersion());
callback.run();
});
refresh();
@@ -116,6 +113,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
}
else return true;
})
.filter(Objects::nonNull)
.sorted()
.map(VersionsPageItem::new).collect(Collectors.toList());
}
Loaded 100 of 379 files, more files were not shown because too many files have changed in this diff. Show more