Compare commits

...
151 Commits
Author SHA1 Message Date
huangyuhui 748d195db5 launch4j 2018-07-19 00:55:23 +08:00
huanghongxun 3a9017adb8 Merge pull request #396 from yushijinhun/fix
支持背景热替换
2018-07-18 23:05:56 +08:00
yushijinhun eff3186ab5 Background hot replacement 2018-07-18 22:51:02 +08:00
yushijinhun e5f5fe878e Use bindBidirectional for background type 2018-07-18 21:59:12 +08:00
yushijinhun a47f0f6ecf Use databind for theme subtitle 2018-07-18 21:47:30 +08:00
yushijinhun 9896165b68 Remove theme in Settings 2018-07-18 21:36:44 +08:00
huanghongxun 5e3adcee93 Merge pull request #395 from yushijinhun/fix
Fix ExportWizardProvider leaks proxy config
2018-07-18 20:42:42 +08:00
yushijinhun b252266a46 Fix ExportWizardProvider leaks proxy config 2018-07-18 20:40:18 +08:00
huanghongxun 1d5adea114 Merge pull request #394 from yushijinhun/refactor-settings
重构 Settings
2018-07-18 20:17:12 +08:00
yushijinhun a64a51c7df Refactor proxy UI 2018-07-18 20:07:13 +08:00
yushijinhun ec41c13b45 Update docs for tasks in org.jackhuang.hmcl.download.game 2018-07-18 20:05:54 +08:00
yushijinhun 486ca434ff Remove proxy HMCLDependencyManager 2018-07-18 20:05:54 +08:00
yushijinhun fe87a899c8 Remove proxy from AccountHelper 2018-07-18 20:05:54 +08:00
yushijinhun 06806bd383 Remove proxy from FileDownloadTask/GetTask/NetworkUtils 2018-07-18 20:05:54 +08:00
yushijinhun 2f6c7e1ce2 Remove proxy from AccountFactory 2018-07-18 20:05:54 +08:00
yushijinhun 141cda13f1 Add proxyProperty() 2018-07-18 20:05:54 +08:00
yushijinhun 3e8c8afcd1 Add get/set/property methods for Config 2018-07-18 20:05:16 +08:00
yushijinhun 8236f613e0 Use static import for ConfigHolder.CONFIG 2018-07-18 20:03:34 +08:00
yushijinhun ea6108dca7 Use bindBidirectional 2018-07-18 20:02:33 +08:00
yushijinhun 985a4f07e0 Use Authenticator to set proxy username/password 2018-07-18 20:02:28 +08:00
yushijinhun 5642c579a4 Add ProxyManager 2018-07-17 22:57:33 +08:00
yushijinhun 8e0559fb3b Remove proxyType 2018-07-17 22:56:00 +08:00
yushijinhun a6a1755fee Remove getRawConfig 2018-07-17 22:56:00 +08:00
yushijinhun 846f383f7b Remove backgroundImageTypeProperty 2018-07-17 22:56:00 +08:00
yushijinhun bbbe3726f2 Remove backgroundImageProperty 2018-07-17 22:56:00 +08:00
yushijinhun 6d83fd9903 Remove simple properties in Settings 2018-07-17 22:56:00 +08:00
yushijinhun fac62a8d24 Replace save() with observer pattern 2018-07-17 22:56:00 +08:00
yushijinhun b0d71a5511 Let Config implement Observable 2018-07-17 22:56:00 +08:00
huangyuhui 124ba34d48 Prevent appearance of the horizontal slide bar in install type page 2018-07-17 21:00:22 +08:00
huangyuhui 75270b8ad6 Fix wrong relativized path 2018-07-17 20:56:49 +08:00
yushijinhun 5cf6ef88a6 Let Account implement Observable 2018-07-17 17:04:27 +08:00
huangyuhui b5f72af513 Friendly hint of failing to rename game version 2018-07-16 20:39:21 +08:00
huangyuhui 1532893c5f Change current directory to run directory for launching scripts 2018-07-16 19:51:12 +08:00
huanghongxun a3fbc9595a Merge pull request #385 from yushijinhun/relative-path
添加设置项:若可能,游戏目录使用相对路径
2018-07-15 18:51:34 +08:00
yushijinhun f789343b6e Add game dir option: use relative path 2018-07-14 14:40:59 +08:00
yushijinhun ff74417352 [FileItem]Replace setProperty with bindBidirectional 2018-07-14 14:40:34 +08:00
huanghongxun a0ff843d0c Merge pull request #383 from yushijinhun/fix-382
Fix #382
2018-07-13 18:39:02 +08:00
yushijinhun da93cb0dbb Fix #382 2018-07-13 18:19:18 +08:00
huangyuhui 0de446c608 Update JFoenix 2018-07-11 22:33:29 +08:00
huangyuhui 03bccf16d4 Remove replacement of logs 2018-07-11 20:56:36 +08:00
huangyuhui 635ae8c11e Remove unused preset themes 2018-07-11 20:55:56 +08:00
huangyuhui dfd5a82112 Fix #365 by blocking all hacked Minecraft logs text as XML 2018-07-11 20:11:09 +08:00
huangyuhui 7dad8d452f Filter preview version of OptiFine 2018-07-11 13:08:08 +08:00
huangyuhui 07f8294876 Fixed mis-unbound data. GH-377 2018-07-11 12:46:02 +08:00
huanghongxun 7185cc410b Merge pull request #376 from yushijinhun/fix
Fix selected account is not loaded
2018-07-09 21:41:44 +08:00
yushijinhun 2d985af60d Fix selected account is not loaded 2018-07-09 21:34:43 +08:00
huanghongxun bee2e64482 Merge pull request #373 from yushijinhun/account-page
完善账户信息页的显示
2018-07-09 19:14:26 +08:00
huanghongxun d5dd43c0c1 Merge pull request #372 from yushijinhun/bmclapi
支持从 BMCLAPI 下载 authlib-injector
2018-07-09 19:14:00 +08:00
yushijinhun 09470eeb8a Use auto layout instead of manual sizing 2018-07-09 14:35:30 +08:00
yushijinhun 094249a6fa Set margin of fields in AccountPage 2018-07-09 14:31:41 +08:00
yushijinhun 6b5302907a [AccountPage]Display auth server url in tooltip of auth server name 2018-07-09 14:09:45 +08:00
yushijinhun adc5b08765 Fix account page overflow 2018-07-09 14:04:30 +08:00
yushijinhun 0150e2a8d7 Support downloading authlib-injector from BMCLAPI 2018-07-09 13:17:19 +08:00
huanghongxun 45eaa0c60f Merge pull request #371 from yushijinhun/fix
Refactor(WIP) & Fixes
2018-07-08 20:06:32 +08:00
yushijinhun 66ed683196 Move config load/save code to ConfigHolder(WIP) 2018-07-08 19:32:43 +08:00
yushijinhun aa4239721e Use NIO api to load/save config 2018-07-08 19:19:37 +08:00
yushijinhun 5b0b926cbd Move config serialization/deserialization to Config class 2018-07-08 19:17:53 +08:00
yushijinhun 929ce398eb AddAccountPane 使用数据绑定 & 登录时设置disable为true 2018-07-08 18:33:37 +08:00
yushijinhun b3fdbe021d 添加验证服务器界面回车代表下一步 2018-07-08 17:47:37 +08:00
huanghongxun 423bd68ce8 Merge pull request #369 from yushijinhun/fix
Fixes
2018-07-08 12:36:29 +08:00
yushijinhun 9bffc68c70 Do not reuse dialog 2018-07-08 12:20:38 +08:00
yushijinhun b7a5a60091 Improve English translations 2018-07-08 10:25:41 +08:00
yushijinhun 333b11c19f Fix uncaught JsonParseException, mentioned in #367 2018-07-08 10:11:14 +08:00
huanghongxun fbd4d1b772 Merge pull request #368 from yushijinhun/dnd
支持 DnD 方式添加验证服务器
2018-07-07 23:28:53 +08:00
yushijinhun 06a58514b3 Fix black ComboBox 2018-07-07 23:06:44 +08:00
yushijinhun 80815fc3a0 authlib-injector DnD support 2018-07-07 23:04:56 +08:00
huanghongxun b97c277a3f Merge pull request #364 from yushijinhun/refactor-ui
UI 重构 & bug 修复
2018-07-07 20:53:22 +08:00
yushijinhun 310d693e2b Remove some error cause inferences
Trampoline - High frequency of false positives.
java.lang.OutOfMemoryError - It's more likely to be a memory leak.
2018-07-07 20:51:28 +08:00
yushijinhun c1596f6a11 Replace Controllers.closeDialog with DialogCloseEvent 2018-07-07 20:50:54 +08:00
yushijinhun 76a04343d9 Use Controllers.dialog for AddAuthlibInjectorServerDialog 2018-07-07 20:30:48 +08:00
yushijinhun 25261c99f1 Remove 'class ' prefix in error messages 2018-07-07 20:30:48 +08:00
yushijinhun d0b994fa9d Move add authentication server dialog to a new file 2018-07-07 20:30:48 +08:00
yushijinhun f259601397 Each TransitionHandler instance uses its own empty pane
Using shared empty pane may cause some probleams
2018-07-07 20:30:47 +08:00
yushijinhun 87904109ca [TransitionHandler]Set mouseTransparent of the whole view
, fix bug introduced in 3fb5048f10
2018-07-07 20:30:47 +08:00
yushijinhun 785790ccc4 Move promptText of cboServers to FXML 2018-07-07 20:29:03 +08:00
yushijinhun 759921d00e Fix missing license headers 2018-07-07 20:28:59 +08:00
huangyuhui e9db183d54 Fix blank page of add authlib-injector server dialog 2018-07-07 20:08:40 +08:00
huangyuhui a853374a88 Support new MultiMC modpack 2018-07-07 00:48:48 +08:00
huangyuhui 3fb5048f10 Fix IllegalStateException in DownloadWizardProvider 2018-07-04 21:41:07 +08:00
huangyuhui 0dd2e758e8 Update READMD.md 2018-07-03 13:02:35 +08:00
huanghongxun d1a562452a Merge pull request #362 from yushijinhun/dst-x3
Add the prompt indicating the miss of DST Root CA X3.
2018-07-01 22:55:01 +08:00
yushijinhun 9205a6cbf4 Show warning when DST Root CA X3 is missing 2018-07-01 22:51:48 +08:00
yushijinhun 4c8961f39a Fix NPE when calling i18n during its initialization 2018-07-01 22:50:28 +08:00
huanghongxun 22bdc23a09 Merge pull request #361 from yushijinhun/i18n
Reconstruct I18N.
2018-07-01 22:11:23 +08:00
yushijinhun 764cd16e21 I18n for missing JavaFX error 2018-07-01 22:00:22 +08:00
yushijinhun 7583f4651d Remove I18nException 2018-07-01 21:59:16 +08:00
yushijinhun 13e931cda4 Fallback to use Locales.DEFAULT when Settings has not been initialized 2018-07-01 21:59:12 +08:00
yushijinhun 9743cf8351 Use static import for I18n.i18n 2018-07-01 21:27:30 +08:00
yushijinhun e5ee5702f8 Move i18n things to util.i18n package 2018-07-01 21:27:28 +08:00
huangyuhui 5bd5b25b77 Add debug message of system details 2018-07-01 19:18:55 +08:00
huangyuhui 55d59f3779 Fix not on FX thread 2018-07-01 19:02:32 +08:00
huangyuhui 7d2ea0b77e Add Tranditional Chinese translation 2018-07-01 13:37:09 +08:00
huangyuhui 1f30c299a5 Fix #360 2018-07-01 13:34:52 +08:00
huanghongxun c887d7e649 Merge pull request #358 from yushijinhun/authlib-injector
重构 authlib-injector 下载,并切换到新 API
2018-07-01 11:54:55 +08:00
yushijinhun 3fc7743a14 Add missing license header 2018-06-30 22:45:57 +08:00
yushijinhun 2a232f70db Refactor authlib-injector downloading, switch to new API 2018-06-30 22:45:57 +08:00
yushijinhun 6fae131287 Display (None) instead of empty string if no Yggdrasil server is available when adding account 2018-06-30 22:44:26 +08:00
yushijinhun 7ee7ef25f9 Refactor integrity check in FileDownloadTask, support multi algorithms 2018-06-30 22:43:57 +08:00
huangyuhui 2bd4bc1472 Add debug message in Task.run 2018-06-30 22:24:46 +08:00
huanghongxun aa170b647e Merge pull request #359 from yushijinhun/fixes
Fix bugs with yggdrasil authentication.
2018-06-30 22:12:06 +08:00
huangyuhui 4fa01216b1 Add debug message for tracing EmptyStackException and IllegalStateException 2018-06-30 22:09:35 +08:00
yushijinhun a82eb996a4 Add missing license header 2018-06-30 21:54:31 +08:00
yushijinhun 9315684ee3 Use CredentialExpiredException to indicate password login
instead of Non-ServerDisconnectException
2018-06-30 21:53:11 +08:00
huangyuhui 34f7774ce9 Fix NPE when the game launched and suddenly crashes 2018-06-30 21:36:57 +08:00
huangyuhui 75031a04cc Fix not on FX application thread 2018-06-30 21:28:41 +08:00
yushijinhun cfb0526cbb Fix potential NPE in YggdrasilAccount(selectedProfile is null) 2018-06-30 20:54:27 +08:00
yushijinhun 9fa56a9cd4 Regard token to be invalid only when remote throws ForbiddenOperationException 2018-06-30 18:52:37 +08:00
huangyuhui c4db9c9e12 Always show the advanced version setting pane as some people cannot find the options 2018-06-27 01:06:43 +08:00
huangyuhui 71272371e4 Fix #356 2018-06-26 21:55:29 +08:00
huangyuhui 91c6c4d233 Reduce size of welcome.png 2018-06-23 22:37:41 +08:00
huangyuhui 8a6653a926 Only show welcome page to Chinese 2018-06-23 22:25:39 +08:00
huangyuhui 76caf84e39 Welcome page 2018-06-23 22:15:23 +08:00
huangyuhui 0fee286b84 Use BMCLAPI as default ownload provider 2018-06-23 19:42:36 +08:00
huangyuhui 4af18a1575 Change build status icon in README.md 2018-06-23 13:03:28 +08:00
huangyuhui d9293f4da8 Display remote error message when RemoteAuthenticationException thrown 2018-06-21 19:31:16 +08:00
huangyuhui db29f97397 Update the link for crash reports 2018-06-17 18:22:22 +08:00
huangyuhui 573f41611d Force using Java 8 when launching Minecraft 1.13 or later 2018-06-17 18:13:47 +08:00
huanghongxun 28ed103ec0 Merge pull request #353 from yushijinhun/translations
完善 UI 提示消息
2018-06-17 17:45:59 +08:00
huanghongxun 5e37750f00 Merge pull request #352 from yushijinhun/refactor-update
添加 hmcl.version.override 选项用于调试更新功能
2018-06-17 17:40:07 +08:00
yushijinhun 5fe3cd9406 移除input.url.http错误提示 2018-06-17 17:28:12 +08:00
yushijinhun b00cc99408 BMCLAPI的链接使用https 2018-06-17 17:27:44 +08:00
yushijinhun 9a66c3bba9 完善翻译
中英文间补白、更正大小写、调整措辞
2018-06-17 17:25:22 +08:00
yushijinhun 546921b445 删除未被使用过的i18n条目 2018-06-17 16:40:22 +08:00
yushijinhun 8b698d3a4c 更新设置页面中依赖信息 2018-06-17 15:33:35 +08:00
yushijinhun c4608a949b 处理未捕获异常时立即进行log,防止异常被忽略
THROWABLE_SET改名为CAUGHT_EXCEPTIONS,使用并发安全集合
2018-06-17 15:04:31 +08:00
yushijinhun bba1c559a3 为连接authlib-injector服务器失败添加错误消息 2018-06-17 14:41:26 +08:00
yushijinhun bdee9ea72f 将获取验证服务器信息的代码移入AuthlibInjectorServer 2018-06-17 14:26:38 +08:00
yushijinhun 520923ba08 跳过更新时记录日志 2018-06-17 13:44:04 +08:00
yushijinhun 936a74597f 若版本号包含SNAPSHOT则也不更新
通过Gradle构建出来的JAR版本号为3.1.SNAPSHOT这样的
2018-06-17 13:40:09 +08:00
yushijinhun 97132745cc 增加hmcl.version.override选项用于覆写HMCL版本 2018-06-17 13:30:05 +08:00
yushijinhun 2bb30f65f3 在AppDataUpgrader中使用ReflectiveOperationException替代具体反射异常 2018-06-17 13:11:56 +08:00
huangyuhui ec04d55793 Fix transition 2018-06-17 12:05:39 +08:00
huangyuhui 88671db566 Use spinner instead of indeterminate progress bar 2018-06-17 12:05:25 +08:00
huanghongxun 538f501bc0 Merge pull request #351 from yushijinhun/refactor-yggdrasil
authlib-injector 部分重构
2018-06-17 10:34:47 +08:00
huangyuhui 3b75d41a2f 修改更新服务器连接 2018-06-17 10:34:13 +08:00
yushijinhun 110149d0e6 AuthlibInjectorAccountFactory的addtionalData改为AuthlibInjectorServer类型 2018-06-16 23:14:04 +08:00
yushijinhun 89b68781fa 修复添加验证服务器页面的错误信息不会被清除 2018-06-16 23:09:16 +08:00
yushijinhun 4cdc54b9c1 修复添加账号时未选中验证服务器会NPE 2018-06-16 23:06:21 +08:00
yushijinhun 7673595a43 AddAccountPane中服务器列表数据视图分离 2018-06-16 22:58:39 +08:00
yushijinhun 76a259c107 AuthlibInjectorAccount中存储AuthlibInjectorServer对象而不是url 2018-06-16 21:26:36 +08:00
yushijinhun 5f40d01fb4 在AuthlibInjectorAccountFactory中使用AuthlibInjectorServer而不是url 2018-06-16 21:26:36 +08:00
yushijinhun df29d47c0a workaround: 将Settings中的Config字段改为静态 2018-06-16 21:26:36 +08:00
yushijinhun 10d4084dde AuthlibInjectorServerInfo重命名为AuthlibInjectorServer
更正其它措辞:ip->url
2018-06-16 21:26:34 +08:00
yushijinhun ba8cdb1b42 移除AuthlibInjectorServersPage的spinner 2018-06-16 21:25:25 +08:00
yushijinhun 22732fee6a 存储AuthlibInjectorServerInfo对象而不是url字符串 2018-06-16 21:24:30 +08:00
yushijinhun bfbc4613b9 在添加authlib-injector服务器的过程中,传递AuthlibInjectorServerInfo而不是url 2018-06-16 20:33:48 +08:00
yushijinhun 92e5eecaf2 将Settings中DEFAULT_PROFILE, HOME_PROFILE字段移至Profiles 2018-06-16 20:30:14 +08:00
yushijinhun 593159f4a6 将DEFAULT_ICON移至Constants, 并使用懒加载
当类初始化时, JavaFX可能还未启动, 这将使图像加载失败
2018-06-16 20:10:10 +08:00
yushijinhun 53cad0e56a 在因无法读取而移除账户时记录日志 2018-06-16 20:10:10 +08:00
huangyuhui d61006dd67 Fix uncaught IOException 2018-06-13 23:40:16 +08:00
huangyuhui eb565285b2 Fix curse mod downloading 2018-06-10 20:05:36 +08:00
153 changed files with 4029 additions and 2283 deletions

No files matched your search

Binary file not shown.
+18 -64
View File
@@ -1,11 +1,14 @@
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
import java.util.zip.GZIPOutputStream
plugins {
id "edu.sc.seis.launch4j" version "2.4.4"
}
if (!hasProperty('mainClass')) {
ext.mainClass = 'org.jackhuang.hmcl.Main'
}
@@ -54,11 +57,6 @@ jar {
}
doLast {
//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 ->
messageDigest.update(buf, 0, bytesRead)
@@ -76,30 +74,6 @@ processResources {
}
}
task makeExecutable(dependsOn: jar) doLast {
ext {
jar.classifier = ''
makeExecutableinjar = jar.archivePath
jar.classifier = ''
makeExecutableoutjar = jar.archivePath
jar.classifier = ''
}
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe")
def fos = new FileOutputStream(loc)
def is = new FileInputStream(new File(project.buildDir, '../HMCLauncher.exe'))
int read
def bytes = new byte[8192]
while((read = is.read(bytes)) != -1)
fos.write(bytes, 0, read)
is.close()
is = new FileInputStream(makeExecutableoutjar)
while((read = is.read(bytes)) != -1)
fos.write(bytes, 0, read)
is.close()
fos.close()
}
task makePackGZ(dependsOn: jar) doLast {
ext {
jar.classifier = ''
@@ -123,41 +97,21 @@ task makePackGZ(dependsOn: jar) doLast {
fileEx.append sha1Hex
}
task proguard(type: ProGuardTask, dependsOn: jar) {
ext {
def re = jar.classifier
injar = jar.archivePath
jar.classifier = 'optimized'
outjar = jar.archivePath
jar.classifier = re
}
launch4j {
mainClassName = mainClass
icon = "${projectDir}/icon.ico"
injars(/*["filter": "!META-INF/**,**"], */injar)
String javaHome = System.getProperty("java.home")
if (!javaHome.endsWith("jre"))
javaHome += File.separator + "jre"
libraryjars javaHome + "/lib/rt.jar"
libraryjars javaHome + "/lib/jce.jar"
libraryjars javaHome + "/lib/jsse.jar"
libraryjars javaHome + "/lib/ext/jfxrt.jar"
outjars outjar
jvmOptions = ["-XX:MinHeapFreeRatio=5", "-XX:MaxHeapFreeRatio=15"]
configuration 'proguard.pro'
version = mavenVersion
productName = "HMCL"
internalName = "HMCL-${mavenVersion}"
outfile = "HMCL-${mavenVersion}.exe"
outputDir = "libs"
copyright = "Copyright (C) 2018 huangyuhui <[email protected]>"
}
task combineMETA_INF(type: Jar) {
ext {
def re = jar.classifier
injar = jar.archivePath
jar.classifier = 'optimized'
outjar = jar.archivePath
jar.classifier = re
}
from zipTree(outjar)
from zipTree(injar).matching {
include "META-INF/"
}
}
build.dependsOn makeExecutable
build.dependsOn makePackGZ
build.dependsOn makePackGZ
build.dependsOn createExe
-15
View File
@@ -1,15 +0,0 @@
-target 1.8
-dontoptimize
-dontobfuscate
# JFoenix
-keep class com.jfoenix.** {
<fields>;
<methods>;
}
# HMCL
-keep class org.jackhuang.** {
<fields>;
<methods>;
}
@@ -21,7 +21,7 @@ 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;
@@ -37,9 +37,7 @@ 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 {
@@ -82,6 +80,10 @@ public final class Launcher extends Application {
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.LOG.info("*** " + TITLE + " ***");
Logging.LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
Logging.LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
Logging.LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
Logging.LOG.info("Java Home: " + System.getProperty("java.home"));
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -135,32 +137,18 @@ public final class Launcher extends Application {
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 VERSION = System.getProperty("hmcl.version.override", "@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 UPDATE_SERVER = "https://www.huangyuhui.net";
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -17,14 +17,27 @@
*/
package org.jackhuang.hmcl;
import javax.swing.*;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.JOptionPane;
public final class Main {
public static void main(String[] args) {
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
Launcher.main(args);
}
@@ -41,12 +54,35 @@ public final class Main {
try {
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.");
showErrorAndExit(i18n("fatal.missing_javafx"));
}
}
private static void checkDSTRootCAX3() {
TrustManagerFactory tmf;
try {
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
} catch (NoSuchAlgorithmException | KeyStoreException e) {
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
// don't know what to do here
return;
}
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
return;
}
}
}
}
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
@@ -54,4 +90,13 @@ public final class Main {
System.exit(1);
}
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
private static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
}
}
@@ -33,7 +33,6 @@ import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.net.Proxy;
import java.util.*;
public final class AccountHelper {
@@ -43,31 +42,19 @@ public final class AccountHelper {
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
loadSkins(Proxy.NO_PROXY);
}
public static void loadSkins(Proxy proxy) {
for (Account account : Settings.INSTANCE.getAccounts()) {
if (account instanceof YggdrasilAccount) {
new SkinLoadTask((YggdrasilAccount) account, proxy, false).start();
new SkinLoadTask((YggdrasilAccount) account, false).start();
}
}
}
public static Task loadSkinAsync(YggdrasilAccount account) {
return loadSkinAsync(account, Settings.INSTANCE.getProxy());
}
public static Task loadSkinAsync(YggdrasilAccount account, Proxy proxy) {
return new SkinLoadTask(account, proxy, false);
return new SkinLoadTask(account, false);
}
public static Task refreshSkinAsync(YggdrasilAccount account) {
return refreshSkinAsync(account, Settings.INSTANCE.getProxy());
}
public static Task refreshSkinAsync(YggdrasilAccount account, Proxy proxy) {
return new SkinLoadTask(account, proxy, true);
return new SkinLoadTask(account, true);
}
private static File getSkinFile(UUID uuid) {
@@ -94,9 +81,9 @@ public final class AccountHelper {
return getDefaultSkin(uuid, scaleRatio);
}
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio, Proxy proxy) throws Exception {
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio) throws Exception {
File file = getSkinFile(profile.getId());
downloadSkin(account, profile, true, proxy);
downloadSkin(account, profile, true);
if (!file.exists())
return getDefaultSkin(profile.getId(), scaleRatio);
@@ -111,17 +98,15 @@ public final class AccountHelper {
private static class SkinLoadTask extends Task {
private final YggdrasilAccount account;
private final Proxy proxy;
private final boolean refresh;
private final List<Task> dependencies = new LinkedList<>();
public SkinLoadTask(YggdrasilAccount account, Proxy proxy) {
this(account, proxy, false);
public SkinLoadTask(YggdrasilAccount account) {
this(account, false);
}
public SkinLoadTask(YggdrasilAccount account, Proxy proxy, boolean refresh) {
public SkinLoadTask(YggdrasilAccount account, boolean refresh) {
this.account = account;
this.proxy = proxy;
this.refresh = refresh;
}
@@ -140,11 +125,11 @@ public final class AccountHelper {
if (!account.isLoggedIn() && (account.getCharacter() == null || refresh))
DialogController.logIn(account);
downloadSkin(account, refresh, proxy);
downloadSkin(account, refresh);
}
}
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh, Proxy proxy) throws Exception {
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh) throws Exception {
account.clearCache();
Optional<Texture> texture = account.getSkin(profile);
@@ -153,10 +138,10 @@ public final class AccountHelper {
File file = getSkinFile(profile.getId());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
}
private static void downloadSkin(YggdrasilAccount account, boolean refresh, Proxy proxy) throws Exception {
private static void downloadSkin(YggdrasilAccount account, boolean refresh) throws Exception {
account.clearCache();
if (account.getCharacter() == null) return;
@@ -166,7 +151,7 @@ public final class AccountHelper {
File file = getSkinFile(account.getUUID());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
}
public static Image scale(String url, double scaleRatio) {
@@ -22,8 +22,6 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.setting.Profile;
import java.net.Proxy;
/**
* @author huangyuhui
*/
@@ -31,11 +29,7 @@ public class HMCLDependencyManager extends DefaultDependencyManager {
private final Profile profile;
public HMCLDependencyManager(Profile profile, DownloadProvider downloadProvider) {
this(profile, downloadProvider, Proxy.NO_PROXY);
}
public HMCLDependencyManager(Profile profile, DownloadProvider downloadProvider, Proxy proxy) {
super(profile.getRepository(), downloadProvider, proxy);
super(profile.getRepository(), downloadProvider);
this.profile = profile;
}
@@ -18,8 +18,8 @@
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -31,6 +31,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
/**
* @author huangyuhui
*/
@@ -57,7 +59,7 @@ public class HMCLGameDownloadTask extends Task {
public void execute() {
File jar = profile.getRepository().getVersionJar(version);
File cache = new File(Settings.INSTANCE.getCommonPath(), "jars/" + gameVersion + ".jar");
File cache = new File(CONFIG.getCommonDirectory(), "jars/" + gameVersion + ".jar");
if (cache.exists())
try {
FileUtils.copyFile(cache, jar);
@@ -69,8 +71,7 @@ public class HMCLGameDownloadTask extends Task {
dependencies.add(new FileDownloadTask(
NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
cache,
profile.getDependency().getProxy(),
version.getDownloadInfo().getSha1()
new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1())
).then(Task.of(v -> FileUtils.copyFile(cache, jar))));
}
@@ -21,8 +21,10 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.CommandBuilder;
import java.util.List;
import java.util.Map;
/**
* @author huangyuhui
@@ -42,10 +44,10 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
@Override
protected void appendJvmArgs(List<String> result) {
super.appendJvmArgs(result);
result.add("-Dminecraft.launcher.version=" + Launcher.VERSION);
result.add("-Dminecraft.launcher.brand=" + Launcher.NAME);
protected Map<String, String> getConfigurations() {
Map<String, String> res = super.getConfigurations();
res.put("${launcher_name}", Launcher.NAME);
res.put("${launcher_version}", Launcher.VERSION);
return res;
}
}
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -34,6 +33,8 @@ import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
@@ -60,7 +61,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (useSelf(version, assetId))
return super.getAssetDirectory(version, assetId);
else
return new File(Settings.INSTANCE.getCommonPath(), "assets");
return new File(CONFIG.getCommonDirectory(), "assets");
}
@Override
@@ -85,7 +86,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (self.exists() || vs.isNoCommon())
return self;
else
return new File(Settings.INSTANCE.getCommonPath(), "libraries/" + lib.getPath());
return new File(CONFIG.getCommonDirectory(), "libraries/" + lib.getPath());
}
@@ -22,11 +22,13 @@ 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.auth.CredentialExpiredException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
@@ -35,11 +37,13 @@ import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
@@ -47,6 +51,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LauncherHelper {
public static final LauncherHelper INSTANCE = new LauncherHelper();
@@ -76,7 +81,7 @@ public final class LauncherHelper {
}
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
GameRepository repository = profile.getRepository();
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
VersionSetting setting = profile.getVersionSetting(selectedVersion);
@@ -90,18 +95,26 @@ public final class LauncherHelper {
return dependencyManager.checkGameCompletionAsync(version);
})
.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(Launcher.i18n("account.methods"), variables -> {
.then(var -> {
try {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
} catch (AuthenticationException e) {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(i18n("account.methods"), variables -> {
try {
variables.set("account", account.logIn());
} catch (CredentialExpiredException e) {
variables.set("account", DialogController.logIn(account));
} catch (AuthenticationException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
}))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
@@ -119,12 +132,12 @@ public final class LauncherHelper {
.then(variables -> {
DefaultLauncher launcher = variables.get("launcher");
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(Launcher.i18n("version.launch"));
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(Launcher.i18n("version.launch_script"));
}).setName(i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
@@ -136,12 +149,12 @@ public final class LauncherHelper {
else
launchingStepsPane.setCancel(it -> {
process.stop();
Controllers.closeDialog(it);
it.fireEvent(new DialogCloseEvent());
});
} else
Platform.runLater(() -> {
Controllers.closeDialog(launchingStepsPane);
Controllers.dialog(Launcher.i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
@@ -160,13 +173,31 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (!success) {
if (!success && !Controllers.isStopped()) {
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);
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Exception ex = executor.getLastException();
if (ex != null) {
String message;
if (ex instanceof CurseCompletionException) {
message = i18n("modpack.type.curse.error");
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
} else {
message = StringUtils.getStackTrace(ex);
}
Controllers.dialog(message,
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
}
}
});
}
launchingStepsPane.setExecutor(null);
@@ -179,10 +210,12 @@ public final class LauncherHelper {
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
boolean flag = false;
// Without onAccept called, the launching operation will be terminated.
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(Launcher.i18n("launch.wrong_javadir"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
@@ -190,27 +223,38 @@ public final class LauncherHelper {
}
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
Controllers.dialog(Launcher.i18n("launch.advice.newer_java"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
}
flag = true;
}
// LaunchWrapper will crash because of assuming the system class loader is an instance of URLClassLoader.
// cpw has claimed that he will make MinecraftForge of 1.13 and later versions able to run on Java 9.
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);
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
org.jackhuang.hmcl.util.Platform.IS_64_BIT) {
Controllers.dialog(Launcher.i18n("launch.advice.different_platform"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
Controllers.dialog(Launcher.i18n("launch.advice.too_large_memory_for_32bit"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
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);
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
@@ -224,8 +268,9 @@ public final class LauncherHelper {
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE)
Controllers.closeDialog(launchingStepsPane);
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
}
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
@@ -261,15 +306,7 @@ public final class LauncherHelper {
@Override
public void execute() throws Exception {
try {
setResult(supplier.get());
} catch (PermissionException e) {
throw new I18nException(Launcher.i18n("launch.failed.executable_permission"), e);
} catch (ProcessCreationException e) {
throw new I18nException(Launcher.i18n("launch.failed.creating_process") + e.getLocalizedMessage(), e);
} catch (NotDecompressingNativesException e) {
throw new I18nException(Launcher.i18n("launch.failed.decompressing_natives") + e.getLocalizedMessage(), e);
}
setResult(supplier.get());
}
@Override
@@ -388,16 +425,16 @@ public final class LauncherHelper {
switch (exitType) {
case JVM_ERROR:
logWindow.setTitle(Launcher.i18n("launch.failed.cannot_create_jvm"));
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
logWindow.setTitle(Launcher.i18n("launch.failed.exited_abnormally"));
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
break;
}
logWindow.show();
logWindow.onDone.register(() -> {
logWindow.logLine("Commands: " + StringUtils.makeCommand(process.getCommands()), Log4jLevel.INFO);
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
});
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Launcher;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public enum LoadingState {
DEPENDENCIES("launch.state.dependencies"),
@@ -33,6 +33,6 @@ public enum LoadingState {
}
public String getLocalizedMessage() {
return Launcher.i18n(key);
return i18n(key);
}
}
@@ -59,12 +59,12 @@ public final class ModpackHelper {
throw new UnsupportedModpackException(file.toString());
}
public static <T> ModpackConfiguration<T> readModpackConfiguration(File file) throws IOException {
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
else
try {
return Constants.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<T>>() {
return Constants.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
@@ -22,27 +22,20 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
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.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
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.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.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
@@ -58,11 +51,11 @@ public final class Accounts {
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))
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer))
);
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;
@@ -78,33 +71,23 @@ public final class Accounts {
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();
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return CONFIG.getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
// this usually happens when migrating data from an older version
AuthlibInjectorServer server;
try {
server = AuthlibInjectorServer.fetchServerInfo(url);
LOG.info("Migrated authlib injector server " + server);
} catch (IOException e) {
server = new AuthlibInjectorServer(url, url);
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
}
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()));
CONFIG.getAuthlibInjectorServers().add(server);
return server;
});
}
}
@@ -17,19 +17,38 @@
*/
package org.jackhuang.hmcl.setting;
import java.util.HashSet;
import java.io.File;
import java.lang.reflect.Modifier;
import java.net.Proxy;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
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.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
@@ -37,73 +56,377 @@ import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
public final class Config implements Cloneable {
public final class Config implements Cloneable, Observable {
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(Theme.class, new Theme.TypeAdapter())
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
.create();
public static Config fromJson(String json) throws JsonParseException {
Config instance = CONFIG_GSON.fromJson(json, Config.class);
// Gson will replace the property fields (even they are final!)
// So we have to add the listeners again after deserialization
instance.addListenerToProperties();
return instance;
}
@SerializedName("last")
public final StringProperty selectedProfile = new SimpleStringProperty("");
private StringProperty selectedProfile = new SimpleStringProperty("");
@SerializedName("backgroundType")
public final IntegerProperty backgroundImageType = new SimpleIntegerProperty(0);
private ObjectProperty<EnumBackgroundImage> backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT);
@SerializedName("bgpath")
public final StringProperty backgroundImage = new SimpleStringProperty();
private StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("commonpath")
public final StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
private StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
@SerializedName("hasProxy")
public final BooleanProperty hasProxy = new SimpleBooleanProperty();
private BooleanProperty hasProxy = new SimpleBooleanProperty();
@SerializedName("hasProxyAuth")
public final BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
private BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
@SerializedName("proxyType")
public final IntegerProperty proxyType = new SimpleIntegerProperty();
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.DIRECT);
@SerializedName("proxyHost")
public final StringProperty proxyHost = new SimpleStringProperty();
private StringProperty proxyHost = new SimpleStringProperty();
@SerializedName("proxyPort")
public final StringProperty proxyPort = new SimpleStringProperty();
private StringProperty proxyPort = new SimpleStringProperty();
@SerializedName("proxyUserName")
public final StringProperty proxyUser = new SimpleStringProperty();
private StringProperty proxyUser = new SimpleStringProperty();
@SerializedName("proxyPassword")
public final StringProperty proxyPass = new SimpleStringProperty();
private StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("theme")
public final StringProperty theme = new SimpleStringProperty();
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
@SerializedName("localization")
public final StringProperty localization = new SimpleStringProperty();
private StringProperty localization = new SimpleStringProperty();
@SerializedName("downloadtype")
public final IntegerProperty downloadType = new SimpleIntegerProperty();
private IntegerProperty downloadType = new SimpleIntegerProperty(1);
@SerializedName("configurations")
public final ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
public final ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
private ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
public final StringProperty selectedAccount = new SimpleStringProperty("");
private StringProperty selectedAccount = new SimpleStringProperty("");
@SerializedName("fontFamily")
public final StringProperty fontFamily = new SimpleStringProperty("Consolas");
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
@SerializedName("fontSize")
public final DoubleProperty fontSize = new SimpleDoubleProperty(12);
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@SerializedName("logLines")
public final IntegerProperty logLines = new SimpleIntegerProperty(100);
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("authlibInjectorServerURLs")
public final ObservableSet<String> authlibInjectorServerURLs = FXCollections.observableSet(new HashSet<>());
@SerializedName("firstLaunch")
private BooleanProperty firstLaunch = new SimpleBooleanProperty(true);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
addListenerToProperties();
}
private void addListenerToProperties() {
Stream.of(getClass().getDeclaredFields())
.filter(it -> {
int modifiers = it.getModifiers();
return !Modifier.isTransient(modifiers) && !Modifier.isStatic(modifiers);
})
.filter(it -> Observable.class.isAssignableFrom(it.getType()))
.map(it -> {
try {
return (Observable) it.get(this);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to get my own properties");
}
})
.forEach(helper::receiveUpdatesFrom);
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
public String toJson() {
return CONFIG_GSON.toJson(this);
}
@Override
public Config clone() {
return Settings.GSON.fromJson(Settings.GSON.toJson(this), Config.class);
return fromJson(this.toJson());
}
// Getters & Setters & Properties
public String getSelectedProfile() {
return selectedProfile.get();
}
public void setSelectedProfile(String selectedProfile) {
this.selectedProfile.set(selectedProfile);
}
public StringProperty selectedProfileProperty() {
return selectedProfile;
}
public EnumBackgroundImage getBackgroundImageType() {
return backgroundImageType.get();
}
public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) {
this.backgroundImageType.set(backgroundImageType);
}
public ObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
return backgroundImageType;
}
public String getBackgroundImage() {
return backgroundImage.get();
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage.set(backgroundImage);
}
public StringProperty backgroundImageProperty() {
return backgroundImage;
}
public String getCommonDirectory() {
return commonDirectory.get();
}
public void setCommonDirectory(String commonDirectory) {
this.commonDirectory.set(commonDirectory);
}
public StringProperty commonDirectoryProperty() {
return commonDirectory;
}
public boolean hasProxy() {
return hasProxy.get();
}
public void setHasProxy(boolean hasProxy) {
this.hasProxy.set(hasProxy);
}
public BooleanProperty hasProxyProperty() {
return hasProxy;
}
public boolean hasProxyAuth() {
return hasProxyAuth.get();
}
public void setHasProxyAuth(boolean hasProxyAuth) {
this.hasProxyAuth.set(hasProxyAuth);
}
public BooleanProperty hasProxyAuthProperty() {
return hasProxyAuth;
}
public Proxy.Type getProxyType() {
return proxyType.get();
}
public void setProxyType(Proxy.Type proxyType) {
this.proxyType.set(proxyType);
}
public ObjectProperty<Proxy.Type> proxyTypeProperty() {
return proxyType;
}
public String getProxyHost() {
return proxyHost.get();
}
public void setProxyHost(String proxyHost) {
this.proxyHost.set(proxyHost);
}
public StringProperty proxyHostProperty() {
return proxyHost;
}
public String getProxyPort() {
return proxyPort.get();
}
public void setProxyPort(String proxyPort) {
this.proxyPort.set(proxyPort);
}
public StringProperty proxyPortProperty() {
return proxyPort;
}
public String getProxyUser() {
return proxyUser.get();
}
public void setProxyUser(String proxyUser) {
this.proxyUser.set(proxyUser);
}
public StringProperty proxyUserProperty() {
return proxyUser;
}
public String getProxyPass() {
return proxyPass.get();
}
public void setProxyPass(String proxyPass) {
this.proxyPass.set(proxyPass);
}
public StringProperty proxyPassProperty() {
return proxyPass;
}
public Theme getTheme() {
return theme.get();
}
public void setTheme(Theme theme) {
this.theme.set(theme);
}
public ObjectProperty<Theme> themeProperty() {
return theme;
}
public String getLocalization() {
return localization.get();
}
public void setLocalization(String localization) {
this.localization.set(localization);
}
public StringProperty localizationProperty() {
return localization;
}
public int getDownloadType() {
return downloadType.get();
}
public void setDownloadType(int downloadType) {
this.downloadType.set(downloadType);
}
public IntegerProperty downloadTypeProperty() {
return downloadType;
}
public ObservableMap<String, Profile> getConfigurations() {
return configurations;
}
public ObservableList<Map<Object, Object>> getAccounts() {
return accounts;
}
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public String getFontFamily() {
return fontFamily.get();
}
public void setFontFamily(String fontFamily) {
this.fontFamily.set(fontFamily);
}
public StringProperty fontFamilyProperty() {
return fontFamily;
}
public double getFontSize() {
return fontSize.get();
}
public void setFontSize(double fontSize) {
this.fontSize.set(fontSize);
}
public DoubleProperty fontSizeProperty() {
return fontSize;
}
public int getLogLines() {
return logLines.get();
}
public void setLogLines(int logLines) {
this.logLines.set(logLines);
}
public IntegerProperty logLinesProperty() {
return logLines;
}
public boolean isFirstLaunch() {
return firstLaunch.get();
}
public void setFirstLaunch(boolean firstLaunch) {
this.firstLaunch.set(firstLaunch);
}
public BooleanProperty firstLaunchProperty() {
return firstLaunch;
}
public ObservableList<AuthlibInjectorServer> getAuthlibInjectorServers() {
return authlibInjectorServers;
}
}
@@ -0,0 +1,65 @@
/*
* 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.setting;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.Level;
import com.google.gson.JsonParseException;
public final class ConfigHolder {
private ConfigHolder() {}
public static final String CONFIG_FILENAME = "hmcl.json";
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
public static final Config CONFIG = initSettings();
private static Config initSettings() {
Config config = new Config();
if (Files.exists(CONFIG_PATH)) {
try {
Config deserialized = Config.fromJson(new String(Files.readAllBytes(CONFIG_PATH), UTF_8));
if (deserialized == null) {
LOG.finer("Settings file is empty, use the default settings.");
} else {
config = deserialized;
}
LOG.finest("Initialized settings.");
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Something happened wrongly when load settings.", e);
}
}
return config;
}
static void saveConfig(Config config) {
try {
Files.write(CONFIG_PATH, config.toJson().getBytes(UTF_8));
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Failed to save config", ex);
}
}
}
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.game.HMCLDependencyManager;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
@@ -83,8 +86,18 @@ public final class Profile {
nameProperty.set(name);
}
public Profile() {
this("Default");
private BooleanProperty useRelativePathProperty = new SimpleBooleanProperty(this, "useRelativePath", false);
public BooleanProperty useRelativePathProperty() {
return useRelativePathProperty();
}
public boolean isUseRelativePath() {
return useRelativePathProperty.get();
}
public void setUseRelativePath(boolean useRelativePath) {
useRelativePathProperty.set(useRelativePath);
}
public Profile(String name) {
@@ -109,13 +122,14 @@ public final class Profile {
}
public HMCLDependencyManager getDependency() {
return new HMCLDependencyManager(this, Settings.INSTANCE.getDownloadProvider(), Settings.INSTANCE.getProxy());
return new HMCLDependencyManager(this, Settings.INSTANCE.getDownloadProvider());
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
getGlobal().setGlobal(true); // always keep global.isGlobal = true
getGlobal().setUsesGlobal(true);
return getGlobal();
} else
return vs;
@@ -152,6 +166,7 @@ public final class Profile {
return new ToStringBuilder(this)
.append("gameDir", getGameDir())
.append("name", getName())
.append("useRelativePath", isUseRelativePath())
.toString();
}
@@ -159,6 +174,7 @@ public final class Profile {
nameProperty.addListener(listener);
globalProperty.addListener(listener);
gameDirProperty.addListener(listener);
useRelativePathProperty.addListener(listener);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -175,6 +191,7 @@ public final class Profile {
JsonObject jsonObject = new JsonObject();
jsonObject.add("global", context.serialize(src.getGlobal()));
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
return jsonObject;
}
@@ -187,6 +204,8 @@ public final class Profile {
Profile profile = new Profile("Default", new File(gameDir));
profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class));
profile.setUseRelativePath(Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
return profile;
}
@@ -17,18 +17,22 @@
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.Launcher;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Profiles {
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
private Profiles() {
}
public static String getProfileDisplayName(Profile profile) {
switch (profile.getName()) {
case Settings.DEFAULT_PROFILE:
return Launcher.i18n("profile.default");
case Settings.HOME_PROFILE:
return Launcher.i18n("profile.home");
case Profiles.DEFAULT_PROFILE:
return i18n("profile.default");
case Profiles.HOME_PROFILE:
return i18n("profile.home");
default:
return profile.getName();
}
@@ -1,36 +0,0 @@
/*
* 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.setting;
import org.jackhuang.hmcl.util.Lang;
import java.net.Proxy;
import java.util.List;
/**
* @author huangyuhui
*/
public final class Proxies {
private Proxies() {}
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);
}
}
@@ -0,0 +1,117 @@
/*
* 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.setting;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
public final class ProxyManager {
private ProxyManager() {
}
private static final ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
() -> {
String host = CONFIG.getProxyHost();
Integer port = Lang.toIntOrNull(CONFIG.getProxyPort());
if (!CONFIG.hasProxy() || StringUtils.isBlank(host) || port == null || CONFIG.getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
return new Proxy(CONFIG.getProxyType(), new InetSocketAddress(host, port));
}
},
CONFIG.proxyTypeProperty(),
CONFIG.proxyHostProperty(),
CONFIG.proxyPortProperty(),
CONFIG.hasProxyProperty());
public static Proxy getProxy() {
return proxyProperty.get();
}
public static ObservableObjectValue<Proxy> proxyProperty() {
return proxyProperty;
}
static {
initProxy();
}
private static void initProxy() {
proxyProperty.addListener(observable -> updateSystemProxy());
updateSystemProxy();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (CONFIG.hasProxyAuth()) {
String username = CONFIG.getProxyUser();
String password = CONFIG.getProxyPass();
if (username != null && password != null) {
return new PasswordAuthentication(username, password.toCharArray());
}
}
return null;
}
});
}
private static void updateSystemProxy() {
Proxy proxy = proxyProperty.get();
if (proxy.type() == Proxy.Type.DIRECT) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
} else {
InetSocketAddress address = (InetSocketAddress) proxy.address();
String host = address.getHostString();
String port = String.valueOf(address.getPort());
if (proxy.type() == Type.HTTP) {
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", port);
System.setProperty("https.proxyHost", host);
System.setProperty("https.proxyPort", port);
} else if (proxy.type() == Type.SOCKS) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.setProperty("socksProxyHost", host);
System.setProperty("socksProxyPort", port);
}
}
}
}
@@ -17,161 +17,107 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.scene.text.Font;
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.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.io.File;
import java.io.IOException;
import java.net.Authenticator;
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 java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
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)
.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 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 final Config SETTINGS = initSettings();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private Settings() {
loadProxy();
private final boolean firstLaunch;
for (Iterator<Map<Object, Object>> iterator = SETTINGS.accounts.iterator(); iterator.hasNext();) {
private InvalidationListener accountChangeListener =
source -> CONFIG.getAccounts().setAll(
accounts.values().stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
return storage;
})
.collect(toList()));
private Settings() {
firstLaunch = CONFIG.isFirstLaunch();
CONFIG.setFirstLaunch(false);
ProxyManager.getProxy(); // init ProxyManager
for (Iterator<Map<Object, Object>> iterator = CONFIG.getAccounts().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.
LOG.warning("Unrecognized account type, removing: " + settings);
iterator.remove();
continue;
}
Account account;
try {
account = factory.fromStorage(settings, getProxy());
account = factory.fromStorage(settings);
} catch (Exception e) {
// storage is malformed, delete.
LOG.log(Level.WARNING, "Malformed account storage, removing: " + settings, e);
iterator.remove();
continue;
}
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
}
checkAuthlibInjectorAccounts();
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(this::removeDanglingAuthlibInjectorAccounts));
this.selectedAccount.set(accounts.get(CONFIG.getSelectedAccount()));
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());
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
profileEntry.getValue().setName(profileEntry.getKey());
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
profileEntry.getValue().addPropertyChangedListener(e -> save());
}
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
CONFIG.addListener(source -> save());
}
private Config initSettings() {
Config c = new Config();
if (SETTINGS_FILE.exists())
try {
String str = FileUtils.readText(SETTINGS_FILE);
if (StringUtils.isBlank(str))
Logging.LOG.finer("Settings file is empty, use the default settings.");
else {
Config d = GSON.fromJson(str, Config.class);
if (d != null)
c = d;
}
Logging.LOG.finest("Initialized settings.");
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Something happened wrongly when load settings.", e);
}
return c;
private void save() {
ConfigHolder.saveConfig(CONFIG);
}
public void save() {
try {
SETTINGS.accounts.clear();
for (Account account : accounts.values()) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
SETTINGS.accounts.add(storage);
}
FileUtils.writeText(SETTINGS_FILE, GSON.toJson(SETTINGS));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to save config", ex);
}
public boolean isFirstLaunch() {
return firstLaunch;
}
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.commonDirectory.get()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.commonDirectory.set(get());
save();
}
};
public String getCommonPath() {
return commonPath.get();
}
public StringProperty commonPathProperty() {
return commonPath;
}
public void setCommonPath(String commonPath) {
this.commonPath.set(commonPath);
}
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.localization.get());
private Locales.SupportedLocale locale = Locales.getLocaleByName(CONFIG.getLocalization());
public Locales.SupportedLocale getLocale() {
return locale;
@@ -179,156 +125,41 @@ public class Settings {
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
SETTINGS.localization.set(Locales.getNameByLocale(locale));
save();
}
private Proxy proxy = Proxy.NO_PROXY;
public Proxy getProxy() {
return proxy;
}
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.proxyType.get());
public Proxy.Type getProxyType() {
return proxyType;
}
public void setProxyType(Proxy.Type proxyType) {
this.proxyType = proxyType;
SETTINGS.proxyType.set(Proxies.PROXIES.indexOf(proxyType));
save();
loadProxy();
}
public String getProxyHost() {
return SETTINGS.proxyHost.get();
}
public void setProxyHost(String proxyHost) {
SETTINGS.proxyHost.set(proxyHost);
save();
}
public String getProxyPort() {
return SETTINGS.proxyPort.get();
}
public void setProxyPort(String proxyPort) {
SETTINGS.proxyPort.set(proxyPort);
save();
}
public String getProxyUser() {
return SETTINGS.proxyUser.get();
}
public void setProxyUser(String proxyUser) {
SETTINGS.proxyUser.set(proxyUser);
save();
}
public String getProxyPass() {
return SETTINGS.proxyPass.get();
}
public void setProxyPass(String proxyPass) {
SETTINGS.proxyPass.set(proxyPass);
save();
}
public boolean hasProxy() {
return SETTINGS.hasProxy.get();
}
public void setHasProxy(boolean hasProxy) {
SETTINGS.hasProxy.set(hasProxy);
save();
}
public boolean hasProxyAuth() {
return SETTINGS.hasProxyAuth.get();
}
public void setHasProxyAuth(boolean hasProxyAuth) {
SETTINGS.hasProxyAuth.set(hasProxyAuth);
save();
}
private void loadProxy() {
String host = getProxyHost();
Integer port = Lang.toIntOrNull(getProxyPort());
if (!hasProxy() || StringUtils.isBlank(host) || port == null || getProxyType() == Proxy.Type.DIRECT)
proxy = Proxy.NO_PROXY;
else {
System.setProperty("http.proxyHost", getProxyHost());
System.setProperty("http.proxyPort", getProxyPort());
proxy = new Proxy(proxyType, new InetSocketAddress(host, port));
String user = getProxyUser();
String pass = getProxyPass();
if (hasProxyAuth() && StringUtils.isNotBlank(user) && StringUtils.isNotBlank(pass)) {
System.setProperty("http.proxyUser", user);
System.setProperty("http.proxyPassword", pass);
Authenticator.setDefault(new Authenticator() {
@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, pass.toCharArray());
}
});
}
}
CONFIG.setLocalization(Locales.getNameByLocale(locale));
}
public Font getFont() {
return Font.font(SETTINGS.fontFamily.get(), SETTINGS.fontSize.get());
return Font.font(CONFIG.getFontFamily(), CONFIG.getFontSize());
}
public void setFont(Font font) {
SETTINGS.fontFamily.set(font.getFamily());
SETTINGS.fontSize.set(font.getSize());
save();
CONFIG.setFontFamily(font.getFamily());
CONFIG.setFontSize(font.getSize());
}
public int getLogLines() {
return Math.max(SETTINGS.logLines.get(), 100);
return Math.max(CONFIG.getLogLines(), 100);
}
public void setLogLines(int logLines) {
SETTINGS.logLines.set(logLines);
save();
CONFIG.setLogLines(logLines);
}
/****************************************
* 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);
}
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
* Don't call this before {@link #migrateAuthlibInjectorServers()} is called, otherwise old data would be lost.
*/
private void removeDanglingAuthlibInjectorAccounts() {
accounts.values().stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(((AuthlibInjectorAccount) it).getServer()))
.collect(toList())
.forEach(this::deleteAccount);
}
/****************************************
@@ -336,22 +167,21 @@ public class Settings {
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(SETTINGS.downloadType.get());
return DownloadProviders.getDownloadProvider(CONFIG.getDownloadType());
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
int index = DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(downloadProvider);
if (index == -1)
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
SETTINGS.downloadType.set(index);
save();
CONFIG.setDownloadType(index);
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.selectedAccount.get())) {
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", null) {
@Override
public Account get() {
Account a = super.get();
@@ -373,8 +203,7 @@ public class Settings {
public void invalidated() {
super.invalidated();
SETTINGS.selectedAccount.set(getValue() == null ? "" : Accounts.getAccountId(getValue()));
save();
CONFIG.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
}
};
@@ -392,6 +221,9 @@ public class Settings {
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
@@ -406,93 +238,25 @@ public class Settings {
}
public void deleteAccount(String name, String character) {
accounts.remove(Accounts.getAccountId(name, character));
Account removed = accounts.remove(Accounts.getAccountId(name, character));
if (removed != null) {
removed.removeListener(accountChangeListener);
accountChangeListener.invalidated(removed);
onAccountLoading();
selectedAccount.get();
onAccountLoading();
selectedAccount.get();
}
}
public void deleteAccount(Account account) {
accounts.remove(Accounts.getAccountId(account));
account.removeListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
selectedAccount.get();
}
/****************************************
* BACKGROUND *
****************************************/
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.backgroundImage.get()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.backgroundImage.set(get());
save();
}
};
public String getBackgroundImage() {
return backgroundImage.get();
}
public ImmediateStringProperty backgroundImageProperty() {
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage.set(backgroundImage);
}
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.backgroundImageType.get())) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.backgroundImageType.set(get().ordinal());
save();
}
};
public EnumBackgroundImage getBackgroundImageType() {
return backgroundImageType.get();
}
public ImmediateObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
return backgroundImageType;
}
public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) {
this.backgroundImageType.set(backgroundImageType);
}
/****************************************
* THEME *
****************************************/
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.theme.set(get().getName().toLowerCase());
save();
}
};
public Theme getTheme() {
return theme.get();
}
public void setTheme(Theme theme) {
this.theme.set(theme);
}
public ImmediateObjectProperty<Theme> themeProperty() {
return theme;
}
/****************************************
* PROFILES *
****************************************/
@@ -500,20 +264,18 @@ public class Settings {
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(SETTINGS.selectedProfile.get())) {
if (!hasProfile(CONFIG.getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
SETTINGS.selectedProfile.set(selectedProfile);
save();
CONFIG.setSelectedProfile(selectedProfile);
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(SETTINGS.selectedProfile.get());
return getProfile(CONFIG.getSelectedProfile());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.selectedProfile.get())) {
SETTINGS.selectedProfile.set(selectedProfile.getName());
save();
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), CONFIG.getSelectedProfile())) {
CONFIG.setSelectedProfile(selectedProfile.getName());
Schedulers.computation().schedule(this::onProfileChanged);
}
}
@@ -530,7 +292,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.configurations;
return CONFIG.getConfigurations();
}
public Collection<Profile> getProfiles() {
@@ -545,8 +307,6 @@ public class Settings {
Schedulers.computation().schedule(this::onProfileLoading);
ver.nameProperty().setChangedListener(this::profileNameChanged);
save();
}
public void deleteProfile(Profile profile) {
@@ -561,8 +321,12 @@ 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, Launcher.MINECRAFT_DIRECTORY));
Profile current = new Profile(Profiles.DEFAULT_PROFILE);
current.setUseRelativePath(true);
getProfileMap().put(Profiles.DEFAULT_PROFILE, current);
Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY);
getProfileMap().put(Profiles.HOME_PROFILE, home);
}
}
@@ -587,8 +351,4 @@ public class Settings {
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
public Config getRawConfig() {
return SETTINGS.clone();
}
}
@@ -24,6 +24,11 @@ import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
@@ -31,13 +36,15 @@ 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};
public static final Color[] SUGGESTED_COLORS = new Color[]{
Color.web("#5C6BC0"), // blue
Color.web("#283593"), // dark blue
Color.web("#43A047"), // green
Color.web("#E67E22"), // orange
Color.web("#9C27B0"), // purple
Color.web("#F44336") // red
};
private final String color;
private final String name;
@@ -68,8 +75,7 @@ public class Theme {
}
public String[] getStylesheets() {
String name = isCustom() ? BLUE.getName() : this.name;
String css = Theme.class.getResource("/assets/css/" + name + ".css").toExternalForm();
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
@@ -77,7 +83,8 @@ public class Theme {
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet", e);
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = Theme.class.getResource("/assets/css/blue.css").toExternalForm();
}
return new String[]{
@@ -98,10 +105,6 @@ public class Theme {
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);
@@ -117,7 +120,7 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> Settings.INSTANCE.getTheme().getForegroundColor(), Settings.INSTANCE.themeProperty());
return Bindings.createObjectBinding(() -> CONFIG.getTheme().getForegroundColor(), CONFIG.themeProperty());
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -127,4 +130,16 @@ public class Theme {
public static ObjectBinding<Color> whiteFillBinding() {
return Bindings.createObjectBinding(() -> Color.WHITE);
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
@Override
public void write(JsonWriter out, Theme value) throws IOException {
out.value(value.getName().toLowerCase());
}
@Override
public Theme read(JsonReader in) throws IOException {
return getTheme(in.nextString()).orElse(Theme.BLUE);
}
}
}
@@ -30,6 +30,8 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
/**
*
* @author huangyuhui
@@ -517,10 +519,10 @@ public final class VersionSetting {
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setProxyHost(Settings.INSTANCE.getProxyHost())
.setProxyPort(Settings.INSTANCE.getProxyPort())
.setProxyUser(Settings.INSTANCE.getProxyUser())
.setProxyPass(Settings.INSTANCE.getProxyPass())
.setProxyHost(CONFIG.getProxyHost())
.setProxyPort(CONFIG.getProxyPort())
.setProxyUser(CONFIG.getProxyUser())
.setProxyPass(CONFIG.getProxyPass())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs())
.create();
@@ -21,13 +21,13 @@ 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.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 org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import java.util.function.Consumer;
@@ -35,7 +35,6 @@ 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;
@@ -43,11 +42,10 @@ public class AccountLoginPane extends StackPane {
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
public AccountLoginPane(Account oldAccount, Consumer<Region> closeConsumer, Consumer<AuthInfo> success, Runnable failed) {
public AccountLoginPane(Account oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;
this.closeConsumer = closeConsumer;
FXUtils.loadFXML(this, "/assets/fxml/account-login.fxml");
@@ -70,9 +68,9 @@ public class AccountLoginPane extends StackPane {
Object account = variable.get("login");
if (account instanceof AuthInfo) {
success.accept(((AuthInfo) account));
closeConsumer.accept(this);
fireEvent(new DialogCloseEvent());
} else if (account instanceof NoSelectedCharacterException) {
closeConsumer.accept(this);
fireEvent(new DialogCloseEvent());
} else if (account instanceof Exception) {
lblCreationWarning.setText(AddAccountPane.accountException((Exception) account));
}
@@ -84,6 +82,6 @@ public class AccountLoginPane extends StackPane {
@FXML
private void onCancel() {
failed.run();
closeConsumer.accept(this);
fireEvent(new DialogCloseEvent());
}
}
@@ -28,19 +28,22 @@ 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.authlibinjector.AuthlibInjectorServer;
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 static org.jackhuang.hmcl.ui.FXUtils.installTooltip;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Optional;
public class AccountPage extends StackPane implements DecoratorPage {
@@ -74,23 +77,20 @@ public class AccountPage extends StackPane implements DecoratorPage {
this.account = account;
this.item = item;
title = new SimpleStringProperty(this, "title", Launcher.i18n("account") + " - " + account.getCharacter());
title = new SimpleStringProperty(this, "title", 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);
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
lblServer.setText(server.getName());
installTooltip(lblServer, server.getUrl());
} 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));
@@ -19,43 +19,48 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
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.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
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.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
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 {
@@ -64,130 +69,115 @@ public class AddAccountPane extends StackPane {
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private JFXComboBox<AuthlibInjectorServer> 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;
@FXML private SpinnerPane acceptPane;
public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
transitionHandler = new TransitionHandler(acceptPane);
acceptPane.getChildren().setAll(btnAccept);
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), CONFIG.getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
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.getItems().setAll(i18n("account.methods.offline"), i18n("account.methods.yggdrasil"), i18n("account.methods.authlib_injector"));
cboType.getSelectionModel().select(0);
// These two lines can eliminate black, don't know why.
cboServers.getItems().setAll(new TwoLineListItem("", ""));
cboServers.getSelectionModel().select(0);
ReadOnlyIntegerProperty loginTypeIdProperty = cboType.getSelectionModel().selectedIndexProperty();
txtPassword.setOnAction(e -> onCreationAccept());
txtUsername.setOnAction(e -> onCreationAccept());
txtUsername.getValidators().add(new Validator(Launcher.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
txtPassword.visibleProperty().bind(loginTypeIdProperty.isNotEqualTo(0));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
txtUsername.textProperty().addListener(it -> validateAcceptButton());
txtPassword.textProperty().addListener(it -> validateAcceptButton());
cboServers.visibleProperty().bind(loginTypeIdProperty.isEqualTo(2));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtUsername.validate() || (loginTypeIdProperty.get() != 0 && !txtPassword.validate()),
txtUsername.textProperty(), txtPassword.textProperty(), loginTypeIdProperty));
}
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());
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
}
}
@FXML
private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
if (btnAccept.isDisabled())
return;
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();
}
Object addtionalData;
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();
int type = cboType.getSelectionModel().getSelectedIndex();
AccountFactory<?> factory;
switch (type) {
case 0:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY);
addtionalData = null;
break;
case 1:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY);
addtionalData = null;
break;
case 2:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY);
Optional<AuthlibInjectorServer> server = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem());
if (server.isPresent()) {
addtionalData = server.get();
} else {
lblCreationWarning.setText(i18n("account.failed.no_selected_server"));
return;
}
break;
default:
throw new Error();
}
acceptPane.showSpinner();
lblCreationWarning.setText("");
setDisable(true);
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, addtionalData))
.finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblCreationWarning.setText(accountException(exception));
}
setDisable(false);
acceptPane.hideSpinner();
}).start();
}
@FXML
private void onCreationCancel() {
finalization.accept(this);
fireEvent(new DialogCloseEvent());
}
@FXML
private void onManageInjecterServers() {
finalization.accept(this);
fireEvent(new DialogCloseEvent());
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();
@@ -198,11 +188,11 @@ public class AddAccountPane extends StackPane {
{
setStyle("-fx-padding: 8px;");
cancel.setText(Launcher.i18n("button.cancel"));
cancel.setText(i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(Launcher.i18n("account.choose"));
listBox.startCategory(i18n("account.choose"));
setCenter(listBox);
@@ -221,16 +211,16 @@ public class AddAccountPane extends StackPane {
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = FXUtils.DEFAULT_ICON;
image = null;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == FXUtils.DEFAULT_ICON)
portraitView.setImage(FXUtils.DEFAULT_ICON);
else {
if (image == null) {
portraitView.setImage(Constants.DEFAULT_ICON.get());
} else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
@@ -244,7 +234,7 @@ public class AddAccountPane extends StackPane {
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> showSelector(this));
JFXUtilities.runInFX(() -> Controllers.dialog(this));
try {
latch.await();
@@ -252,35 +242,41 @@ public class AddAccountPane extends StackPane {
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
JFXUtilities.runInFX(AddAccountPane.this::closeSelector);
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account);
} finally {
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
}
}
}
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");
if (exception instanceof NoCharacterException) {
return i18n("account.failed.no_character");
} 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");
return i18n("account.failed.connect_authentication_server");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials"))
return i18n("account.failed.invalid_credentials");
else if (remoteMessage.contains("Invalid token"))
return i18n("account.failed.invalid_token");
else if (remoteMessage.contains("Invalid username or password"))
return i18n("account.failed.invalid_password");
}
return exception.getMessage();
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
return exception.getClass().getName() + ": " + 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);
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return i18n("account.methods.yggdrasil");
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
}
@@ -0,0 +1,142 @@
/*
* 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 static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.IOException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
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.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.NetworkUtils;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
public class AddAuthlibInjectorServerPane extends StackPane {
@FXML private StackPane addServerContainer;
@FXML private Label lblServerUrl;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@FXML private Label lblServerWarning;
@FXML private JFXTextField txtServerUrl;
@FXML private JFXDialogLayout addServerPane;
@FXML private JFXDialogLayout confirmServerPane;
@FXML private SpinnerPane nextPane;
@FXML private JFXButton btnAddNext;
private TransitionHandler transitionHandler;
private AuthlibInjectorServer serverBeingAdded;
public AddAuthlibInjectorServerPane(String url) {
this();
txtServerUrl.setText(url);
onAddNext();
}
public AddAuthlibInjectorServerPane() {
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
transitionHandler = new TransitionHandler(addServerContainer);
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
btnAddNext.disableProperty().bind(
Bindings.createBooleanBinding(txtServerUrl::validate, txtServerUrl.textProperty()).not());
nextPane.hideSpinner();
}
private String fixInputUrl(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
}
private String resolveFetchExceptionMessage(Throwable exception) {
if (exception instanceof IOException) {
return i18n("account.failed.connect_injector_server");
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
@FXML
private void onAddCancel() {
fireEvent(new DialogCloseEvent());
}
@FXML
private void onAddNext() {
if (btnAddNext.isDisabled())
return;
lblCreationWarning.setText("");
String url = fixInputUrl(txtServerUrl.getText());
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.fetchServerInfo(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();
if (isDependentsSucceeded) {
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
lblCreationWarning.setText(resolveFetchExceptionMessage(variables.<Exception>get("lastException")));
}
}).start();
}
@FXML
private void onAddPrev() {
transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
}
@FXML
private void onAddFinish() {
if (!CONFIG.getAuthlibInjectorServers().contains(serverBeingAdded)) {
CONFIG.getAuthlibInjectorServers().add(serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
}
@@ -23,19 +23,19 @@ 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.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import java.util.function.Consumer;
public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServerInfo info;
private final AuthlibInjectorServer server;
private final Label lblServerName = new Label();
private final Label lblServerIp = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServerInfo info, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.info = info;
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.server = server;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;");
@@ -54,11 +54,11 @@ public final class AuthlibInjectorServerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(info.getServerName());
lblServerIp.setText(info.getServerIp());
lblServerName.setText(server.getName());
lblServerIp.setText(server.getUrl());
}
public AuthlibInjectorServerInfo getInfo() {
return info;
public AuthlibInjectorServer getServer() {
return server;
}
}
@@ -1,151 +1,70 @@
/*
* 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.*;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
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;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("account.injector.server"));
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("account.injector.manage.title"));
@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;
private InvalidationListener serversListener;
{
FXUtils.loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
FXUtils.smoothScrolling(scrollPane);
transitionHandler = new TransitionHandler(addServerContainer);
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
getChildren().remove(dialog);
dialog.setDialogContainer(this);
txtServerIp.textProperty().addListener((a, b, newValue) ->
btnAddNext.setDisable(!txtServerIp.validate()));
loading();
serversListener = observable -> updateServersList();
CONFIG.getAuthlibInjectorServers().addListener(new WeakInvalidationListener(serversListener));
updateServersList();
}
private void removeServer(AuthlibInjectorServerItem item) {
Settings.INSTANCE.removeAuthlibInjectorServerURL(item.getInfo().getServerIp());
loading();
private void updateServersList() {
listPane.getChildren().setAll(
CONFIG.getAuthlibInjectorServers().stream()
.map(server -> new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer())))
.collect(toList()));
}
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();
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
public String getTitle() {
@@ -26,12 +26,16 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.JavaVersion;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import java.util.function.Consumer;
public final class Controllers {
@@ -104,7 +108,7 @@ public final class Controllers {
decorator.setCustomMaximize(false);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().setAll(Settings.INSTANCE.getTheme().getStylesheets());
scene.getStylesheets().setAll(CONFIG.getTheme().getStylesheets());
stage.setMinWidth(804);
stage.setMaxWidth(804);
stage.setMinHeight(521);
@@ -132,15 +136,17 @@ public final class Controllers {
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, Controllers::closeDialog, type, onAccept));
dialog(new MessageDialogPane(text, title, type, onAccept));
}
public static void confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, Controllers::closeDialog, onAccept, onCancel));
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
}
public static void inputDialog(String text, Consumer<String> onResult) {
dialog(new InputDialogPane(text, Controllers::closeDialog, onResult));
public static InputDialogPane inputDialog(String text, FutureCallback<String> onResult) {
InputDialogPane pane = new InputDialogPane(text, onResult);
dialog(pane);
return pane;
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
@@ -156,8 +162,12 @@ public final class Controllers {
return pane;
}
/**
* Use {@link DialogCloseEvent}
*/
public static void closeDialog(Region content) {
if (stage == null) // shut down
return;
decorator.closeDialog(content);
}
@@ -174,6 +184,10 @@ public final class Controllers {
getLeftPaneController().showUpdate();
}
public static boolean isStopped() {
return decorator == null;
}
public static void shutdown() {
mainPage = null;
settingsPage = null;
@@ -27,6 +27,9 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.Launcher;
/**
@@ -37,9 +40,9 @@ public class CrashWindow extends Stage {
public CrashWindow(String text) {
Label lblCrash = new Label();
if (Launcher.UPDATE_CHECKER.isOutOfDate())
lblCrash.setText(Launcher.i18n("launcher.crash_out_dated"));
lblCrash.setText(i18n("launcher.crash_out_dated"));
else
lblCrash.setText(Launcher.i18n("launcher.crash"));
lblCrash.setText(i18n("launcher.crash"));
lblCrash.setWrapText(true);
TextArea textArea = new TextArea();
@@ -47,7 +50,7 @@ public class CrashWindow extends Stage {
textArea.setEditable(false);
Button btnContact = new Button();
btnContact.setText(Launcher.i18n("launcher.contact"));
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
@@ -65,7 +68,7 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(new Image("/assets/img/icon.png"));
setTitle(Launcher.i18n("message.error"));
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
}
@@ -22,12 +22,17 @@ import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
@@ -38,6 +43,8 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -45,7 +52,9 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
@@ -53,17 +62,26 @@ 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.DialogCloseEvent;
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;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class Decorator extends StackPane implements TaskExecutorDialogWizardDisplayer {
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
@@ -75,6 +93,8 @@ 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 static final String PROPERTY_DIALOG_CLOSE_HANDLER = Decorator.class.getName() + ".dialog.closeListener";
private final ObjectProperty<Runnable> onCloseButtonAction;
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
@@ -83,13 +103,14 @@ 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, maximized;
private BoundingBox originalBox, maximizedBox;
private final TransitionHandler animationHandler;
private final StackContainerPane dialogPane = new StackContainerPane();
private JFXDialog dialog;
private StackContainerPane dialogPane;
@FXML
private StackPane contentPlaceHolder;
@@ -129,6 +150,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private JFXButton btnClose;
@FXML
private HBox navLeft;
@FXML
private ImageView welcomeView;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
@@ -166,11 +189,18 @@ 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.setContent(dialogPane);
welcomeView.setCursor(Cursor.HAND);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> drawerWrapper.getChildren().remove(welcomeView))
);
nowAnimation.play();
});
if (!Settings.INSTANCE.isFirstLaunch() || Settings.INSTANCE.getLocale().getLocale() != Locale.CHINA)
drawerWrapper.getChildren().remove(welcomeView);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
@@ -187,79 +217,87 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
animationHandler = new TransitionHandler(contentPlaceHolder);
loadBackground();
setupBackground();
setupAuthlibInjectorDnD();
}
private void loadBackground() {
// ==== Background ====
private void setupBackground() {
drawerWrapper.backgroundProperty().bind(
Bindings.createObjectBinding(
() -> {
Image image = null;
if (CONFIG.getBackgroundImageType() == EnumBackgroundImage.CUSTOM) {
image = tryLoadImage(Paths.get(CONFIG.getBackgroundImage()))
.orElse(null);
}
if (image == null) {
image = loadDefaultBackgroundImage();
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
},
CONFIG.backgroundImageTypeProperty(),
CONFIG.backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
/**
* Load background image from bg/, background.png, background.jpg
*/
private Image loadDefaultBackgroundImage() {
Optional<Image> image = randomImageIn(Paths.get("bg"));
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.png"));
}
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.jpg"));
}
return image.orElse(defaultBackground);
}
private Optional<Image> randomImageIn(Path imageDir) {
if (!Files.isDirectory(imageDir)) {
return Optional.empty();
}
List<Path> candidates;
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());
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) {
candidates = Files.list(imageDir)
.filter(Files::isRegularFile)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
})
.collect(toList());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to list files in ./bg", e);
return Optional.empty();
}
Random rnd = new Random();
while (candidates.size() > 0) {
int selected = rnd.nextInt(candidates.size());
Optional<Image> loaded = tryLoadImage(candidates.get(selected));
if (loaded.isPresent()) {
return Optional.of(loaded.get());
} else {
candidates.remove(selected);
}
}
return Optional.empty();
}
private static Image searchBackgroundImage(Image def, String customPath) {
Random random = new Random();
boolean loaded = false;
Image background = def;
// custom path
if (StringUtils.isNotBlank(customPath)) {
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
background = new Image("file:" + customPath);
loaded = true;
} catch (IllegalArgumentException ignore) {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
}
// images in ./bg
if (!loaded) {
File backgroundImageFile = new File("bg");
if (backgroundImageFile.isDirectory()) {
File[] backgroundPath = backgroundImageFile.listFiles(file -> StringUtils.containsOne(FileUtils.getExtension(file), "png", "jpg"));
if (backgroundPath != null && backgroundPath.length > 0) {
int index = random.nextInt(backgroundPath.length);
try {
background = new Image("file:" + backgroundPath[index].getAbsolutePath());
loaded = true;
} catch (IllegalArgumentException ignore) {
}
}
}
}
// background.png
if (!loaded) {
File backgroundImageFile = new File("background.png");
if (backgroundImageFile.exists()) {
try {
background = new Image("file:" + backgroundImageFile.getAbsolutePath());
loaded = true;
} catch (IllegalArgumentException ignore) {
}
}
}
// background.jpg
if (!loaded) {
File backgroundImageFile = new File("background.jpg");
if (backgroundImageFile.exists()) {
try {
background = new Image("file:" + backgroundImageFile.getAbsolutePath());
loaded = true;
} catch (IllegalArgumentException ignore) {
}
}
}
return background;
return Optional.empty();
}
// ====
@FXML
private void onMouseMoved(MouseEvent mouseEvent) {
@@ -542,20 +580,37 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialogPane.isEmpty())
if (dialog == null) {
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialog.setContent(dialogPane);
dialog.setDialogContainer(drawerWrapper);
dialog.setOverlayClose(false);
dialog.show();
}
dialogPane.push(node);
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(DialogCloseEvent.CLOSE, handler);
}
@SuppressWarnings("unchecked")
public void closeDialog(Node node) {
FXUtils.checkFxUserThread();
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty())
Platform.runLater(() -> {
if (dialogPane.getChildren().isEmpty())
dialog.close();
});
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
}
}
public void startWizard(WizardProvider wizardProvider) {
@@ -649,4 +704,10 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
public AdvancedListBox getLeftPane() {
return leftPane;
}
private void setupAuthlibInjectorDnD() {
addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler());
addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler(
url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url))));
}
}
@@ -34,7 +34,7 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
JFXUtilities.runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, Controllers::closeDialog, it -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
@@ -23,6 +23,7 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -30,19 +31,22 @@ import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
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.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.io.File;
import java.io.IOException;
@@ -52,6 +56,7 @@ import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
@@ -68,6 +73,10 @@ public final class FXUtils {
}
}
public static InvalidationListener onInvalidating(Runnable action) {
return arg -> action.run();
}
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener((a, b, c) -> consumer.accept(c));
}
@@ -194,7 +203,7 @@ public final class FXUtils {
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Launcher.RESOURCE_BUNDLE);
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
@@ -402,7 +411,32 @@ public final class FXUtils {
}
}
public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png");
public static <T> StringConverter<T> stringConverter(Function<T, String> func) {
return new StringConverter<T>() {
@Override
public String toString(T object) {
return object == null ? "" : func.apply(object);
}
@Override
public T fromString(String string) {
throw new UnsupportedOperationException();
}
};
}
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
return view -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setGraphic(graphicBuilder.apply(item));
}
}
};
}
public static final Interpolator SINE = new Interpolator() {
@Override
@@ -20,7 +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;
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.LinkedList;
import java.util.Optional;
@@ -90,7 +91,7 @@ public class InstallerController {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
Controllers.dialog(Launcher.i18n("version.cannot_read"));
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
}
@@ -21,7 +21,8 @@ import com.jfoenix.effects.JFXDepthManager;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.Launcher;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.function.Consumer;
@@ -44,7 +45,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(Launcher.i18n("archive.version") + ": " + version);
lblInstallerVersion.setText(i18n("archive.version") + ": " + version);
}
@FXML
@@ -28,12 +28,11 @@ import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
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.download.VersionList;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -46,9 +45,11 @@ 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.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.HashMap;
@@ -61,27 +62,27 @@ public final class LeftPaneController {
private final VBox profilePane = new VBox();
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 VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), Launcher.i18n("settings.launcher")), iconedItem -> {
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), 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 -> {
.add(new ClassTitle(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())
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem)
.add(new ClassTitle(Launcher.i18n("profile.title").toUpperCase(), Lang.apply(new JFXButton(), button -> {
.add(new ClassTitle(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 ->
@@ -100,7 +101,7 @@ public final class LeftPaneController {
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane(Controllers::closeDialog));
Controllers.dialog(new AddAccountPane());
}
private void onSelectedAccountChanged(Account newAccount) {
@@ -122,7 +123,7 @@ public final class LeftPaneController {
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 ? Launcher.i18n("profile.selected") : "");
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? i18n("profile.selected") : "");
}
}
});
@@ -143,9 +144,9 @@ public final class LeftPaneController {
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return Launcher.i18n("account.methods.offline");
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
@@ -186,9 +187,9 @@ public final class LeftPaneController {
} 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 (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
if (selectedAccount == account)
ripplerContainer.setSelected(true);
@@ -207,7 +208,7 @@ public final class LeftPaneController {
}
public void showUpdate() {
launcherSettingsItem.setText(Launcher.i18n("update.found"));
launcherSettingsItem.setText(i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
}
@@ -228,10 +229,10 @@ public final class LeftPaneController {
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
Controllers.closeDialog(region.get());
region.get().fireEvent(new DialogCloseEvent());
checkAccount();
})).executor();
region.set(Controllers.taskDialog(executor, Launcher.i18n("modpack.installing"), ""));
region.set(Controllers.taskDialog(executor, i18n("modpack.installing"), ""));
executor.start();
showNewAccount = false;
} catch (UnsupportedModpackException ignore) {
@@ -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.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
@@ -43,6 +43,9 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.concurrent.CountDownLatch;
/**
@@ -62,8 +65,8 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
setTitle(Launcher.i18n("logwindow.title"));
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -29,7 +29,7 @@ import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -42,12 +42,12 @@ import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
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 java.io.File;
import java.io.IOException;
import java.util.List;
@@ -57,10 +57,11 @@ import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("main_page"));
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("main_page"));
private Profile profile;
@@ -95,10 +96,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
this.profile = event.getProfile();
});
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), Launcher.i18n("install")));
FXUtils.installTooltip(btnAdd, Launcher.i18n("install"));
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), i18n("install")));
FXUtils.installTooltip(btnAdd, i18n("install"));
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersionsAsync().start());
FXUtils.installTooltip(btnRefresh, Launcher.i18n("button.refresh"));
FXUtils.installTooltip(btnRefresh, i18n("button.refresh"));
}
private String modifyVersion(String gameVersion, String version) {
@@ -117,13 +118,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
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");
libraries.append(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");
libraries.append(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");
libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
@@ -138,15 +139,15 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Launcher.i18n("login.empty_username"));
Controllers.dialog(i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
if (repository.getRunDirectory(id).isDirectory())
chooser.setInitialDirectory(repository.getRunDirectory(id));
chooser.setTitle(Launcher.i18n("version.launch_script.save"));
chooser.setTitle(i18n("version.launch_script.save"));
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(Launcher.i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(Launcher.i18n("extension.sh"), "*.sh"));
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
@@ -158,25 +159,25 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
item.setOnUpdateButtonClicked(event -> {
FileChooser chooser = new FileChooser();
chooser.setTitle(Launcher.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
AtomicReference<Region> region = new AtomicReference<>();
try {
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"), ""));
.then(Task.of(Schedulers.javafx(), () -> region.get().fireEvent(new DialogCloseEvent()))).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.update"), ""));
executor.start();
} catch (UnsupportedModpackException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.unsupported"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (MismatchedModpackTypeException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.mismatched_type"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.mismatched_type"), 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);
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
}
});
@@ -187,10 +188,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
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")
i18n("version.manage.rename"),
i18n("version.manage.remove"),
i18n("modpack.export"),
i18n("folder.game")
));
versionList.setOnMouseClicked(e -> {
versionPopup.hide();
@@ -214,7 +215,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
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(Launcher.i18n("login.empty_username"));
Controllers.dialog(i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
}
@@ -26,13 +26,14 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.io.IOException;
@@ -42,7 +43,6 @@ 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 {
@FXML
@@ -140,8 +140,8 @@ public final class ModController {
@FXML
private void onAdd() {
FileChooser chooser = new FileChooser();
chooser.setTitle(Launcher.i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(Launcher.i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
chooser.setTitle(i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
// It's guaranteed that succeeded and failed are thread safe here.
@@ -163,10 +163,10 @@ public final class ModController {
}).with(Task.of(Schedulers.javafx(), variables -> {
List<String> prompt = new LinkedList<>();
if (!succeeded.isEmpty())
prompt.add(Launcher.i18n("mods.add.success", String.join(", ", succeeded)));
prompt.add(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"));
prompt.add(i18n("mods.add.failed", String.join(", ", failed)));
Controllers.dialog(String.join("\n", prompt), i18n("mods.add"));
loadMods(modManager, versionId);
})).start();
}
@@ -17,14 +17,16 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.function.Consumer;
@@ -40,7 +42,9 @@ public final class ModItem extends BorderPane {
setCenter(modItem);
JFXButton btnRemove = new JFXButton();
FXUtils.installTooltip(btnRemove, Launcher.i18n("mods.remove"));
JFXUtilities.runInFX(() -> {
FXUtils.installTooltip(btnRemove, i18n("mods.remove"));
});
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
@@ -50,7 +54,7 @@ public final class ModItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
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());
modItem.setSubtitle(info.getName() + ", " + i18n("archive.version") + ": " + info.getVersion() + ", " + i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + i18n("archive.author") + ": " + info.getAuthors());
chkEnabled.setSelected(info.isActive());
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
info.activeProperty().set(newValue));
@@ -18,18 +18,20 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.Optional;
@@ -39,12 +41,11 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
private final StringProperty location;
private final Profile profile;
@FXML
private JFXTextField txtProfileName;
@FXML
private FileItem gameDir;
@FXML private JFXTextField txtProfileName;
@FXML private FileItem gameDir;
@FXML private JFXButton btnSave;
@FXML private JFXButton btnDelete;
@FXML private JFXCheckBox toggleUseRelativePath;
/**
* @param profile null if creating a new profile.
@@ -54,7 +55,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
String profileDisplayName = Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse("");
title = new SimpleStringProperty(this, "title",
profile == null ? Launcher.i18n("profile.new") : Launcher.i18n("profile") + " - " + profileDisplayName);
profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName);
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(""));
@@ -64,13 +65,16 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
gameDir.setProperty(location);
gameDir.pathProperty().bindBidirectional(location);
FXUtils.onChangeAndOperate(location, it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
if (profile == null)
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
if (profile == null) {
btnDelete.setVisible(false);
} else {
toggleUseRelativePath.setSelected(profile.isUseRelativePath());
}
}
@FXML
@@ -85,13 +89,17 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
private void onSave() {
if (profile != null) {
profile.setName(txtProfileName.getText());
if (StringUtils.isNotBlank(getLocation()))
profile.setUseRelativePath(toggleUseRelativePath.isSelected());
if (StringUtils.isNotBlank(getLocation())) {
profile.setGameDir(new File(getLocation()));
}
} else {
if (StringUtils.isBlank(getLocation())) {
gameDir.onExplore();
}
Settings.INSTANCE.putProfile(new Profile(txtProfileName.getText(), new File(getLocation())));
Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
Settings.INSTANCE.putProfile(newProfile);
}
Settings.INSTANCE.onProfileLoading();
@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.When;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
@@ -27,7 +30,6 @@ import javafx.collections.ObservableList;
import javafx.fxml.FXML;
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;
@@ -42,14 +44,17 @@ import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.Locales;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.net.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.Objects;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("settings.launcher"));
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("settings.launcher"));
@FXML
private JFXTextField txtProxyHost;
@@ -84,9 +89,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private StackPane themeColorPickerContainer;
@FXML
private JFXRadioButton chkNoProxy;
@FXML
private JFXRadioButton chkManualProxy;
private JFXCheckBox chkEnableProxy;
@FXML
private JFXRadioButton chkProxyHttp;
@FXML
@@ -103,18 +106,6 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
FXUtils.smoothScrolling(scroll);
txtProxyHost.setText(Settings.INSTANCE.getProxyHost());
txtProxyHost.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyHost(newValue));
txtProxyPort.setText(Settings.INSTANCE.getProxyPort());
txtProxyPort.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPort(newValue));
txtProxyUsername.setText(Settings.INSTANCE.getProxyUser());
txtProxyUsername.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyUser(newValue));
txtProxyPassword.setText(Settings.INSTANCE.getProxyPass());
txtProxyPassword.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPass(newValue));
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
@@ -145,83 +136,96 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(Settings.INSTANCE.getLocale()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setLocale(Locales.getLocale(newValue.intValue())));
// ==== Proxy ====
txtProxyHost.textProperty().bindBidirectional(CONFIG.proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(CONFIG.proxyPortProperty());
txtProxyUsername.textProperty().bindBidirectional(CONFIG.proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(CONFIG.proxyPassProperty());
proxyPane.disableProperty().bind(chkEnableProxy.selectedProperty().not());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty());
ObjectProperty<Proxy.Type> selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
invalidated();
}
@Override
protected void invalidated() {
Proxy.Type type = Objects.requireNonNull(get());
if (type == Proxy.Type.DIRECT) {
set(Proxy.Type.HTTP); // HTTP by default
} else {
chkProxyHttp.setSelected(type == Proxy.Type.HTTP);
chkProxySocks.setSelected(type == Proxy.Type.SOCKS);
}
}
};
selectedProxyType.bindBidirectional(CONFIG.proxyTypeProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyHttp.setUserData(Proxy.Type.HTTP);
chkProxyHttp.setToggleGroup(proxyConfigurationGroup);
chkProxySocks.setUserData(Proxy.Type.SOCKS);
chkProxySocks.setToggleGroup(proxyConfigurationGroup);
proxyConfigurationGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
selectedProxyType.set((Proxy.Type) toggle.getUserData());
}
}));
// ====
for (Toggle toggle : proxyConfigurationGroup.getToggles())
if (toggle.getUserData() == Settings.INSTANCE.getProxyType())
toggle.setSelected(true);
fileCommonLocation.pathProperty().bindBidirectional(CONFIG.commonDirectoryProperty());
ToggleGroup hasProxyGroup = new ToggleGroup();
chkNoProxy.setToggleGroup(hasProxyGroup);
chkManualProxy.setToggleGroup(hasProxyGroup);
if (!Settings.INSTANCE.hasProxy())
chkNoProxy.setSelected(true);
else
chkManualProxy.setSelected(true);
proxyPane.disableProperty().bind(chkNoProxy.selectedProperty());
hasProxyGroup.selectedToggleProperty().addListener((a, b, newValue) ->
Settings.INSTANCE.setHasProxy(newValue != chkNoProxy));
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));
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty());
FXUtils.installTooltip(btnUpdate, Launcher.i18n("update.tooltip"));
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
// background
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(Launcher.i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
));
FXUtils.bindString(backgroundItem.getTxtCustom(), Settings.INSTANCE.backgroundImageProperty());
backgroundItem.setCustomUserData(EnumBackgroundImage.CUSTOM);
backgroundItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Settings.INSTANCE.getBackgroundImageType()).findFirst().ifPresent(it -> it.setSelected(true));
backgroundItem.getTxtCustom().textProperty().bindBidirectional(CONFIG.backgroundImageProperty());
Settings.INSTANCE.backgroundImageProperty().setChangedListener(it -> initBackgroundItemSubtitle());
Settings.INSTANCE.backgroundImageTypeProperty().setChangedListener(it -> initBackgroundItemSubtitle());
initBackgroundItemSubtitle();
ObjectProperty<EnumBackgroundImage> backgroundType = new SimpleObjectProperty<EnumBackgroundImage>(EnumBackgroundImage.DEFAULT) {
{
invalidated();
}
backgroundItem.setToggleSelectedListener(newValue ->
Settings.INSTANCE.setBackgroundImageType((EnumBackgroundImage) newValue.getUserData()));
@Override
protected void invalidated() {
backgroundItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == get())
.findFirst()
.ifPresent(it -> it.setSelected(true));
}
};
backgroundItem.getGroup().selectedToggleProperty().addListener((observable, oldValue, newValue) -> backgroundType.set((EnumBackgroundImage) newValue.getUserData()));
backgroundType.bindBidirectional(CONFIG.backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
new When(backgroundType.isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(CONFIG.backgroundImageProperty()));
// theme
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()));
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
Settings.INSTANCE.setTheme(theme);
CONFIG.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(Launcher.i18n("launcher.background.default"));
break;
case CUSTOM:
backgroundItem.setSubtitle(Settings.INSTANCE.getBackgroundImage());
break;
}
}
public String getTitle() {
return title.get();
}
@@ -239,16 +243,16 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate());
if (Launcher.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(Launcher.i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
lblUpdateSub.setText(i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(Launcher.i18n("update.found"));
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else {
lblUpdateSub.setText(Launcher.i18n("update.latest"));
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(Launcher.i18n("update"));
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
}
}
@@ -1,3 +1,20 @@
/*
* 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 javafx.beans.property.SimpleStringProperty;
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
@@ -33,8 +34,9 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Optional;
@@ -173,10 +175,12 @@ public final class VersionItem extends StackPane {
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"));
JFXUtilities.runInFX(() -> {
FXUtils.installTooltip(btnSettings, i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, i18n("version.update"));
FXUtils.installTooltip(btnLaunch, i18n("version.launch"));
FXUtils.installTooltip(btnScript, 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);
@@ -26,12 +26,13 @@ import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane;
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;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
@@ -79,17 +80,17 @@ public final class VersionPage extends StackPane implements DecoratorPage {
browsePopup = new JFXPopup(browseList);
managementPopup = new JFXPopup(managementList);
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"));
FXUtils.installTooltip(btnDelete, i18n("version.manage.remove"));
FXUtils.installTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management"));
FXUtils.installTooltip(btnExport, i18n("modpack.export"));
}
public void load(String id, Profile profile) {
this.version = id;
this.profile = profile;
title.set(Launcher.i18n("settings.game") + " - " + id);
title.set(i18n("settings.game") + " - " + id);
versionSettingsController.loadVersionSetting(profile, id);
modController.setParentTab(tabPane);
@@ -185,7 +186,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void deleteVersion(Profile profile, String version) {
Controllers.confirmDialog(Launcher.i18n("version.manage.remove.confirm", version), Launcher.i18n("message.confirm"), () -> {
Controllers.confirmDialog(i18n("version.manage.remove.confirm", version), i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
@@ -194,15 +195,18 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void renameVersion(Profile profile, String version) {
Controllers.inputDialog(Launcher.i18n("version.manage.rename.message"), res -> {
Controllers.inputDialog(i18n("version.manage.rename.message"), (res, resolve, reject) -> {
if (profile.getRepository().renameVersion(version, res)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
resolve.run();
} else {
reject.accept(i18n("version.manage.rename.fail"));
}
});
}).setInitialText(version);
}
public static void exportVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), Launcher.i18n("modpack.wizard"));
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), i18n("modpack.wizard"));
}
}
@@ -29,7 +29,7 @@ import javafx.scene.control.Toggle;
import javafx.scene.image.Image;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -39,12 +39,12 @@ 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.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -52,6 +52,7 @@ public final class VersionSettingsController {
private VersionSetting lastVersionSetting = null;
private Profile profile;
private String versionId;
private boolean javaItemsLoaded;
@FXML private VBox rootPane;
@FXML private ScrollPane scroll;
@@ -79,7 +80,7 @@ public final class VersionSettingsController {
@FXML
private void initialize() {
lblPhysicalMemory.setText(Launcher.i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
lblPhysicalMemory.setText(i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
FXUtils.smoothScrolling(scroll);
@@ -89,6 +90,7 @@ public final class VersionSettingsController {
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
});
@@ -96,19 +98,17 @@ public final class VersionSettingsController {
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
gameDirItem.loadChildren(Arrays.asList(
gameDirItem.createChildren(Launcher.i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(Launcher.i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
gameDirItem.createChildren(i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
));
globalItem.loadChildren(Arrays.asList(
globalItem.createChildren(Launcher.i18n("settings.type.global"), true),
globalItem.createChildren(Launcher.i18n("settings.type.special"), false)
globalItem.createChildren(i18n("settings.type.global"), true),
globalItem.createChildren(i18n("settings.type.special"), false)
));
}
public void loadVersionSetting(Profile profile, String versionId) {
rootPane.getChildren().remove(advancedSettingsPane);
this.profile = profile;
this.versionId = versionId;
@@ -117,6 +117,7 @@ public final class VersionSettingsController {
gameDirItem.setDisable(profile.getRepository().isModpack(versionId));
globalItem.setDisable(profile.getRepository().isModpack(versionId));
// unbind data fields
if (lastVersionSetting != null) {
FXUtils.unbindInt(txtWidth, lastVersionSetting.widthProperty());
FXUtils.unbindInt(txtHeight, lastVersionSetting.heightProperty());
@@ -136,6 +137,12 @@ public final class VersionSettingsController {
FXUtils.unbindEnum(cboLauncherVisibility);
}
// unbind data fields
globalItem.setToggleSelectedListener(null);
javaItem.setToggleSelectedListener(null);
gameDirItem.setToggleSelectedListener(null);
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
FXUtils.bindInt(txtMaxMemory, versionSetting.maxMemoryProperty());
@@ -173,6 +180,8 @@ public final class VersionSettingsController {
else
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.FALSE).findFirst().ifPresent(it -> it.setSelected(true));
globalItem.setToggleSelectedListener(newValue -> {
// do not call versionSettings.setUsesGlobal(true/false)
// because versionSettings can be the global one.
if ((Boolean) newValue.getUserData())
profile.globalizeVersionSetting(versionId);
else
@@ -182,7 +191,7 @@ public final class VersionSettingsController {
});
versionSetting.usesGlobalProperty().setChangedListenerAndOperate(it ->
globalItem.setSubtitle(Launcher.i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
globalItem.setSubtitle(i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
gameDirItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == versionSetting.getGameDirType())
@@ -204,23 +213,34 @@ public final class VersionSettingsController {
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !JavaVersion.getJREsImmediately().isPresent() /* JREs are still being loaded */) {
|| !javaItemsLoaded /* 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;
List<Toggle> toggles = javaItem.getGroup().getToggles();
if ("Custom".equals(lastVersionSetting.getJava())) {
toggles.stream()
.filter(javaItem::isCustomToggle)
.findFirst()
.get().setSelected(true);
} else {
JavaVersion selectedJava;
try {
selectedJava = lastVersionSetting.getJavaVersion();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
toggles.stream()
.filter(it -> it.getUserData() == selectedJava)
.findFirst()
.orElseGet( // fallback to select current java
() -> toggles.stream()
.filter(it -> it.getUserData() == JavaVersion.fromCurrentEnvironment())
.findFirst()
.get())
.setSelected(true);
;
}
}
@@ -235,18 +255,10 @@ public final class VersionSettingsController {
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
}
@FXML
private void onShowAdvanced() {
if (!rootPane.getChildren().contains(advancedSettingsPane))
rootPane.getChildren().add(advancedSettingsPane);
else
rootPane.getChildren().remove(advancedSettingsPane);
}
@FXML
private void onExploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("extension.png"), "*.png"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIcon(versionId);
@@ -264,7 +276,7 @@ public final class VersionSettingsController {
if (iconFile.exists())
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconPickerItem.setImage(FXUtils.DEFAULT_ICON);
iconPickerItem.setImage(Constants.DEFAULT_ICON.get());
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
}
@@ -21,14 +21,15 @@ import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.Settings;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
public class WebStage extends Stage {
private final WebView webView = new WebView();
public WebStage() {
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -18,11 +18,12 @@
package org.jackhuang.hmcl.ui.animation;
import javafx.animation.KeyFrame;
import javafx.util.Duration;
import java.util.List;
@FunctionalInterface
public interface AnimationProducer {
void init(AnimationHandler handler);
List<KeyFrame> animate(AnimationHandler handler);
}
@@ -20,17 +20,43 @@ package org.jackhuang.hmcl.ui.animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.scene.Node;
import javafx.util.Duration;
import org.jackhuang.hmcl.util.Lang;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
public enum ContainerAnimations {
NONE(c -> Collections.emptyList()),
NONE(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c -> Collections.emptyList()),
/**
* A fade between the old and new view
*/
FADE(c ->
FADE(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().opacityProperty(), 1.0D, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
@@ -40,7 +66,12 @@ public enum ContainerAnimations {
/**
* A zoom effect
*/
ZOOM_IN(c ->
ZOOM_IN(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
@@ -52,7 +83,12 @@ public enum ContainerAnimations {
/**
* A zoom effect
*/
ZOOM_OUT(c ->
ZOOM_OUT(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}, c ->
(Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
@@ -64,7 +100,14 @@ public enum ContainerAnimations {
/**
* A swipe effect
*/
SWIPE_LEFT(c ->
SWIPE_LEFT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
@@ -75,7 +118,14 @@ public enum ContainerAnimations {
/**
* A swipe effect
*/
SWIPE_RIGHT(c ->
SWIPE_RIGHT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
@@ -85,8 +135,18 @@ public enum ContainerAnimations {
private final AnimationProducer animationProducer;
ContainerAnimations(AnimationProducer animationProducer) {
this.animationProducer = animationProducer;
ContainerAnimations(Consumer<AnimationHandler> init, Function<AnimationHandler, List<KeyFrame>> animationProducer) {
this.animationProducer = new AnimationProducer() {
@Override
public void init(AnimationHandler handler) {
init.accept(handler);
}
@Override
public List<KeyFrame> animate(AnimationHandler handler) {
return animationProducer.apply(handler);
}
};
}
public AnimationProducer getAnimationProducer() {
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
@@ -78,28 +79,38 @@ public final class TransitionHandler implements AnimationHandler {
updateContent(newView);
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(transition.animate(this));
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> view.getChildren().remove(previousNode)));
nowAnimation.play();
animation = nowAnimation;
transition.init(this);
// runLater or "init" will not work
Platform.runLater(() -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(transition.animate(this));
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
view.setMouseTransparent(false);
view.getChildren().remove(previousNode);
}));
nowAnimation.play();
animation = nowAnimation;
});
}
private void updateContent(Node newView) {
if (view.getWidth() > 0 && view.getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null)
previousNode = NULL;
previousNode = EMPTY_PANE;
} else
previousNode = NULL;
previousNode = EMPTY_PANE;
if (previousNode == currentNode)
previousNode = NULL;
if (previousNode == newView)
previousNode = EMPTY_PANE;
view.setMouseTransparent(true);
currentNode = newView;
view.getChildren().setAll(previousNode, currentNode);
}
private static final StackPane NULL = new StackPane();
private final StackPane EMPTY_PANE = new StackPane();
}
@@ -15,22 +15,31 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.auth.authlibinjector;
package org.jackhuang.hmcl.ui.construct;
public class AuthlibInjectorServerInfo {
private final String serverIp;
private final String serverName;
import org.jackhuang.hmcl.ui.Controllers;
public AuthlibInjectorServerInfo(String serverIp, String serverName) {
this.serverIp = serverIp;
this.serverName = serverName;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.layout.Region;
/**
* Indicates a close operation on the dialog.
*
* @author yushijinhun
* @see Controllers#dialog(Region)
*/
public class DialogCloseEvent extends Event {
public static final EventType<DialogCloseEvent> CLOSE = new EventType<>("CLOSE");
public DialogCloseEvent() {
super(CLOSE);
}
public String getServerIp() {
return serverIp;
public DialogCloseEvent(Object source, EventTarget target) {
super(source, target, CLOSE);
}
public String getServerName() {
return serverName;
}
}
@@ -18,7 +18,9 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.Property;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
@@ -26,46 +28,70 @@ import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
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;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileItem extends BorderPane {
private Property<String> property;
private final Label x = new Label();
private final Label lblPath = new Label();
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
public FileItem() {
VBox left = new VBox();
Label name = new Label();
name.textProperty().bind(nameProperty());
x.getStyleClass().addAll("subtitle-label");
left.getChildren().addAll(name, x);
lblPath.getStyleClass().addAll("subtitle-label");
lblPath.textProperty().bind(path);
left.getChildren().addAll(name, lblPath);
setLeft(left);
JFXButton right = new JFXButton();
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
FXUtils.installTooltip(right, Launcher.i18n("button.edit"));
FXUtils.installTooltip(right, i18n("button.edit"));
setRight(right);
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
}
/**
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
*/
private String processPath(String path) {
Path given = Paths.get(path).toAbsolutePath();
if (isConvertToRelativePath()) {
try {
return Paths.get(".").normalize().toAbsolutePath().relativize(given).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return given.normalize().toString();
}
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
if (property.getValue() != null) {
File file = new File(property.getValue());
if (path.get() != null) {
File file = new File(path.get());
if (file.exists()) {
if (file.isFile())
file = file.getAbsoluteFile().getParentFile();
@@ -76,16 +102,12 @@ public class FileItem extends BorderPane {
}
chooser.titleProperty().bind(titleProperty());
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
property.setValue(selectedDir.getAbsolutePath());
if (selectedDir != null) {
path.set(processPath(selectedDir.toString()));
}
chooser.titleProperty().unbind();
}
public void setProperty(Property<String> property) {
this.property = property;
x.textProperty().bind(property);
}
public String getName() {
return name.get();
}
@@ -121,4 +143,28 @@ public class FileItem extends BorderPane {
public void setTooltip(String tooltip) {
this.tooltip.set(tooltip);
}
public String getPath() {
return path.get();
}
public StringProperty pathProperty() {
return path;
}
public void setPath(String path) {
this.path.set(path);
}
public boolean isConvertToRelativePath() {
return convertToRelativePath.get();
}
public BooleanProperty convertToRelativePathProperty() {
return convertToRelativePath;
}
public void setConvertToRelativePath(boolean convertToRelativePath) {
this.convertToRelativePath.set(convertToRelativePath);
}
}
@@ -15,7 +15,9 @@ 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 static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -41,7 +43,7 @@ public final class ImagePickerItem extends BorderPane {
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, Launcher.i18n("button.edit"));
FXUtils.installTooltip(selectButton, i18n("button.edit"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton);
@@ -19,13 +19,12 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
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;
import org.jackhuang.hmcl.util.FutureCallback;
public class InputDialogPane extends StackPane {
@@ -37,14 +36,28 @@ public class InputDialogPane extends StackPane {
private JFXTextField textField;
@FXML
private Label content;
@FXML
private Label lblCreationWarning;
public InputDialogPane(String text, Consumer<Region> closeConsumer, Consumer<String> onResult) {
public InputDialogPane(String text, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
cancelButton.setOnMouseClicked(e -> closeConsumer.accept(this));
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> {
onResult.accept(textField.getText());
closeConsumer.accept(this);
onResult.call(textField.getText(), () -> {
fireEvent(new DialogCloseEvent());
}, msg -> {
lblCreationWarning.setText(msg);
});
});
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> !textField.validate(),
textField.textProperty()
));
}
public void setInitialText(String text) {
textField.setText(text);
}
}
@@ -20,17 +20,19 @@ 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.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.FXUtils;
import javax.swing.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Optional;
public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = Launcher.i18n("message.info");
private static final String TITLE = i18n("message.info");
/**
* User Operation: Yes
@@ -21,18 +21,16 @@ import com.jfoenix.controls.JFXButton;
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.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Optional;
import java.util.function.Consumer;
public final class MessageDialogPane extends StackPane {
private boolean closingDialog = true;
@FXML
private JFXButton acceptButton;
@@ -47,7 +45,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, Consumer<Region> closeConsumer, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, int type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -55,7 +53,7 @@ public final class MessageDialogPane extends StackPane {
content.setText(text);
acceptButton.setOnMouseClicked(e -> {
closeConsumer.accept(MessageDialogPane.this);
fireEvent(new DialogCloseEvent());
Optional.ofNullable(onAccept).ifPresent(Runnable::run);
});
@@ -82,22 +80,18 @@ public final class MessageDialogPane extends StackPane {
}
}
public MessageDialogPane(String text, String title, Consumer<Region> closeConsumer, Runnable onAccept, Runnable onCancel) {
this(text, title, closeConsumer, MessageBox.QUESTION_MESSAGE, onAccept);
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageBox.QUESTION_MESSAGE, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
closeConsumer.accept(MessageDialogPane.this);
fireEvent(new DialogCloseEvent());
Optional.ofNullable(onCancel).ifPresent(Runnable::run);
});
acceptButton.setText(Launcher.i18n("button.yes"));
cancelButton.setText(Launcher.i18n("button.no"));
acceptButton.setText(i18n("button.yes"));
cancelButton.setText(i18n("button.no"));
actions.getChildren().add(cancelButton);
}
public void disableClosingDialog() {
closingDialog = false;
}
}
@@ -13,16 +13,17 @@ 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 static org.jackhuang.hmcl.util.i18n.I18n.i18n;
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 StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final ToggleGroup group = new ToggleGroup();
private final JFXColorPicker colorPicker = new JFXColorPicker();
@@ -37,19 +37,20 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
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;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.Collection;
import java.util.function.Consumer;
public class MultiFileItem 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 StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
@@ -147,7 +148,7 @@ public class MultiFileItem extends ComponentList {
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(Launcher.i18n(getChooserTitle()));
chooser.setTitle(i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
@@ -0,0 +1,61 @@
/*
* 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 com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
@DefaultProperty("content")
public class SpinnerPane extends StackPane {
private final TransitionHandler transitionHandler = new TransitionHandler(this);
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
public SpinnerPane() {
getChildren().setAll(contentPane);
content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue));
}
public void showSpinner() {
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
}
public void hideSpinner() {
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
public Node getContent() {
return content.get();
}
public ObjectProperty<Node> contentProperty() {
return content;
}
public void setContent(Node content) {
this.content.set(content);
}
}
@@ -19,13 +19,14 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.AbstractWizardDisplayer;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Map;
@@ -34,11 +35,11 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
@Override
default void handleTask(Map<String, Object> settings, Task task) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(it -> {
Controllers.closeDialog(it);
it.fireEvent(new DialogCloseEvent());
Controllers.navigate(null);
});
pane.setTitle(Launcher.i18n("message.doing"));
pane.setTitle(i18n("message.doing"));
pane.setProgress(Double.MAX_VALUE);
if (settings.containsKey("title")) {
Object title = settings.get("title");
@@ -61,12 +62,12 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
@Override
public void onStop(boolean success, TaskExecutor executor) {
JFXUtilities.runInFX(() -> {
Controllers.closeDialog(pane);
pane.fireEvent(new DialogCloseEvent());
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));
Controllers.dialog(i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null));
} else {
if (executor.getLastException() == null)
return;
@@ -74,7 +75,7 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
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));
Controllers.dialog(appendix, i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
}
});
@@ -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.Launcher;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameAssetRefreshTask;
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.HashMap;
import java.util.Map;
@@ -62,29 +63,29 @@ public final class TaskListPane extends StackPane {
return;
if (task instanceof GameAssetRefreshTask) {
task.setName(Launcher.i18n("assets.download"));
task.setName(i18n("assets.download"));
} else if (task instanceof GameAssetDownloadTask) {
task.setName(Launcher.i18n("assets.download_all"));
task.setName(i18n("assets.download_all"));
} else if (task instanceof ForgeInstallTask) {
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.forge")));
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.liteloader")));
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
} else if (task instanceof OptiFineInstallTask) {
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.optifine")));
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
} else if (task instanceof CurseCompletionTask) {
task.setName(Launcher.i18n("modpack.type.curse.completion"));
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
task.setName(Launcher.i18n("modpack.installing"));
task.setName(i18n("modpack.installing"));
} else if (task instanceof CurseInstallTask) {
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.curse")));
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.multimc")));
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.hmcl")));
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof HMCLModpackExportTask) {
task.setName(Launcher.i18n("modpack.export"));
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(Launcher.i18n("modpack.scan"));
task.setName(i18n("modpack.scan"));
}
ProgressListNode node = new ProgressListNode(task);
@@ -22,7 +22,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
@@ -30,12 +30,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Lang;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.Launcher.i18n;
class AdditionalInstallersPage extends StackPane implements WizardPage {
private final InstallerWizardProvider provider;
private final WizardController controller;
@@ -100,7 +99,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Launcher.i18n("settings.tabs.installers");
return i18n("settings.tabs.installers");
}
private String getVersion(String id) {
@@ -109,24 +108,24 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(Launcher.i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
btnForge.setDisable(provider.getForge() != null);
if (provider.getForge() != null || controller.getSettings().containsKey("forge"))
lblForge.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), getVersion("forge")));
lblForge.setText(i18n("install.installer.version", i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), getVersion("forge")));
else
lblForge.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.forge")));
lblForge.setText(i18n("install.installer.not_installed", i18n("install.installer.forge")));
btnLiteLoader.setDisable(provider.getLiteLoader() != null);
if (provider.getLiteLoader() != null || controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), getVersion("liteloader")));
lblLiteLoader.setText(i18n("install.installer.version", i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), getVersion("liteloader")));
else
lblLiteLoader.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.liteloader")));
lblLiteLoader.setText(i18n("install.installer.not_installed", i18n("install.installer.liteloader")));
btnOptiFine.setDisable(provider.getOptiFine() != null);
if (provider.getOptiFine() != null || controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), getVersion("optifine")));
lblOptiFine.setText(i18n("install.installer.version", i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), getVersion("optifine")));
else
lblOptiFine.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.optifine")));
lblOptiFine.setText(i18n("install.installer.not_installed", i18n("install.installer.optifine")));
}
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
@@ -30,12 +30,11 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import java.io.File;
import java.util.Map;
import static org.jackhuang.hmcl.Launcher.i18n;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class DownloadWizardProvider implements WizardProvider {
private Profile profile;
@@ -69,7 +68,7 @@ public final class DownloadWizardProvider implements WizardProvider {
return 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);
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
if (selected == null || modpack == null || name == null) return null;
@@ -78,8 +77,8 @@ public final class DownloadWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", Launcher.i18n("install.success"));
settings.put("failure_message", Launcher.i18n("install.failed"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
switch (Lang.parseInt(settings.get(InstallTypePage.INSTALL_TYPE), -1)) {
case 0: return finishVersionDownloadingAsync(settings);
@@ -102,10 +101,10 @@ public final class DownloadWizardProvider implements WizardProvider {
case 1:
return new ModpackPage(controller);
default:
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings);
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings + ", pages: " + controller.getPages());
}
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings);
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@@ -20,10 +20,11 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXListView;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
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;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Map;
@@ -49,7 +50,7 @@ public final class InstallTypePage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Launcher.i18n("install.select");
return i18n("install.select");
}
public static final String INSTALL_TYPE = "INSTALL_TYPE";
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Map;
@@ -80,8 +81,8 @@ public final class InstallerWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", Launcher.i18n("install.success"));
settings.put("failure_message", Launcher.i18n("install.failed"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
Task ret = Task.empty();
@@ -23,7 +23,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Map;
@@ -75,30 +76,30 @@ public class InstallersPage extends StackPane implements WizardPage {
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(Launcher.i18n("install.new_game.already_exists"));
hasVersion.setMessage(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, Launcher.i18n("install.installer.choose", Launcher.i18n("install.installer.forge")), gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.forge")), gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
});
btnLiteLoader.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 1);
controller.onNext(new VersionsPage(controller, Launcher.i18n("install.installer.choose", Launcher.i18n("install.installer.liteloader")), gameVersion, downloadProvider, "liteloader", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.liteloader")), gameVersion, downloadProvider, "liteloader", () -> controller.onPrev(false)));
});
btnOptiFine.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 2);
controller.onNext(new VersionsPage(controller, Launcher.i18n("install.installer.choose", Launcher.i18n("install.installer.optifine")), gameVersion, downloadProvider, "optifine", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.optifine")), gameVersion, downloadProvider, "optifine", () -> controller.onPrev(false)));
});
}
@Override
public String getTitle() {
return Launcher.i18n("install.new_game");
return i18n("install.new_game");
}
private String getVersion(String id) {
@@ -107,21 +108,21 @@ public class InstallersPage extends StackPane implements WizardPage {
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(Launcher.i18n("install.new_game.current_game_version") + ": " + getVersion("game"));
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + getVersion("game"));
if (controller.getSettings().containsKey("forge"))
lblForge.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.forge")) + ": " + getVersion("forge"));
lblForge.setText(i18n("install.installer.version", i18n("install.installer.forge")) + ": " + getVersion("forge"));
else
lblForge.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.forge")));
lblForge.setText(i18n("install.installer.not_installed", i18n("install.installer.forge")));
if (controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.liteloader")) + ": " + getVersion("liteloader"));
lblLiteLoader.setText(i18n("install.installer.version", i18n("install.installer.liteloader")) + ": " + getVersion("liteloader"));
else
lblLiteLoader.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.liteloader")));
lblLiteLoader.setText(i18n("install.installer.not_installed", i18n("install.installer.liteloader")));
if (controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(Launcher.i18n("install.installer.version", Launcher.i18n("install.installer.optifine")) + ": " + getVersion("optifine"));
lblOptiFine.setText(i18n("install.installer.version", i18n("install.installer.optifine")) + ": " + getVersion("optifine"));
else
lblOptiFine.setText(Launcher.i18n("install.installer.not_installed", Launcher.i18n("install.installer.optifine")));
lblOptiFine.setText(i18n("install.installer.not_installed", i18n("install.installer.optifine")));
}
@Override
@@ -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.Launcher;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.Map;
@@ -75,28 +76,28 @@ public final class ModpackPage extends StackPane implements WizardPage {
Profile profile = (Profile) controller.getSettings().get("PROFILE");
FileChooser chooser = new FileChooser();
chooser.setTitle(Launcher.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) Platform.runLater(() -> Controllers.navigate(null));
else {
controller.getSettings().put(MODPACK_FILE, selectedFile);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
txtModpackName.getValidators().addAll(
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))
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
try {
manifest = ModpackHelper.readModpackManifest(selectedFile);
controller.getSettings().put(MODPACK_CURSEFORGE_MANIFEST, manifest);
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
} catch (UnsupportedModpackException e) {
txtModpackName.setText(Launcher.i18n("modpack.task.install.error"));
txtModpackName.setText(i18n("modpack.task.install.error"));
}
}
}
@@ -118,17 +119,17 @@ public final class ModpackPage extends StackPane implements WizardPage {
if (manifest != null) {
WebStage stage = new WebStage();
stage.getWebView().getEngine().loadContent(manifest.getDescription());
stage.setTitle(Launcher.i18n("modpack.wizard.step.3"));
stage.setTitle(i18n("modpack.wizard.step.3"));
stage.showAndWait();
}
}
@Override
public String getTitle() {
return Launcher.i18n("modpack.task.install");
return i18n("modpack.task.install");
}
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_CURSEFORGE_MANIFEST = "CURSEFORGE_MANIFEST";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
}
@@ -23,12 +23,13 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersionTag;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.ui.FXUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Objects;
@@ -57,15 +58,15 @@ public final class VersionsPageItem extends StackPane {
if (remoteVersion.getTag() instanceof GameRemoteVersionTag) {
switch (((GameRemoteVersionTag) remoteVersion.getTag()).getType()) {
case RELEASE:
lblGameVersion.setText(Launcher.i18n("version.game.release"));
lblGameVersion.setText(i18n("version.game.release"));
imageView.setImage(new Image("/assets/img/icon.png", 32, 32, false, true));
break;
case SNAPSHOT:
lblGameVersion.setText(Launcher.i18n("version.game.snapshot"));
lblGameVersion.setText(i18n("version.game.snapshot"));
imageView.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
break;
default:
lblGameVersion.setText(Launcher.i18n("version.game.old"));
lblGameVersion.setText(i18n("version.game.old"));
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
}
@@ -23,8 +23,8 @@ import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
@@ -37,6 +37,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
public final class ExportWizardProvider implements WizardProvider {
private final Profile profile;
private final String version;
@@ -80,20 +82,13 @@ public final class ExportWizardProvider implements WizardProvider {
boolean flag = true;
try (ZipEngine zip = new ZipEngine(modpackFile)) {
Config config = Settings.INSTANCE.getRawConfig();
config.hasProxy.set(false);
config.selectedProfile.set("");
config.commonDirectory.set(null);
config.fontFamily.set("Consolas");
config.fontSize.set(12);
config.localization.set(null);
config.accounts.clear();
config.selectedAccount.set("");
config.logLines.set(100);
config.configurations.clear();
zip.putTextFile(Settings.GSON.toJson(config), Settings.SETTINGS_FILE_NAME);
Config exported = new Config();
exported.setBackgroundImageType(CONFIG.getBackgroundImageType());
exported.setBackgroundImage(CONFIG.getBackgroundImage());
exported.setTheme(CONFIG.getTheme());
exported.setDownloadType(CONFIG.getDownloadType());
exported.getAuthlibInjectorServers().setAll(CONFIG.getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
@@ -25,7 +25,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.ModAdviser;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -34,7 +34,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
@@ -43,6 +42,7 @@ import java.util.Objects;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
@@ -151,23 +151,23 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
@Override
public String getTitle() {
return Launcher.i18n("modpack.wizard.step.2.title");
return i18n("modpack.wizard.step.2.title");
}
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
private static final Map<String, String> TRANSLATION = mapOf(
pair("minecraft/servers.dat", Launcher.i18n("modpack.files.servers_dat")),
pair("minecraft/saves", Launcher.i18n("modpack.files.saves")),
pair("minecraft/mods", Launcher.i18n("modpack.files.mods")),
pair("minecraft/config", Launcher.i18n("modpack.files.config")),
pair("minecraft/liteconfig", Launcher.i18n("modpack.files.liteconfig")),
pair("minecraft/resourcepacks", Launcher.i18n("modpack.files.resourcepacks")),
pair("minecraft/resources", Launcher.i18n("modpack.files.resourcepacks")),
pair("minecraft/options.txt", Launcher.i18n("modpack.files.options_txt")),
pair("minecraft/optionsshaders.txt", Launcher.i18n("modpack.files.optionsshaders_txt")),
pair("minecraft/mods/VoxelMods", Launcher.i18n("modpack.files.mods.voxelmods")),
pair("minecraft/dumps", Launcher.i18n("modpack.files.dumps")),
pair("minecraft/blueprints", Launcher.i18n("modpack.files.blueprints")),
pair("minecraft/scripts", Launcher.i18n("modpack.files.scripts"))
pair("minecraft/servers.dat", i18n("modpack.files.servers_dat")),
pair("minecraft/saves", i18n("modpack.files.saves")),
pair("minecraft/mods", i18n("modpack.files.mods")),
pair("minecraft/config", i18n("modpack.files.config")),
pair("minecraft/liteconfig", i18n("modpack.files.liteconfig")),
pair("minecraft/resourcepacks", i18n("modpack.files.resourcepacks")),
pair("minecraft/resources", i18n("modpack.files.resourcepacks")),
pair("minecraft/options.txt", i18n("modpack.files.options_txt")),
pair("minecraft/optionsshaders.txt", i18n("modpack.files.optionsshaders_txt")),
pair("minecraft/mods/VoxelMods", i18n("modpack.files.mods.voxelmods")),
pair("minecraft/dumps", i18n("modpack.files.dumps")),
pair("minecraft/blueprints", i18n("modpack.files.blueprints")),
pair("minecraft/scripts", i18n("modpack.files.scripts"))
);
}
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.List;
@@ -81,8 +82,8 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@FXML
private void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(Launcher.i18n("modpack.wizard.step.initialization.save"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File file = fileChooser.showSaveDialog(Controllers.getStage());
if (file == null) {
Controllers.navigate(null);
@@ -109,7 +110,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Launcher.i18n("modpack.wizard.step.1.title");
return i18n("modpack.wizard.step.1.title");
}
public static final String MODPACK_NAME = "modpack.name";
@@ -20,10 +20,7 @@ package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import java.util.*;
public class WizardController implements Navigation {
private final WizardDisplayer displayer;
@@ -47,6 +44,10 @@ public class WizardController implements Navigation {
this.provider = provider;
}
public List<Node> getPages() {
return Collections.unmodifiableList(pages);
}
@Override
public void onStart() {
Objects.requireNonNull(provider);
@@ -81,6 +82,10 @@ public class WizardController implements Navigation {
@Override
public void onPrev(boolean cleanUp) {
if (!canPrev()) {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
}
Node page = pages.pop();
if (cleanUp && page instanceof WizardPage)
((WizardPage) page).cleanup(settings);
@@ -23,17 +23,19 @@ import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@@ -49,7 +51,7 @@ import java.util.zip.GZIPInputStream;
*/
public class AppDataUpgrader extends IUpgrader {
private void launchNewerVersion(List<String> args, File jar) throws IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, InvocationTargetException, IllegalAccessException {
private void launchNewerVersion(List<String> args, File jar) throws IOException, ReflectiveOperationException {
try (JarFile jarFile = new JarFile(jar)) {
String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class");
if (mainClass == null)
@@ -91,7 +93,7 @@ public class AppDataUpgrader extends IUpgrader {
}
} catch (JsonParseException ex) {
f.delete();
} catch (IOException | NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException | ClassNotFoundException t) {
} catch (IOException | ReflectiveOperationException t) {
Logging.LOG.log(Level.SEVERE, "Unable to execute newer version application", t);
AppDataUpgraderPackGzTask.HMCL_VER_FILE.delete(); // delete version json, let HMCL re-download the newer version.
}
@@ -113,13 +115,13 @@ public class AppDataUpgrader extends IUpgrader {
Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
@@ -131,13 +133,13 @@ public class AppDataUpgrader extends IUpgrader {
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
@@ -153,7 +155,7 @@ public class AppDataUpgrader extends IUpgrader {
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(Launcher.i18n("update.no_browser"));
MessageBox.show(i18n("update.no_browser"));
}
}
})).start();
@@ -183,7 +185,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -233,7 +235,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -19,14 +19,14 @@ package org.jackhuang.hmcl.upgrade;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@@ -35,6 +35,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
*
@@ -57,11 +58,11 @@ public class NewFileUpgrader extends IUpgrader {
URL url = requestDownloadLink();
if (url == null) return;
File newf = new File(url.getFile());
Controllers.dialog(Launcher.i18n("message.downloading"));
Controllers.dialog(i18n("message.downloading"));
Task task = new FileDownloadTask(url, newf);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
if (executor.test()) {
try {
new ProcessBuilder(newf.getCanonicalPath(), "--removeOldLauncher", getRealPath())
@@ -72,7 +73,7 @@ public class NewFileUpgrader extends IUpgrader {
}
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
}
private static String getRealPath() {
@@ -27,9 +27,10 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.VersionNumber;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.IOException;
import java.util.Collection;
@@ -78,8 +79,10 @@ public final class UpdateChecker {
@Override
public void execute() throws Exception {
if (Launcher.VERSION.contains("@"))
if (isDevelopmentVersion(Launcher.VERSION)) {
LOG.info("Current version is a development version, skip updating");
return;
}
if (value == null) {
versionString = http.getResult();
@@ -87,9 +90,9 @@ public final class UpdateChecker {
}
if (value == null) {
Logging.LOG.warning("Unable to check update...");
LOG.warning("Unable to check update...");
if (showMessage)
MessageBox.show(Launcher.i18n("update.failed"));
MessageBox.show(i18n("update.failed"));
} else if (base.compareTo(value) < 0)
outOfDate = true;
if (outOfDate)
@@ -103,6 +106,11 @@ public final class UpdateChecker {
};
}
private boolean isDevelopmentVersion(String version) {
return version.contains("@") || // eg. @HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
}
/**
* Get the <b>cached</b> newest version number, use "process" method to
* download!
@@ -124,12 +132,13 @@ public final class UpdateChecker {
return new TaskResult<Map<String, String>>() {
@Override
public void execute() {
if (download_link == null)
if (download_link == null) {
try {
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update_link.php")), Map.class);
} catch (JsonSyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to get update link.", e);
LOG.log(Level.SEVERE, "Failed to get update link.", e);
}
}
setResult(download_link);
}
@@ -21,12 +21,17 @@ import javafx.application.Platform;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import static java.util.Collections.newSetFromMap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
/**
@@ -34,21 +39,19 @@ import java.util.logging.Level;
*/
public class CrashReporter implements Thread.UncaughtExceptionHandler {
private static final HashMap<String, String> SOURCE = new HashMap<String, String>() {
private static final Map<String, String> SOURCE = new HashMap<String, String>() {
{
put("javafx.fxml.LoadException", Launcher.i18n("crash.NoClassDefFound"));
put("Location is not set", Launcher.i18n("crash.NoClassDefFound"));
put("UnsatisfiedLinkError", Launcher.i18n("crash.user_fault"));
put("java.lang.NoClassDefFoundError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.VerifyError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchFieldError", Launcher.i18n("crash.NoClassDefFound"));
put("netscape.javascript.JSException", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.IncompatibleClassChangeError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.ClassFormatError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.OutOfMemoryError", "FUCKING MEMORY LIMIT!");
put("Trampoline", Launcher.i18n("launcher.update_java"));
put("com.sun.javafx.css.StyleManager.findMatchingStyles", Launcher.i18n("launcher.update_java"));
put("javafx.fxml.LoadException", i18n("crash.NoClassDefFound"));
put("Location is not set", i18n("crash.NoClassDefFound"));
put("UnsatisfiedLinkError", i18n("crash.user_fault"));
put("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound"));
put("java.lang.VerifyError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound"));
put("netscape.javascript.JSException", i18n("crash.NoClassDefFound"));
put("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound"));
put("java.lang.ClassFormatError", i18n("crash.NoClassDefFound"));
put("com.sun.javafx.css.StyleManager.findMatchingStyles", i18n("launcher.update_java"));
put("NoSuchAlgorithmException", "Has your operating system been installed completely or is a ghost system?");
}
};
@@ -59,11 +62,11 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
if (s.contains(entry.getKey())) {
if (StringUtils.isNotBlank(entry.getValue())) {
String info = entry.getValue();
Logging.LOG.severe(info);
LOG.severe(info);
try {
MessageBox.show(info);
} catch (Throwable t) {
Logging.LOG.log(Level.SEVERE, "Unable to show message", t);
LOG.log(Level.SEVERE, "Unable to show message", t);
}
}
return false;
@@ -71,17 +74,21 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
return true;
}
private static Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
@Override
public void uncaughtException(Thread t, Throwable e) {
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
return;
if (THROWABLE_SET.contains(stackTrace))
return;
THROWABLE_SET.add(stackTrace);
LOG.log(Level.SEVERE, "Uncaught exception in thread " + t.getName(), e);
try {
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
return;
if (CAUGHT_EXCEPTIONS.contains(stackTrace))
return;
CAUGHT_EXCEPTIONS.add(stackTrace);
String text = "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Launcher.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
@@ -93,21 +100,18 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n";
Logging.LOG.log(Level.SEVERE, text);
LOG.log(Level.SEVERE, text);
if (checkThrowable(e)) {
Platform.runLater(() -> new CrashWindow(text).show());
if (!Launcher.UPDATE_CHECKER.isOutOfDate())
reportToServer(text);
}
} catch (Throwable ex) {
Logging.LOG.log(Level.SEVERE, "Unable to caught exception", ex);
Logging.LOG.log(Level.SEVERE, "There is the original exception", e);
} catch (Throwable handlingException) {
LOG.log(Level.SEVERE, "Unable to handle uncaught exception", handlingException);
}
}
private static final HashSet<String> THROWABLE_SET = new HashSet<>();
private void reportToServer(final String text) {
Thread t = new Thread(() -> {
HashMap<String, String> map = new HashMap<>();
@@ -115,11 +119,11 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
map.put("version", Launcher.VERSION);
map.put("log", Logging.getLogs());
try {
String response = NetworkUtils.doPost(NetworkUtils.toURL("https://huangyuhui.duapp.com/hmcl/crash.php"), map);
String response = NetworkUtils.doPost(NetworkUtils.toURL("https://hmcl.huangyuhui.net/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response))
Logging.LOG.log(Level.SEVERE, "Crash server response: " + response);
LOG.log(Level.SEVERE, "Crash server response: " + response);
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Unable to post HMCL server.", ex);
LOG.log(Level.SEVERE, "Unable to post HMCL server.", ex);
}
});
t.setDaemon(true);
@@ -0,0 +1,59 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util.i18n;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.logging.Level;
import org.jackhuang.hmcl.setting.Settings;
public final class I18n {
private I18n() {}
private static ResourceBundle RESOURCE_BUNDLE = null;
static {
try {
RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
} catch (Throwable e) {
LOG.log(Level.SEVERE, "Settings cannot be initialized", e);
}
}
public static ResourceBundle getResourceBundle() {
return RESOURCE_BUNDLE == null ? Locales.DEFAULT.getResourceBundle() : RESOURCE_BUNDLE;
}
public static String i18n(String key, Object... formatArgs) {
return String.format(i18n(key), formatArgs);
}
public static String i18n(String key) {
try {
return getResourceBundle().getString(key);
} catch (MissingResourceException e) {
LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
return key;
}
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -15,9 +15,8 @@
* 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.setting;
package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.ui.construct.UTF8Control;
import org.jackhuang.hmcl.util.Lang;
import java.util.List;
@@ -53,7 +52,7 @@ public final class Locales {
*/
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN);
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH);
public static SupportedLocale getLocale(int index) {
return Lang.get(LOCALES, index).orElse(DEFAULT);
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -15,7 +15,9 @@
* 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;
package org.jackhuang.hmcl.util.i18n;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.io.InputStream;
@@ -26,14 +28,14 @@ import java.util.Locale;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
public final class UTF8Control extends ResourceBundle.Control {
final class UTF8Control extends ResourceBundle.Control {
public static final UTF8Control INSTANCE = new UTF8Control();
private UTF8Control() {}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IOException {
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IOException {
// The below is a copy of the default implementation.
String bundleName = toBundleName(baseName, locale);
String resourceName = toResourceName(bundleName, "properties");
@@ -54,7 +56,7 @@ public final class UTF8Control extends ResourceBundle.Control {
if (stream != null) {
try {
// Only this line is changed to make it to read properties files as UTF-8.
bundle = new PropertyResourceBundle(new InputStreamReader(stream, "UTF-8"));
bundle = new PropertyResourceBundle(new InputStreamReader(stream, UTF_8));
} finally {
stream.close();
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #283593;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: white;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #43A047;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: black;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #E67E22;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: black;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #9C27B0;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: white;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #F44336;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: black;
}
+8 -1
View File
@@ -569,7 +569,6 @@
.list-cell:selected, .jfx-list-cell:selected {
-fx-background-insets: 0.0;
-fx-text-fill: BLACK;
-fx-background-color: black;
}
.jfx-list-cell:filled:hover,
@@ -919,6 +918,14 @@
-fx-stroke-width: 3.0;
}
.small-spinner-pane .jfx-spinner {
-jfx-radius: 10;
}
.small-spinner-pane .jfx-spinner > .arc {
-fx-stroke-width: 3.0;
}
.second-spinner {
-jfx-radius: 30;
}
@@ -5,6 +5,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
type="StackPane">
@@ -28,7 +29,7 @@
<Label fx:id="lblInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<JFXComboBox fx:id="cboServers" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<JFXComboBox fx:id="cboServers" promptText="%general.prompt.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Hyperlink fx:id="linkManageInjectorServers" text="%account.injector.manage"
onMouseClicked="#onManageInjecterServers" GridPane.columnIndex="2" GridPane.rowIndex="1"/>
@@ -36,7 +37,7 @@
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<JFXTextField fx:id="txtUsername" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.columnSpan="2"
FXUtils.validateWhileTextChanged="true">
FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
<validators>
<RequiredFieldValidator message="%input.not_empty">
</RequiredFieldValidator>
@@ -46,7 +47,7 @@
<Label fx:id="lblPassword" text="%account.password" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
<JFXPasswordField fx:id="txtPassword" GridPane.columnIndex="1" GridPane.rowIndex="3"
GridPane.columnSpan="2" FXUtils.validateWhileTextChanged="true">
GridPane.columnSpan="2" FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
<validators>
<RequiredFieldValidator message="%input.not_empty">
</RequiredFieldValidator>
@@ -56,10 +57,9 @@
</body>
<actions>
<Label fx:id="lblCreationWarning"/>
<StackPane fx:id="acceptPane">
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
<JFXSpinner fx:id="spinnerAccept" styleClass="small-spinner" />
</StackPane>
</SpinnerPane>
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
</actions>
</JFXDialogLayout>
@@ -2,6 +2,7 @@
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.ComponentList?>
<?import com.jfoenix.controls.JFXProgressBar?>
@@ -20,6 +21,9 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblType" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
@@ -33,6 +37,9 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblCharacter" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
@@ -46,6 +53,9 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblEmail" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
@@ -59,24 +69,26 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblServer" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
</BorderPane>
</ComponentList>
<BorderPane pickOnBounds="false" style="-fx-padding: 4;">
<left>
<JFXButton BorderPane.alignment="BOTTOM_LEFT" fx:id="btnDelete" onMouseClicked="#onDelete"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</left>
<right>
<JFXButton BorderPane.alignment="BOTTOM_RIGHT" fx:id="btnRefresh" onMouseClicked="#onRefresh"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</right>
</BorderPane>
</VBox>
<BorderPane pickOnBounds="false" style="-fx-padding: 4;">
<left>
<JFXButton BorderPane.alignment="BOTTOM_LEFT" fx:id="btnDelete" onMouseClicked="#onDelete"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</left>
<right>
<JFXButton BorderPane.alignment="BOTTOM_RIGHT" fx:id="btnRefresh" onMouseClicked="#onRefresh"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</right>
</BorderPane>
<JFXProgressBar fx:id="progressBar" StackPane.alignment="TOP_CENTER" visible="false" />
</fx:root>
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
<?import org.jackhuang.hmcl.ui.construct.URLValidator?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<StackPane fx:id="addServerContainer">
<JFXDialogLayout fx:id="addServerPane">
<heading>
<Label text="%account.injector.add" />
</heading>
<body>
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url" onAction="#onAddNext">
<validators>
<URLValidator message="%input.url">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
</protocols>
</URLValidator>
</validators>
</JFXTextField>
</body>
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
<SpinnerPane fx:id="nextPane" styleClass="small-spinner-pane">
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
</SpinnerPane>
</actions>
</JFXDialogLayout>
<JFXDialogLayout fx:id="confirmServerPane">
<heading>
<Label text="%account.injector.add" />
</heading>
<body>
<GridPane vgap="15" hgap="15" style="-fx-padding: 15 0 0 0;">
<columnConstraints>
<ColumnConstraints maxWidth="100" />
<ColumnConstraints />
</columnConstraints>
<Label text="%account.injector.server_url" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="%account.injector.server_name" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label fx:id="lblServerUrl" GridPane.columnIndex="1" GridPane.rowIndex="0" />
<Label fx:id="lblServerName" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label fx:id="lblServerWarning" text="%account.injector.http" style="-fx-text-fill: red;"
GridPane.rowIndex="2" GridPane.columnSpan="2" GridPane.columnIndex="0" />
</GridPane>
</body>
<actions>
<JFXButton onMouseClicked="#onAddPrev" text="%wizard.prev" styleClass="dialog-cancel" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
<JFXButton onMouseClicked="#onAddFinish" text="%wizard.finish" styleClass="dialog-accept" />
</actions>
</JFXDialogLayout>
</StackPane>
</fx:root>
@@ -1,14 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.URLValidator?>
<?import java.lang.String?>
<?import com.jfoenix.controls.*?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<JFXSpinner fx:id="spinner" style="-fx-radius:16" styleClass="materialDesign-purple, first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fx:id="scrollPane" fitToWidth="true" fitToHeight="true">
<VBox fx:id="listPane" spacing="10" style="-fx-padding: 20 20 70 20;">
@@ -23,62 +21,4 @@
</JFXButton>
</VBox>
</StackPane>
<JFXDialog fx:id="dialog">
<StackPane>
<StackPane fx:id="addServerContainer">
<JFXDialogLayout fx:id="addServerPane">
<heading>
<Label text="%account.injector.add" />
</heading>
<body>
<JFXTextField fx:id="txtServerIp" promptText="%account.injector.server_ip">
<validators>
<URLValidator message="%input.url">
</URLValidator>
<URLValidator message="%input.url.http">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
</protocols>
</URLValidator>
</validators>
</JFXTextField>
</body>
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
</actions>
</JFXDialogLayout>
<JFXDialogLayout fx:id="confirmServerPane">
<heading>
<Label text="%account.injector.add" />
</heading>
<body>
<GridPane vgap="15" hgap="15" style="-fx-padding: 15 0 0 0;">
<columnConstraints>
<ColumnConstraints maxWidth="100" />
<ColumnConstraints />
</columnConstraints>
<Label text="%account.injector.server_ip" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="%account.injector.server_name" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label fx:id="lblServerIp" GridPane.columnIndex="1" GridPane.rowIndex="0" />
<Label fx:id="lblServerName" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label fx:id="lblServerWarning" text="%account.injector.http" style="-fx-text-fill: red;" GridPane.rowIndex="2" GridPane.columnSpan="2" GridPane.columnIndex="0" />
</GridPane>
</body>
<actions>
<JFXButton onMouseClicked="#onAddPrev" text="%wizard.prev" styleClass="dialog-cancel" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
<JFXButton onMouseClicked="#onAddFinish" text="%wizard.finish" styleClass="dialog-accept" />
</actions>
</JFXDialogLayout>
</StackPane>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER" />
</StackPane>
</JFXDialog>
</fx:root>
@@ -8,6 +8,8 @@
<?import org.jackhuang.hmcl.ui.construct.AdvancedListBox?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import java.lang.String?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<fx:root xmlns="http://javafx.com/javafx"
type="StackPane"
xmlns:fx="http://javafx.com/fxml">
@@ -44,6 +46,9 @@
</StackPane>
</center>
</BorderPane>
<ImageView fx:id="welcomeView">
<Image url="/assets/img/welcome.png" />
</ImageView>
</StackPane>
</center>
<top>
@@ -5,6 +5,7 @@
<?import com.jfoenix.controls.JFXTextField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
@@ -16,8 +17,11 @@
<JFXTextField fx:id="textField" />
</body>
<actions>
<Label fx:id="lblCreationWarning"/>
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
<JFXButton fx:id="acceptButton" text="%button.ok" styleClass="dialog-accept"/>
</SpinnerPane>
<JFXButton fx:id="cancelButton" styleClass="dialog-cancel" text="%button.cancel" />
<JFXButton fx:id="acceptButton" styleClass="dialog-accept" text="%button.ok" />
</actions>
</JFXDialogLayout>
</fx:root>
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXCheckBox?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.validation.RequiredFieldValidator?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.*?>
<?import org.jackhuang.hmcl.ui.construct.ComponentList?>
<?import org.jackhuang.hmcl.ui.construct.FileItem?>
<fx:root xmlns="http://javafx.com/javafx"
@@ -32,6 +34,12 @@
</BorderPane>
<FileItem fx:id="gameDir" name="%profile.instance_directory" title="%profile.instance_directory.choose"/>
<JFXCheckBox fx:id="toggleUseRelativePath" text="%profile.use_relative_path" StackPane.alignment="CENTER_LEFT">
<StackPane.margin>
<Insets left="-10"/>
</StackPane.margin>
</JFXCheckBox>
</ComponentList>
</VBox>
@@ -55,8 +55,7 @@
<ComponentList title="%settings.launcher.proxy"> <!-- proxy -->
<VBox spacing="10">
<JFXRadioButton fx:id="chkNoProxy" text="%settings.launcher.proxy.no_proxy" />
<JFXRadioButton fx:id="chkManualProxy" text="%settings.launcher.proxy.has_proxy" />
<JFXCheckBox fx:id="chkEnableProxy" text="%settings.launcher.proxy.enable" />
<VBox fx:id="proxyPane" style="-fx-padding: 0 0 0 30;">
<HBox>
<JFXRadioButton fx:id="chkProxyHttp" text="%settings.launcher.proxy.http" />
@@ -104,8 +104,8 @@
</right>
</BorderPane>
</ComponentList>
<HBox alignment="CENTER" style="-fx-padding: 10 0 10 0;">
<JFXButton text="%settings.advanced" onMouseClicked="#onShowAdvanced" buttonType="RAISED" styleClass="jfx-button-raised"/>
<HBox alignment="CENTER_LEFT" style="-fx-padding: 20 0 12 0;">
<Label text="%settings.advanced" style="-fx-text-fill: #616161;" />
</HBox>
<ComponentList fx:id="advancedSettingsPane" depth="1">
<JFXTextField labelFloat="true" promptText="%settings.advanced.jvm_args" styleClass="fit-width"
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

@@ -20,9 +20,10 @@ about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=Author
about.author.statement=huanghongxun ([email protected])
about.thanks_to=Thanks to
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi2.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors.
about.dependency=Dependency
about.dependency.statement=Gson (Google, Apache License 2.0)\nCommons Compress (Apache, Apache License 2.0)
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors who participated in this project via issues, pull requests, etc.
about.dependency=Dependencies
# Due to space limitations, only first authors are listed
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=Statement
about.claim.statement=Minecraft is copyrighted by Mojang AB. We are not responsible for any copyright infringement arising from the use of this software.
about.open_source=Open Source
@@ -31,26 +32,28 @@ about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=Accounts
account.character=character
account.choose=Choose a character
account.current=Current
account.create=Create a new account
account.email=Email
account.failed.connect_authentication_server=Cannot connect the authentication server. Check your network.
account.failed.invalid_credentials=Incorrect password. Or forbidden to log in temporarily.
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to log in.
account.failed.no_charactor=No character in this account.
account.injector.add=Add authentication server
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.no_character=No character in this account.
account.failed.no_selected_server=No authentication server is selected.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.injector.add=Add an authentication server
account.injector.manage=Manage authentication servers
account.injector.manage.title=Authentication servers
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
account.injector.server=Auth Server
account.injector.server_ip=Server URL
account.injector.server_url=Server URL
account.injector.server_name=Server Name
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.no_method=No login method
account.methods.offline=Offline
account.methods.yggdrasil=Mojang
account.missing=Missing account
account.missing=None
account.password=Password
account.username=Name
@@ -59,72 +62,39 @@ archive.game_version=Game
archive.version=Version
assets.download=Download assets
assets.download_all=Check for the completion of assets
assets.failed=Failed to get the list, try again.
assets.failed_download=Failed to download assets, may cause no sounds and language files.
assets.download_all=Check the integrity of assets
button.about=About
button.cancel=Cancel
button.clear=Clear
button.close=Close
button.copy=Copy
button.delete=Delete
button.download=Download
button.edit=Edit
button.explore=Explore
button.info=Info
button.install=Install
button.more=More
button.no=No
button.ok=OK
button.others=Others
button.preview=Preview
button.refresh=Refresh
button.retry=Retry
button.save=Save
button.test=Test
button.yes=Yes
color.blue=Blue
color.dark_blue=Dark Blue
color.green=Green
color.orange=Orange
color.purple=Purple
color.red=Red
color.recent=Recommended
color.custom=Custom Color
crash.NoClassDefFound=Please check "HMCL" software is complete.
crash.advice.ClassNotFoundException=Minecraft or mods are incomplete. Retry if there are some libraries that have not downloaded or update your game and mods! Or you can try Game Settings -> Manage (Version) -> Delete library files to solve the problem.
crash.advice.ConcurrentModificationException=Maybe your Java is newer than 1.8.0_11, you could downgrade to Java 7.
crash.advice.LWJGLException=Maybe your video driver does not work well, please update your video driver.
crash.advice.NoSuchFieldError=Minecraft or mods are incomplete. Retry if there are some libraries that have not downloaded or update your game and mods!
crash.advice.OpenGL=Maybe drivers caused problems.
crash.advice.OutOfMemoryError=The maximum memory of JVM is too small, please modify it.
crash.advice.SecurityException=Maybe you have modified minecraft.jar but have not removed the META-INF.
crash.advice.UnsupportedClassVersionError=Maybe your java is too old, try to update the java.
crash.advice.no=No advice.
crash.advice.no_lwjgl=Maybe drivers caused problems.
crash.advice.otherwise=Maybe mods caused problems.
crash.error=Minecraft has crashed.
crash.launcher=Launcher has crashed!
crash.minecraft=Minecraft has crashed!
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
download=Download
download.BMCL=BMCLAPI (bangbang93, http://bmclapi2.bangbang93.com/)
download.BMCL=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.failed=Failed to download
download.failed.refresh=Unable to load version list. Click here to retry.
download.mojang=Mojang
download.not_200=Failed to download, the response code is %s.
download.source=Download Source
download.successfully=Downloaded successfully
extension.bat=Windows Bat file
extension.mod=Mod file
extension.png=Image file
extension.sh=Bash shell
fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use HMCL, but HMCL will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause HMCL not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem.
folder.config=Configs
folder.coremod=Core Mod
folder.game=Game Dir
@@ -133,16 +103,16 @@ folder.resourcepacks=Resourcepacks
folder.saves=Saves
folder.screenshots=Screenshots
general.prompt.empty=(None)
input.email=The username must be an e-mail.
input.number=Must be a number.
input.not_empty=Input Requrired!
input.not_empty=Required field
input.url=Must be a valid URL.
input.url.http=Only Http or Https accepted.
install=Install New Game
install.failed=Failed to install
install.installer.choose=Choose a %s version
install.installer.failed=Unable to install %s.
install.installer.forge=Forge
install.installer.game=Game
install.installer.install=
@@ -150,41 +120,33 @@ install.installer.liteloader=LiteLoader
install.installer.not_installed=%s not Installed
install.installer.optifine=OptiFine
install.installer.version=%s Version
install.modpack=Install a modpack (CurseForge supported)
install.modpack=Install a modpack
install.new_game=Install a New Game
install.new_game.already_exists=This version has already been existing.
install.new_game.current_game_version=Current Game Version
install.no_version=The version is not found.
install.no_version_if_intall=The required version is not found, do you wish to install the version automatically?
install.release_time=Release Time
install.select=Select an operation
install.success=Installed successfully
install.time=Time
install.type=Type
lang=English
lang.default=Belong to OS language.
lang.default=Use system language
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or newer Java version.
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
launch.advice.newer_java=Java 8 is suggested, which can make game run more fluently. And many mods and Minecraft 1.12 and newer versions requires Java 8.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game probably crash. The launcher will try to launch it.
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game probably crash. The maximum memory is 1024MB. The launcher will try to launch it.
launch.circular_dependency_versions=Found circular dependency versions, please check if your client has been modified.
launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine cannot be created. Java arguments may have problems. You can enable the no args mode in the settings.
launch.failed.creating_process=Failed to create process, maybe your java path is wrong, please modify your java path.
launch.failed.decompressing_natives=Unable to decompress native libraries.
launch.failed.downloading_libraries=Did not finish downloading libraries, continue launching game?
launch.failed.executable_permission=Unable to add permission to the launch script
launch.failed.exited_abnormally=Game exited abnormally, please visit the log, or ask someone for help.
launch.failed.prelaunch_command=Prelaunch command fail.
launch.state.dependencies=Dependencies
launch.state.done=Done
launch.state.logging_in=Logging In
launch.state.modpack=Preparing for modpack
launch.state.waiting_launching=Waiting for game launching
launch.unsupported_launcher_version=Sorry, the launcher cannot launch minecraft, but will retry launching it.
launch.wrong_javadir=Incorrect Java directory, will reset to default Java directory.
launcher=Launcher
@@ -196,61 +158,31 @@ launcher.common_directory.choose=Choose common directory.
launcher.contact=Contact Us
launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
launcher.exit_failed=Failed to shutdown.
launcher.modpack=Documentations for modpacks.
launcher.restart=Options will be in operations only if restart this app.
launcher.tab.about=About
launcher.tab.general=General
launcher.tab.ui=UI
launcher.update_java=Please upgrade your Java.
launcher.update_launcher=Check for update
launcher.versions_json_not_formatted=The version information of %s is malformed! Redownload it?
launcher.versions_json_not_matched=The version %s is malformed! There are a json:%s in this version. Do you want to fix this problem?
launcher.versions_json_not_matched_cannot_auto_completion=The version %s lost version information file, delete it?
login.changed_client_token=The server response has changed the client token.
login.choose_charactor=Please choose the character you want
login.empty_username=You have not set a username!
login.enter_password=Please enter your password.
login.enter_username=Please enter your name.
login.failed=Failed to login
login.invalid_username=Invalid username
login.invalid_uuid_and_username=Invalid UUID and username
login.no_valid_character=No valid character, please visit skinme.cc and create your own character.
logwindow.search=Search
logwindow.show_lines=Show Lines
logwindow.terminate_game=Terminate Game
logwindow.title=Log
main_page=Home
message.cancelled=Cancelled
message.cannot_open_explorer=Cannot open explorer:
message.confirm=Confirm
message.doing=Please wait
message.downloading=Downloading...
message.error=Error
message.info=Info
message.loading=Loading...
message.success=Tasks succeeded
message.unknown=Unknown
minecraft.invalid=Invalid
minecraft.invalid_jar=Invalid Jar
minecraft.modified=(Modified!)
minecraft.not_a_file=Not a file
minecraft.not_found=Not found
minecraft.not_readable=Not readable
minecraft.wrong_path=Wrong Minecraft path, the launcher could not find the path.
modpack=Mod pack
modpack.choose=Choose a modpack zip file which you want to import. If you want to update the modpack, please enter the version you want to update.
modpack.choose=Choose a modpack zip file which you want to install.
modpack.desc=Describe your modpack, including precautions, changlog, supporting Markdown(also supporting online pictures).
modpack.enter_name=Enter your desired name for this game.
modpack.export=Export Modpack
modpack.export_error=Failed to export the modpack, maybe the format of your game directory is incorrect or failed to manage files.
modpack.export_finished=Exporting the modpack finished. See
modpack.files.blueprints=BuildCraft blueprints
modpack.files.config=Mod configs
modpack.files.dumps=NEI debug output
@@ -263,22 +195,20 @@ modpack.files.resourcepacks=Resource(Texture) packs
modpack.files.saves=Saved games
modpack.files.scripts=MineTweaker configuration
modpack.files.servers_dat=Multiplayer servers list
modpack.included_launcher=The modpack is included the launcher, you can publish it directly.
modpack.install=Install %s modpack
modpack.installing=Installing modpack
modpack.introduction=Curse, MultiMC, HMCL, MCBBS modpacks supprted.
modpack.invalid=Invalid modpack file.
modpack.mismatched_type=Mismatched modpack type, your current game is %s modpack, but your update file is %s modpack.
modpack.name=Modpack Name
modpack.not_a_valid_location=Not a valid modpack path
modpack.not_a_valid_name=Not a valid modpack name
modpack.not_included_launcher=You can use the modpack by clicking the "Import Modpack" button.
modpack.scan=Scanning this modpack
modpack.task.install=Import Modpack
modpack.task.install.error=Failed to install the modpack. Maybe the files is incorrect, or a management issue occurred.
modpack.task.install.will=Install the modpack:
modpack.type.curse=Curse
modpack.type.curse.completion=Install relative files to Curse modpack
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
@@ -289,7 +219,6 @@ modpack.wizard.step.1.title=Set the basic options to the modpack.
modpack.wizard.step.2=Files selection
modpack.wizard.step.2.title=Choose the files you want to put into the modpack.
modpack.wizard.step.3=Description
modpack.wizard.step.3.title=Describe your modpack.
modpack.wizard.step.initialization.exported_version=The exported game version
modpack.wizard.step.initialization.include_launcher=Include the launcher
modpack.wizard.step.initialization.save=Choose a path to export the game files to
@@ -300,11 +229,8 @@ mods.add=Add mods
mods.add.failed=Failed to add mods %s.
mods.add.success=Successfully added mods %s.
mods.choose_mod=Choose your mods
mods.default_information=Please ensure that you have installed Forge or LiteLoader before installing mods!\nYou can drop your mod files from explorer/finder, and delete mods by the delete button.\nDisable a mod by leaving the check box unchecked; Choose an item to get the information.
mods.remove=Remove
operation.confirm_stop=Terminate the operations?
operation.stopped=The operation was aborted.
profile=Profile
profile.default=Current directory
@@ -312,10 +238,9 @@ profile.home=User home
profile.instance_directory=Game Directory
profile.instance_directory.choose=Choose Game Directory
profile.new=New Config
profile.new_name=New Profile Name:
profile.remove=Sure to remove profile %s?
profile.title=Game Directories
profile.selected=Selected
profile.use_relative_path=Use relative path for game directory if possible
selector.choose=Choose
selector.choose_file=Select a file
@@ -323,7 +248,7 @@ selector.custom=Custom
settings=Settings
settings.advanced=Advanced
settings.advanced=Advanced Settings
settings.advanced.dont_check_game_completeness=Don't check game completeness
settings.advanced.game_dir.default=Default (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except assets,libraries)
@@ -342,10 +267,7 @@ settings.advanced.precall_command=Pre-Launch command(will be executed before gam
settings.advanced.server_ip=Server Host
settings.advanced.wrapper_launcher=Wrapper Launcher(i.e. optirun...)
settings.cannot_remove_default_config=Cannot remove the default configution.
settings.custom=Custom
settings.default=Default
settings.failed_load=Failed to load settings file. Remove it?
settings.game=Games
settings.game.dimension=Game Window Dimension
@@ -366,8 +288,7 @@ settings.launcher.language=Language
settings.launcher.log_font=Log Font
settings.launcher.proxy=Proxy
settings.launcher.proxy.authentication=Proxy Authentication
settings.launcher.proxy.no_proxy=No proxy
settings.launcher.proxy.has_proxy=Proxy configuration
settings.launcher.proxy.enable=Enable Proxy
settings.launcher.proxy.host=Host
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=Password
@@ -378,24 +299,18 @@ settings.launcher.theme=Theme
settings.max_memory=Max Memory/MB
settings.physical_memory=Physical Memory Size
settings.run_directory=Run Directory(Version Isolation)
settings.show_log=Show Logs
settings.tabs.assets_downloads=Assets
settings.tabs.game_download=Games
settings.tabs.installers=Installers
settings.test_game=Test game
settings.type.global=Global version settings(all shared)
settings.type=Version setting type
settings.type.special=Specialized version settings(will not affect other versions)
settings.update_version=Update version json.
update=Update
update.failed=Failed to check for updates.
update.found=Found Update!
update.newest_version=Newest version: %s
update.found=Update Available!
update.newest_version=Latest version: %s
update.latest=This is latest Version.
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
update.should_open_link=Are you willing to update the launcher?
update.tooltip=Update
version.cannot_read=Unable to gather the game version. Cannot continue auto-installing.
@@ -409,20 +324,16 @@ version.launch_script.failed=Unable to make launch script.
version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.redownload_json=Redownload Minecraft Configuration(minecraft.json)
version.manage.remove=Delete this version
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove version %s?
version.manage.remove_libraries=Delete library files
version.manage.rename=Rename this version
version.manage.rename=Rename this game
version.manage.rename.message=Please enter the new name
version.manage.rename.fail=Failed to rename this game.
version.settings=Settings
version.update=Update
wizard.prev=< Prev
wizard.close=Close
wizard.failed=Failed
wizard.finish=Finish
wizard.help=Help
wizard.next=Next >
wizard.steps=Steps
wizard.summary=Summary
@@ -0,0 +1,339 @@
# Hello Minecraft! Launcher.
# Copyright (C) 2013 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/}.
#
#author: huangyuhui
about.copyright=版權
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鳴謝
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默認背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.dependency=依賴
# 由於篇幅限制,僅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=免責聲明
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟件產生的版權問題,軟件製作方概不負責。請支持正版。
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=賬戶
account.character=角色
account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.no_character=該帳號沒有角色
account.failed.no_selected_server=未選擇認證服務器
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.manage.title=認證服務器
account.injector.http=警告:此服務器使用不安全的 HTTP 協議,您的密碼在登錄時會被明文傳輸。
account.injector.server=認證服務器
account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.methods.no_method=沒有登入方式
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
account.password=密碼
account.username=用戶名
archive.author=作者
archive.game_version=遊戲版本
archive.version=版本
assets.download=下載資源
assets.download_all=檢查資源文件完整性
button.cancel=取消
button.clear=清除
button.delete=刪除
button.edit=修改
button.install=安裝
button.no=否
button.ok=確定
button.refresh=刷新
button.save=保存
button.yes=是
color.recent=推薦
color.custom=自定義顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟件崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.refresh=加載版本列表失敗,點擊此處重試。
download.mojang=官方
extension.bat=Windows 腳本
extension.mod=模組文件
extension.png=圖片文件
extension.sh=Bash 腳本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=當前 Java 平台缺少 DST Root CA X3 證書。\n您依然可以使用 HMCL,但將無法連接到部分站點(如使用 Let's Encrypt 證書的站點),這可能會使 HMCL 無法正常工作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
folder.game=遊戲文件夾
folder.mod=MOD文件夾
folder.resourcepacks=資源包文件夾
folder.saves=存檔文件夾
folder.screenshots=截圖文件夾
general.prompt.empty=(無)
input.email=用戶名必須是郵箱
input.number=必須是數字
input.not_empty=必填項
input.url=必須是合法的鏈接
install=添加遊戲
install.failed=安裝失敗
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
install.installer.install=安裝 %s
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
install.installer.version=%s 版本
install.modpack=安裝整合包
install.new_game=安裝新遊戲版本
install.new_game.already_exists=此版本已經存在,請換一個名字
install.new_game.current_game_version=當前遊戲版本
install.select=請選擇安裝方式
install.success=安裝成功
lang=繁體中文
lang.default=跟隨系統語言
launch.advice.different_platform=您的系統是 64 位的但是 Java 是 32 位的,推薦您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java9=不高於 1.13 的安裝了 Mod 的 Minecraft 不支持 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=檢測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更流暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要需要 Java 8。
launch.advice.not_enough_space=您設置的內存大小過大,由於超過了系統內存大小 %dMB,所以可能影響遊戲體驗或無法啓動遊戲,啓動器仍會嘗試啓動。
launch.advice.too_large_memory_for_32bit=您設置的內存大小過大,由於可能超過了 32 位 Java 的內存分配限制,所以可能無法啓動遊戲,請將內存調至 1024MB 或更小,啓動器仍會嘗試啓動。
launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.state.dependencies=正在處理遊戲依賴
launch.state.done=啓動完成
launch.state.logging_in=登錄中
launch.state.modpack=正在下載必要文件
launch.state.waiting_launching=等待遊戲啓動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重置爲默認 Java 路徑。
launcher=啓動器
launcher.background=背景地址
launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
launcher.update_java=請更新您的 Java
login.empty_username=你還未設置用戶名!
login.enter_password=請輸入您的密碼
logwindow.show_lines=顯示行數
logwindow.terminate_game=結束遊戲進程
logwindow.title=日誌
main_page=主頁
message.confirm=提示
message.doing=請耐心等待
message.downloading=正在下載...
message.error=錯誤
message.info=提示
message.success=已完成
message.unknown=未知
modpack=整合包
modpack.choose=選擇要安裝的遊戲整合包文件
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支持 Markdown(圖片請用網絡圖)。
modpack.enter_name=給遊戲起個你喜歡的名字
modpack.export=導出整合包
modpack.files.blueprints=BuildCraft 藍圖
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 調試輸出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods 配置,如小地圖
modpack.files.options_txt=遊戲設定
modpack.files.optionsshaders_txt=光影設定
modpack.files.resourcepacks=資源包(材質包)
modpack.files.saves=遊戲存檔
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人遊戲服務器列表
modpack.install=安裝%s整合包
modpack.installing=正在安裝整合包
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級文件,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,當前遊戲是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.name=整合包名稱
modpack.not_a_valid_name=不是一個有效的整合包名稱
modpack.scan=解析整合包
modpack.task.install=導入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式不正確或操作文件失敗。
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=該整合包不被支持。僅 HMCL、MultiMC、Curse 整合包受支持。
modpack.update=正在升級整合包
modpack.wizard=導出整合包嚮導
modpack.wizard.step.1=基本設置
modpack.wizard.step.1.title=設置整合包的主要信息
modpack.wizard.step.2=文件選擇
modpack.wizard.step.2.title=選中你想加到整合包中的文件或文件夾
modpack.wizard.step.3=整合包描述
modpack.wizard.step.initialization.exported_version=要導出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啓動器
modpack.wizard.step.initialization.save=選擇要導出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啓動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應當將不允許非官方途徑傳播的 Mod、材質包等納入整合包。\n整合包會保存您目前的下載源設置
mods=模組管理
mods.add=添加模組
mods.add.failed=添加模組 %s 失敗。
mods.add.success=成功添加模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
profile=配置
profile.default=當前目錄
profile.home=主文件夾
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.title=遊戲目錄
profile.selected=已選中
profile.use_relative_path=若可能,遊戲目錄使用相對路徑
selector.choose=選擇
selector.choose_file=選擇文件
selector.custom=自定義
settings=普通設置
settings.advanced=高級設置
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.game_dir.default=默認(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=啓動器默認添加的參數(請不要重複添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=內存永久保存區域(不必填寫,MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.launcher_visibility.close=遊戲啓動後結束啓動器
settings.advanced.launcher_visibility.hide=遊戲啓動後隱藏啓動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啓動器並在遊戲結束後重新打開
settings.advanced.launcher_visibility.keep=保持啓動器可見
settings.advanced.launcher_visible=啓動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.no_common=不使用公共路徑
settings.advanced.no_jvm_args=不添加默認的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啓動前執行命令(不必填寫,將在遊戲啓動前調用)
settings.advanced.server_ip=直入服務器 IP 地址(不必填寫,啓動遊戲後直接進入對應服務器)
settings.advanced.wrapper_launcher=前置指令(不必填寫,如 optirun)
settings.custom=自定義
settings.game=遊戲設置
settings.game.dimension=遊戲窗口分辨率
settings.game.exploration=瀏覽
settings.game.fullscreen=全屏
settings.game.game_directory=遊戲路徑(版本隔離)
settings.game.game_directory.choose=選擇遊戲路徑
settings.game.java_directory=Java 路徑
settings.game.java_directory.choose=選擇 Java 路徑
settings.game.management=管理
settings.icon=遊戲圖標
settings.launcher=啓動器設置
settings.launcher.common_path.tooltip=啓動器將所有遊戲資源及依賴庫文件放於此集中管理,如果遊戲文件夾內有現成的將不會使用公共庫文件
settings.launcher.download_source=下載源
settings.launcher.language=語言
settings.launcher.log_font=日誌字體
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=身份驗證
settings.launcher.proxy.enable=启用代理
settings.launcher.proxy.host=主機
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=端口
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=賬戶
settings.launcher.theme=主題
settings.max_memory=最大內存(MB)
settings.physical_memory=物理內存大小
settings.show_log=查看日誌
settings.tabs.installers=自動安裝
settings.type.global=全局版本設置(使用該設置的版本共用一套設定)
settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
update=啓動器更新
update.failed=檢查更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
update.latest=當前版本爲最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼板了,您可以手動粘貼網址打開頁面
update.tooltip=更新
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
version.forbidden_name=此版本名稱不受支持,請換一個名字
version.game.old=遠古版
version.game.release=穩定版
version.game.snapshot=測試版
version.launch=啓動遊戲
version.launch_script=生成啓動腳本
version.launch_script.failed=生成啓動腳本失敗
version.launch_script.save=保存啓動腳本
version.launch_script.success=啓動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源配置(assets_index.json)
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.manage.rename.fail=重命名版本失敗,可能文件被佔用或者名字有特殊字符
version.settings=遊戲設置
version.update=更新
wizard.prev=< 上一步
wizard.failed=失敗
wizard.finish=完成
wizard.next=下一步 >
@@ -20,9 +20,10 @@ about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鸣谢
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有参与本项目,issue,pull requests的贡献者
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有通过 Issues、Pull Requests 等方式参与本项目的贡献者
about.dependency=依赖
about.dependency.statement=Gson (Google, Apache License 2.0)\nCommons Compress (Apache, Apache License 2.0)
# 由于篇幅限制,仅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=免责声明
about.claim.statement=Minecraft 版权归 Mojang AB 所有,使用本软件产生的版权问题,软件制作方概不负责。请支持正版。
about.open_source=开源
@@ -31,19 +32,21 @@ about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=账户
account.character=角色
account.choose=选择一个角色
account.current=当前账户
account.create=新建账户
account.email=邮箱
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_charactor=该帐号没有角色
account.failed.no_character=该帐号没有角色
account.failed.no_selected_server=未选择认证服务器
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
account.injector.http=警告:此服务器使用不安全的http协议,您的密码在登录时会被明文传输。
account.injector.manage.title=认证服务器
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
account.injector.server=认证服务器
account.injector.server_ip=服务器地址
account.injector.server_url=服务器地址
account.injector.server_name=服务器名称
account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
@@ -60,71 +63,38 @@ archive.version=版本
assets.download=下载资源
assets.download_all=检查资源文件完整性
assets.failed=获取列表失败,请刷新重试。
assets.failed_download=下载资源文件失败,可能导致没有中文和声音。
button.about=关于
button.cancel=取消
button.clear=清除
button.close=关闭
button.copy=复制
button.delete=删除
button.download=下载
button.edit=修改
button.explore=浏览
button.info=信息
button.install=安装
button.more=更多
button.no=否
button.ok=确定
button.others=其他
button.preview=预览
button.refresh=刷新
button.retry=重试
button.save=保存
button.test=测试
button.yes=是
color.blue=蓝色
color.dark_blue=深蓝色
color.green=绿色
color.orange=橙色
color.purple=紫色
color.red=红色
color.recent=推荐
color.custom=自定义颜色
crash.NoClassDefFound=请确认HMCL本体是否完整,或更新您的Java。
crash.advice.ClassNotFoundException=Minecraft不完整或Mod冲突,如果有未能下载的文件请下载成功后重试或是客户端损坏请重试请重新制作客户端或下载整合包解决问题,另可尝试游戏设置->(版本)管理->删除库文件解决问题
crash.advice.ConcurrentModificationException=这可能是因为您的Java版本高于Java 1.8.0_11导致的,可以尝试卸载Java8安装Java7。
crash.advice.LWJGLException=您的电脑不正常,可能需要使用驱动精灵或其他安装器更新显卡驱动。
crash.advice.NoSuchFieldError=Minecraft不完整或Mod冲突,如果有未能下载的文件请下载成功后重试或是客户端损坏请重试请重新制作客户端或下载整合包解决问题。
crash.advice.OpenGL=可能是显卡/声卡驱动问题,也可能是Mod导致的问题。
crash.advice.OutOfMemoryError=内存溢出,您设置的Minecraft最大内存过小,请调大!
crash.advice.SecurityException=可能是您修改了minecraft.jar但未删除META-INF文件夹的原因。请通过压缩软件删除jar中的META-INF文件夹。
crash.advice.UnsupportedClassVersionError=这可能是因为您的Java版本过于老旧,可以尝试更换最新Java并在版本设置的Java路径中设置.
crash.advice.no=无建议。
crash.advice.no_lwjgl=可能是游戏依赖库不完整或解压依赖库时出错。可以通过下载整合包解决问题。
crash.advice.otherwise=可能是Mod或其他问题。
crash.error=您的Minecraft崩溃了。
crash.launcher=启动器崩溃了!
crash.minecraft=Minecraft崩溃了!请认真阅读建议。
crash.user_fault=您的系统或Java环境可能安装不当导致本软件崩溃,请检查您的Java环境或您的电脑!可以尝试重新安装Java。
crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新您的 Java。
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
download=下载
download.BMCL=BMCLAPI (bangbang93, http://bmclapi2.bangbang93.com/)
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方
download.not_200=下载失败,HTTP状态码:%s.
download.source=下载源
download.successfully=下载完成
extension.bat=Windows 脚本
extension.mod=模组文件
extension.png=图片文件
extension.sh=Bash 脚本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Let's Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
folder.config=配置文件夹
folder.coremod=核心MOD文件夹
folder.game=游戏文件夹
@@ -133,178 +103,134 @@ folder.resourcepacks=资源包文件夹
folder.saves=存档文件夹
folder.screenshots=截图文件夹
general.prompt.empty=(无)
input.email=用户名必须是邮箱
input.number=必须是数字
input.not_empty=必填项
input.url=必须是合法的链接
input.url.http=只支持Http或Https协议
install=添加游戏
install.failed=安装失败
install.installer.choose=选择%s版本
install.installer.failed=未能成功安装%s.
install.installer.choose=选择 %s 版本
install.installer.forge=Forge
install.installer.game=游戏
install.installer.install=安装%s
install.installer.install=安装 %s
install.installer.liteloader=LiteLoader
install.installer.not_installed=暂不安装%s,可以点击此处安装
install.installer.not_installed=暂不安装 %s,可以点击此处安装
install.installer.optifine=OptiFine
install.installer.version=%s版本
install.installer.version=%s 版本
install.modpack=安装整合包
install.new_game=安装新游戏版本
install.new_game.already_exists=此版本已经存在,请换一个名字
install.new_game.current_game_version=当前游戏版本
install.no_version=未找到要安装的对应MC版本
install.no_version_if_intall=未找到要安装的对应MC版本,是否自动安装需要的MC版本?
install.release_time=发布时间
install.select=请选择安装方式
install.success=安装成功
install.time=时间
install.type=类型
lang=简体中文
lang.default=跟随系统语言
launch.advice.different_platform=您的系统是64位的但是Java是32位的,推荐您安装64位Java.
launch.advice.java9=不高于1.13的Minecraft不支持Java 9或更高版本,请使用Java 8.
launch.advice.newer_java=检测到您未使用Java 8及更新版本,Java 8能使游戏更流畅而且Minecraft 1.12及更新版本和很多Mod强制需要需要Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小%dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了32位Java的内存分配限制,所以可能无法启动游戏,请将内存调至1024MB或更小,启动器仍会尝试启动。
launch.circular_dependency_versions=发现游戏版本循环引用,请确认您的客户端未被修改或修改导致出现此问题。
launch.advice.different_platform=您的系统是 64 位的但是 Java 是 32 位的,推荐您安装 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java9=不高于 1.13 的安装了 Mod 的 Minecraft 不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建Java虚拟机,可能是Java参数有问题,可以在设置中开启无参数模式启动
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是Java路径错误。
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.decompressing_natives=未能解压游戏本地库。
launch.failed.downloading_libraries=未完成游戏依赖库的下载,还要继续启动游戏吗?
launch.failed.executable_permission=未能为启动文件添加执行权限
launch.failed.executable_permission=未能为启动文件添加执行权限。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.failed.prelaunch_command=启动前执行命令执行失败。
launch.state.dependencies=正在处理游戏依赖
launch.state.done=启动完成
launch.state.logging_in=登录中
launch.state.modpack=正在下载必要文件
launch.state.waiting_launching=等待游戏启动
launch.unsupported_launcher_version=对不起,本启动器现在可能不能启动这个版本的Minecraft,但启动器还是会尝试启动,请尽快将此问题报告给作者。
launch.wrong_javadir=错误的Java路径,将自动重置为默认Java路径。
launch.wrong_javadir=错误的 Java 路径,将自动重置为默认 Java 路径。
launcher=启动器
launcher.background=背景地址
launcher.background.choose=选择背景路径
launcher.background.default=默认(自动检索启动器同目录下的background.png/jpg及bg文件夹内的图片)
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
launcher.common_directory=公共文件夹
launcher.common_directory.choose=选择公共文件夹
launcher.contact=联系我们
launcher.crash=Hello Minecraft!遇到了无法处理的错误,请复制下列内容并通过mcbbs、贴吧、Github或Minecraft Forum反馈bug。
launcher.crash_out_dated=Hello Minecraft! Launcher遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
launcher.exit_failed=强制退出失败,可能是Forge 1.7.10及更高版本导致的,无法解决。
launcher.modpack=整合包作者帮助
launcher.restart=本界面选项需要重启本启动器生效
launcher.tab.about=关于
launcher.tab.general=通用
launcher.tab.ui=界面
launcher.update_java=请更新您的Java
launcher.update_launcher=检查更新
launcher.versions_json_not_formatted=版本%s信息文件格式错误,是否重新下载?
launcher.versions_json_not_matched=版本%s格式不规范!该版本文件夹下有json:%s,是否更名这个文件来规范格式?
launcher.versions_json_not_matched_cannot_auto_completion=版本%s缺失必要的版本信息文件,是否删除该版本?
launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误,请复制下列内容并通过 MCBBS、贴吧、GitHub 或 Minecraft Forum 反馈 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
launcher.update_java=请更新您的 Java
login.changed_client_token=服务器回应已经修改客户端令牌
login.choose_charactor=请选择您要使用的角色
login.empty_username=你还未设置用户名!
login.enter_password=请输入您的密码
login.enter_username=请输入您的账号
login.failed=登录失败:
login.invalid_username=无效的用户名
login.invalid_uuid_and_username=无效的UUID和用户名
login.no_valid_character=无有效的角色,自行到skinme.cc登陆并创建角色
logwindow.search=查找
logwindow.show_lines=显示行数
logwindow.terminate_game=结束游戏进程
logwindow.title=日志
main_page=主页
message.cancelled=已取消
message.cannot_open_explorer=无法打开文件管理器:
message.confirm=提示
message.doing=请耐心等待
message.downloading=正在下载...
message.error=错误
message.info=提示
message.loading=加载中...
message.success=已完成
message.unknown=未知
minecraft.invalid=无效的
minecraft.invalid_jar=无效的jar包
minecraft.modified=(修改的!)
minecraft.not_a_file=不是文件
minecraft.not_found=找不到minecraft.jar
minecraft.not_readable=minecraft.jar不可读
minecraft.wrong_path=错误的Minecraft路径,启动器未找到设定的Minecraft路径,请检查。
modpack=整合包
modpack.choose=选择要导入的游戏整合包文件,如果您希望更新整合包,请输入要更新的版本名
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持Markdown(图片请用网络图片)。
modpack.choose=选择要安装的游戏整合包文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 Markdown(图片请用网络图)。
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
modpack.export_error=导出失败,可能是您的游戏文件夹格式不正确或操作文件失败
modpack.export_finished=整合包导出完成,参见
modpack.files.blueprints=BuildCraft蓝图
modpack.files.config=Mod配置文件
modpack.files.dumps=NEI调试输出
modpack.files.liteconfig=Mod配置文件
modpack.files.blueprints=BuildCraft 蓝图
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 调试输出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods配置,如小地图
modpack.files.mods.voxelmods=VoxelMods 配置,如小地图
modpack.files.options_txt=游戏设定
modpack.files.optionsshaders_txt=光影设定
modpack.files.resourcepacks=资源包(材质包)
modpack.files.saves=游戏存档
modpack.files.scripts=MineTweaker配置
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人游戏服务器列表
modpack.included_launcher=整合包已包含启动器,可直接发布
modpack.install=安装%s整合包
modpack.installing=正在安装整合包
modpack.introduction=支持 Curse, MultiMC, HMCL, MCBBS 整合包。
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=无效的整合包升级文件,可能是下载时出现问题。
modpack.mismatched_type=整合包类型不匹配,当前游戏是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.name=整合包名称
modpack.not_a_valid_location=不是一个有效整合包位置
modpack.not_a_valid_name=不是一个有效的整合包名称
modpack.not_included_launcher=整合包未包含启动器,可使用本软件的导入整合包功能导入整合包
modpack.scan=解析整合包
modpack.task.install=导入整合包
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败。
modpack.task.install.will=将会安装整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下载Curse整合包相关文件
modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=该整合包不被支持。仅HMCL、MultiMC、Curse整合包受支持。
modpack.unsupported=该整合包不被支持。仅 HMCL、MultiMC、Curse 整合包受支持。
modpack.update=正在升级整合包
modpack.wizard=导出整合包向导
modpack.wizard.step.1=基本设置
modpack.wizard.step.1.title=设置整合包的主要信息
modpack.wizard.step.2=文件选择
modpack.wizard.step.2.title=选中你想加到整合包中的文件(夹)
modpack.wizard.step.2.title=选中你想加到整合包中的文件或文件夹
modpack.wizard.step.3=整合包描述
modpack.wizard.step.3.title=描述你要制作的整合包
modpack.wizard.step.initialization.exported_version=要导出的游戏版本
modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的Minecraft是正式版而非快照版,\n而且不应当将不允许非官方途径传播的Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置
modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置
mods=模组管理
mods.add=添加模组
mods.add.failed=添加模组%s失败。
mods.add.success=成功添加模组%s。
mods.add.failed=添加模组 %s 失败。
mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.default_information=安装Mod前你需要确保已安装Forge或LiteLoader!\n您可以从资源管理器拖动mod文件到列表中来添加mod,同时使用删除键可快速删除选中mod\n点掉mod前面的勾可禁用mod,不会加载;选择mod可以获取mod信息
mods.remove=删除
operation.confirm_stop=真的要终止操作吗?
operation.stopped=操作被强行终止
profile=配置
profile.default=当前目录
@@ -312,10 +238,9 @@ profile.home=主文件夹
profile.instance_directory=游戏路径
profile.instance_directory.choose=选择游戏路径
profile.new=新建配置
profile.new_name=新配置名:
profile.remove=真的要删除配置%s吗?
profile.title=游戏目录
profile.selected=已选中
profile.use_relative_path=若可能,游戏目录使用相对路径
selector.choose=选择
selector.choose_file=选择文件
@@ -325,27 +250,24 @@ settings=普通设置
settings.advanced=高级设置
settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除assets,libraries)
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=启动器默认添加的参数(请不要重复添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.jvm_args=Java虚拟机参数(不必填写)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
settings.advanced.launcher_visibility.hide_reopen=隐藏启动器并在游戏结束后重新打开
settings.advanced.launcher_visibility.keep=保持启动器可见
settings.advanced.launcher_visible=启动器可见性
settings.advanced.minecraft_arguments=Minecraft额外参数(不必填写)
settings.advanced.no_common=不使用公共路径
settings.advanced.no_jvm_args=不添加默认的JVM参数(使用Java9时必勾)
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
settings.advanced.server_ip=直入服务器ip地址(不必填写,启动游戏后直接进入对应服务器)
settings.advanced.wrapper_launcher=前置指令(不必填写,如optirun)
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
settings.advanced.no_common=不使用公共路径(游戏依赖文件存放在公共路径)
settings.advanced.no_jvm_args=不添加默认的 JVM 参数(使用 Java 9 时必勾)
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
settings.advanced.server_ip=直入服务器 IP 地址(不必填写,启动游戏后直接进入对应服务器)
settings.advanced.wrapper_launcher=前置指令(不必填写,如 optirun)
settings.cannot_remove_default_config=不能删除默认配置
settings.custom=自定义
settings.default=默认
settings.failed_load=设置文件加载失败,可能是升级了启动器或被人工修改造成错误,是否清除?
settings.game=游戏设置
settings.game.dimension=游戏窗口分辨率
@@ -353,8 +275,8 @@ settings.game.exploration=浏览
settings.game.fullscreen=全屏
settings.game.game_directory=游戏路径(版本隔离)
settings.game.game_directory.choose=选择游戏路径
settings.game.java_directory=Java路径
settings.game.java_directory.choose=选择Java路径
settings.game.java_directory=Java 路径
settings.game.java_directory.choose=选择 Java 路径
settings.game.management=管理
settings.icon=游戏图标
@@ -365,9 +287,8 @@ settings.launcher.download_source=下载源
settings.launcher.language=语言
settings.launcher.log_font=日志字体
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=代理账户
settings.launcher.proxy.no_proxy=直连
settings.launcher.proxy.has_proxy=启用验证
settings.launcher.proxy.authentication=身份验证
settings.launcher.proxy.enable=启用代理
settings.launcher.proxy.host=主机
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密码
@@ -376,18 +297,13 @@ settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=账户
settings.launcher.theme=主题
settings.max_memory=最大内存/MB
settings.max_memory=最大内存(MB)
settings.physical_memory=物理内存大小
settings.run_directory=运行路径(版本隔离)
settings.show_log=查看日志
settings.tabs.assets_downloads=资源下载
settings.tabs.game_download=游戏下载
settings.tabs.installers=自动安装
settings.test_game=测试游戏
settings.type.global=全局版本设置(使用该设置的版本共用一套设定)
settings.type=版本设置类型
settings.type.special=单独版本设置(不会影响到其他版本的设定)
settings.update_version=更新版本文件
update=启动器更新
update.failed=检查更新失败
@@ -395,7 +311,6 @@ update.found=发现更新
update.newest_version=最新版本为:%s
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
update.should_open_link=是否更新?
update.tooltip=更新
version.cannot_read=读取游戏版本失败,无法进行自动安装
@@ -407,22 +322,18 @@ version.launch=启动游戏
version.launch_script=生成启动脚本
version.launch_script.failed=生成启动脚本失败
version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕: %s.
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.redownload_json=重新下载版本配置(minecraft.json)
version.launch_script.success=启动脚本已生成完毕:%s
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?
version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本
version.manage.rename.message=请输入要改成的名字
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符
version.settings=游戏设置
version.update=更新
wizard.prev=< 上一步
wizard.close=关闭
wizard.failed=失败
wizard.finish=完成
wizard.help=帮助
wizard.next=下一步 >
wizard.steps=步骤
wizard.summary=概要
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff. Show more