Compare commits

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

No files matched your search

Binary file not shown.
+23 -68
View File
@@ -1,8 +1,14 @@
import org.apache.tools.ant.filters.ReplaceTokens
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'
}
@@ -15,11 +21,9 @@ if (buildnumber == null)
def versionroot = System.getenv("VERSION_ROOT")
if (versionroot == null)
versionroot = "3.0"
versionroot = "3.1"
String mavenGroupId = 'HMCL'
String mavenVersion = versionroot + '.' + buildnumber
String bundleName = "Hello Minecraft! Launcher"
version = mavenVersion
dependencies {
@@ -30,7 +34,7 @@ dependencies {
task generateSources(type: Sync) {
from 'src/main/java'
into "$buildDir/generated-src"
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [
filter(ReplaceTokens, tokens: [
'HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING': mavenVersion
])
}
@@ -47,17 +51,12 @@ jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
manifest {
attributes 'Created-By' : 'Copyright(c) 2013-2017 huangyuhui.',
attributes 'Created-By' : 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class' : mainClass,
'Multi-Release': "true"
}
doLast {
new File("build/signed").mkdirs()
ant.signjar(signedjar: archivePath, jar: archivePath,
keystore: "HMCL.keystore", storepass: "123456",
alias: "HMCL")
def messageDigest = MessageDigest.getInstance("SHA1")
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
messageDigest.update(buf, 0, bytesRead)
@@ -75,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 = ''
@@ -122,41 +97,21 @@ task makePackGZ(dependsOn: jar) doLast {
fileEx.append sha1Hex
}
task proguard(type: proguard.gradle.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 <hmcl@huangyuhui.net>"
}
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>;
}
@@ -0,0 +1,154 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.AppDataUpgrader;
import org.jackhuang.hmcl.upgrade.IUpgrader;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public final class Launcher extends Application {
@Override
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UPDATE_CHECKER.process(false)
.then(Task.of(Schedulers.javafx(), () -> {
if (UPDATE_CHECKER.isOutOfDate())
Controllers.showUpdate();
}))
.start();
primaryStage.show();
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
}
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
if (!FileUtils.makeDirectory(LOG_DIRECTORY))
System.out.println("Unable to create log directory " + LOG_DIRECTORY + ", log files cannot be generated.");
try {
Logging.start(LOG_DIRECTORY);
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.LOG.info("*** " + TITLE + " ***");
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.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
}
public static void stopApplication() {
Logging.LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Platform.exit();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
for (URL u : urls)
try {
File f = new File(u.toURI());
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
result.add(f);
} catch (URISyntaxException e) {
return null;
}
}
if (result.isEmpty())
return null;
else
return result;
}
public static 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 = 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 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 = "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";
}
+63 -103
View File
@@ -17,126 +17,86 @@
*/
package org.jackhuang.hmcl;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.AppDataUpgrader;
import org.jackhuang.hmcl.upgrade.IUpgrader;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.Arrays;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
public final class Main extends Application {
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.JOptionPane;
@Override
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
primaryStage.show();
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
}
public final class Main {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
Launcher.main(args);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDirectory);
}
}
private static void checkJavaFX() {
try {
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.LOG.info("*** " + TITLE + " ***");
UPDATE_CHECKER.process(false)
.then(Task.of(Schedulers.javafx(), () -> {
if (UPDATE_CHECKER.isOutOfDate())
Controllers.showUpdate();
}))
.start();
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit(i18n("fatal.missing_javafx"));
}
}
public static void stopApplication() {
JFXUtilities.runInFX(() -> {
stopWithoutPlatform();
Platform.exit();
});
}
public static void stopWithoutPlatform() {
JFXUtilities.runInFX(() -> {
Controllers.getStage().close();
Logging.LOG.info("Shutting down executor services.");
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static String i18n(String key) {
private static void checkDSTRootCAX3() {
TrustManagerFactory tmf;
try {
return RESOURCE_BUNDLE.getString(key);
} catch (Exception e) {
Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
return key;
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;
}
}
public static String i18n(String key, Object... formatArgs) {
return String.format(i18n(key), formatArgs);
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
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"));
}
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = getWorkingDirectory("hmcl");
/**
* 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.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
public static final IUpgrader UPGRADER = new AppDataUpgrader();
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
/**
* 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);
}
public static final String CONTACT = "http://huangyuhui.duapp.com/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when added accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountAddedEvent extends Event {
private final Account account;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountAddedEvent(Object source, Account account) {
super(source);
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("account", account)
.toString();
}
}
@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountLoadingEvent extends Event {
private final Collection<Account> accounts;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountLoadingEvent(Object source, Collection<Account> accounts) {
super(source);
this.accounts = Collections.unmodifiableCollection(accounts);
}
public Collection<Account> getAccounts() {
return accounts;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("accounts", accounts)
.toString();
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.VersionNumber;
/**
@@ -42,4 +43,11 @@ public final class OutOfDateEvent extends Event {
return true;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", getSource())
.append("version", getVersion())
.toString();
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when the selected profile changed.
@@ -44,4 +45,11 @@ public final class ProfileChangedEvent extends Event {
return profile;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("profile", profile)
.toString();
}
}
@@ -17,6 +17,12 @@
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading profiles.
* <br>
@@ -26,14 +32,29 @@ package org.jackhuang.hmcl.event;
*/
public class ProfileLoadingEvent extends Event {
private final Collection<Profile> profiles;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public ProfileLoadingEvent(Object source) {
public ProfileLoadingEvent(Object source, Collection<Profile> profiles) {
super(source);
this.profiles = Collections.unmodifiableCollection(profiles);
}
public Collection<Profile> getProfiles() {
return profiles;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("profiles", profiles)
.toString();
}
}
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.game;
import javafx.geometry.Rectangle2D;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
@@ -33,48 +33,32 @@ import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.net.Proxy;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.*;
public final class AccountHelper {
public static final AccountHelper INSTANCE = new AccountHelper();
private AccountHelper() {}
public static final File SKIN_DIR = new File(Main.HMCL_DIRECTORY, "skins");
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());
return new SkinLoadTask(account, true);
}
public static Task refreshSkinAsync(YggdrasilAccount account, Proxy proxy) {
return new SkinLoadTask(account, proxy, true);
}
private static File getSkinFile(String name) {
return new File(SKIN_DIR, name + ".png");
private static File getSkinFile(UUID uuid) {
return new File(SKIN_DIR, uuid + ".png");
}
public static Image getSkin(YggdrasilAccount account) {
@@ -82,9 +66,11 @@ public final class AccountHelper {
}
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
if (account.getCharacter() == null)
return getDefaultSkin(account, scaleRatio);
File file = getSkinFile(account.getCharacter());
UUID uuid = account.getUUID();
if (uuid == null)
return getSteveSkin(scaleRatio);
File file = getSkinFile(uuid);
if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath());
return new Image("file:" + file.getAbsolutePath(),
@@ -92,15 +78,14 @@ public final class AccountHelper {
original.getHeight() * scaleRatio,
false, false);
}
return getDefaultSkin(account, scaleRatio);
return getDefaultSkin(uuid, scaleRatio);
}
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio, Proxy proxy) throws Exception {
String name = profile.getName();
File file = getSkinFile(name);
downloadSkin(account, profile, true, proxy);
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio) throws Exception {
File file = getSkinFile(profile.getId());
downloadSkin(account, profile, true);
if (!file.exists())
return getDefaultSkin(account, scaleRatio);
return getDefaultSkin(profile.getId(), scaleRatio);
String url = "file:" + file.getAbsolutePath();
return scale(url, scaleRatio);
@@ -113,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;
}
@@ -142,33 +125,33 @@ 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);
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(profile.getName());
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;
Optional<Texture> texture = account.getSkin();
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(account.getCharacter());
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) {
@@ -187,11 +170,8 @@ public final class AccountHelper {
return scale("/assets/img/alex.png", 4);
}
public static Image getDefaultSkin(Account account, double scaleRatio) {
if (account == null)
return getSteveSkin(scaleRatio);
int type = account.getUUID().hashCode() & 1;
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
int type = uuid.hashCode() & 1;
if (type == 1)
return getAlexSkin(scaleRatio);
else
@@ -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))));
}
@@ -17,12 +17,14 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
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=" + Main.VERSION);
result.add("-Dminecraft.launcher.brand=" + Main.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,10 +24,8 @@ 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.Lang;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
@@ -35,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<>();
@@ -61,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
@@ -86,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());
}
@@ -173,10 +173,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
return new File(getVersionRoot(id), "icon.png");
}
public void saveVersionSetting(String id) {
public boolean saveVersionSetting(String id) {
if (!versionSettings.containsKey(id))
return;
Lang.invoke(() -> FileUtils.writeText(getVersionSettingFile(id), GSON.toJson(versionSettings.get(id))));
return false;
File file = getVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
try {
FileUtils.writeText(file, GSON.toJson(versionSettings.get(id)));
return true;
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
return false;
}
}
public boolean forbidsVersion(String id) {
@@ -42,7 +42,6 @@ public final class HMCLModpackInstallTask extends Task {
private final String name;
private final HMCLGameRepository repository;
private final Modpack modpack;
private final File run;
private final List<Task> dependencies = new LinkedList<>();
private final List<Task> dependents = new LinkedList<>();
@@ -52,8 +51,8 @@ public final class HMCLModpackInstallTask extends Task {
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
this.run = repository.getRunDirectory(name);
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists");
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
@@ -17,17 +17,18 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import org.jackhuang.hmcl.Main;
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;
@@ -36,54 +37,57 @@ 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.MessageDialogPane;
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;
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();
private LauncherHelper(){}
private TaskExecutor executor;
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(() -> {});
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
public void launch(String selectedVersion, File scriptFile) {
Profile profile = Settings.INSTANCE.getSelectedProfile();
GameRepository repository = profile.getRepository();
Account account = Settings.INSTANCE.getSelectedAccount();
public void launch(Profile profile, Account account, String selectedVersion, File scriptFile) {
if (account == null)
throw new IllegalStateException("No account");
throw new IllegalArgumentException("No account");
Version version = repository.getVersion(selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> Schedulers.newThread().schedule(() -> launch0(selectedVersion, scriptFile)));
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile));
});
} catch (InterruptedException ignore) {
}
});
}
private void launch0(String selectedVersion, File scriptFile) {
Profile profile = Settings.INSTANCE.getSelectedProfile();
GameRepository repository = profile.getRepository();
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = repository.getResolvedVersion(selectedVersion);
Account account = Settings.INSTANCE.getSelectedAccount();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
executor = Task.of(Schedulers.javafx(), () -> Controllers.dialog(launchingStepsPane))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES)))
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
if (setting.isNotCheckGame())
return null;
@@ -91,38 +95,49 @@ 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(Main.i18n("account.methods"), variables -> {
.then(var -> {
try {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
if (account.canPlayOffline())
variables.set("account", account.playOffline());
else
throw 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));
JFXUtilities.runInFX(() -> Controllers.dialog(launchingStepsPane));
} catch (AuthenticationException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
}))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository, selectedVersion, variables.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
repository,
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
setting.getLauncherVisibility() == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
DefaultLauncher launcher = variables.get("launcher");
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(Main.i18n("version.launch"));
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(Main.i18n("version.launch_script"));
}).setName(i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
@@ -130,15 +145,17 @@ public final class LauncherHelper {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
Main.stopApplication();
Launcher.stopApplication();
else
launchingStepsPane.setCancel(() -> {
launchingStepsPane.setCancel(it -> {
process.stop();
Controllers.closeDialog();
it.fireEvent(new DialogCloseEvent());
});
} else
Platform.runLater(() ->
Controllers.dialog(Main.i18n("version.launch_script.success", scriptFile.getAbsolutePath())));
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
.executor();
@@ -156,18 +173,34 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (!success) {
if (!success && !Controllers.isStopped()) {
Platform.runLater(() -> {
if (executor.getLastException() != null)
Controllers.dialog(I18nException.getStackTrace(executor.getLastException()),
scriptFile == null ? Main.i18n("launch.failed") : Main.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE, Controllers::closeDialog);
else
Controllers.closeDialog();
// 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);
}
}
});
}
LauncherHelper.this.executor = null;
launchingStepsPane.setExecutor(null);
}
});
@@ -175,47 +208,57 @@ public final class LauncherHelper {
}
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
boolean flag = false, suggest = true;
boolean flag = false;
// 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(Main.i18n("launch.wrong_javadir"), Main.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();
flag = true;
}
if (java.getParsedVersion() < JavaVersion.JAVA_8) {
Controllers.dialog(Main.i18n("launch.advice.newer_java"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
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;
}
if (java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
Controllers.dialog(Main.i18n("launch.advice.java9"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
suggest = false;
// 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(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
flag = true;
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
org.jackhuang.hmcl.util.Platform.IS_64_BIT) {
Controllers.dialog(Main.i18n("launch.advice.different_platform"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
Controllers.dialog(Main.i18n("launch.advice.too_large_memory_for_32bit"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
// 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 (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(Main.i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (flag) {
if (suggest && Controllers.getDialogContent() instanceof MessageDialogPane)
((MessageDialogPane) Controllers.getDialogContent()).disableClosingDialog();
} else
if (!flag)
onAccept.run();
}
@@ -225,10 +268,12 @@ public final class LauncherHelper {
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
}
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
Controllers.dialog(launchingStepsPane);
}
private void checkExit(LauncherVisibility v) {
@@ -245,8 +290,8 @@ public final class LauncherHelper {
Platform.runLater(() -> {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
// If we use Main.stop(), log window will be halt immediately.
Main.stopWithoutPlatform();
// If we use Launcher.stop(), log window will be halt immediately.
Launcher.stopWithoutPlatform();
});
break;
}
@@ -261,15 +306,7 @@ public final class LauncherHelper {
@Override
public void execute() throws Exception {
try {
setResult(supplier.get());
} catch (PermissionException e) {
throw new I18nException(Main.i18n("launch.failed.executable_permission"), e);
} catch (ProcessCreationException e) {
throw new I18nException(Main.i18n("launch.failed.creating_process") + e.getLocalizedMessage(), e);
} catch (NotDecompressingNativesException e) {
throw new I18nException(Main.i18n("launch.failed.decompressing_natives") + e.getLocalizedMessage(), e);
}
setResult(supplier.get());
}
@Override
@@ -304,10 +341,10 @@ public final class LauncherHelper {
if (authInfo == null)
forbiddenTokens = Collections.emptyMap();
else
forbiddenTokens = Lang.mapOf(
new Pair<>(authInfo.getAccessToken(), "<access token>"),
new Pair<>(authInfo.getUserId(), "<uuid>"),
new Pair<>(authInfo.getUsername(), "<player>")
forbiddenTokens = mapOf(
pair(authInfo.getAccessToken(), "<access token>"),
pair(UUIDTypeAdapter.fromUUID(authInfo.getUUID()), "<uuid>"),
pair(authInfo.getUsername(), "<player>")
);
visibility = setting.getLauncherVisibility();
@@ -337,7 +374,7 @@ public final class LauncherHelper {
else
System.out.print(log);
logs.add(new Pair<>(log, level));
logs.add(pair(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
logs.removeFirst();
@@ -360,9 +397,12 @@ public final class LauncherHelper {
});
break;
case CLOSE:
throw new Error("Never come to here");
// Never come to here.
break;
case KEEP:
// No operations here
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
break;
case HIDE:
Platform.runLater(() -> {
@@ -385,16 +425,16 @@ public final class LauncherHelper {
switch (exitType) {
case JVM_ERROR:
logWindow.setTitle(Main.i18n("launch.failed.cannot_create_jvm"));
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
logWindow.setTitle(Main.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.Main;
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 Main.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");
@@ -17,27 +17,26 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Main;
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.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.util.*;
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;
/**
* @author huangyuhui
@@ -49,14 +48,14 @@ public final class Accounts {
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf(
new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
new Pair<>(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
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(
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;
@@ -72,33 +71,23 @@ public final class Accounts {
return username + ":" + character;
}
private static String downloadAuthlibInjector() throws Exception {
AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo();
File jar = new File(Main.HMCL_DIRECTORY, "authlib-injector.jar");
File local = new File(Main.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) {
if (AUTHLIB_INJECTOR_SERVER_NAMES.containsKey(serverIp))
return AUTHLIB_INJECTOR_SERVER_NAMES.get(serverIp);
else {
try {
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();
} catch (JsonParseException | IOException | NullPointerException e) {
return null;
}
}
CONFIG.getAuthlibInjectorServers().add(server);
return server;
});
}
}
@@ -17,274 +17,416 @@
*/
package org.jackhuang.hmcl.setting;
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 org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.util.JavaVersion;
import java.util.*;
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;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
final class Config {
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")
private String selectedProfile = "";
private StringProperty selectedProfile = new SimpleStringProperty("");
@SerializedName("backgroundType")
private int backgroundImageType = 0;
private ObjectProperty<EnumBackgroundImage> backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT);
@SerializedName("bgpath")
private String backgroundImage = null;
private StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("commonpath")
private String commonDirectory = Main.MINECRAFT_DIRECTORY.getAbsolutePath();
private StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
@SerializedName("hasProxy")
private boolean hasProxy = false;
private BooleanProperty hasProxy = new SimpleBooleanProperty();
@SerializedName("hasProxyAuth")
private boolean hasProxyAuth = false;
private BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
@SerializedName("proxyType")
private int proxyType = 0;
private ObjectProperty<Proxy.Type> proxyType = new SimpleObjectProperty<>(Proxy.Type.DIRECT);
@SerializedName("proxyHost")
private String proxyHost = null;
private StringProperty proxyHost = new SimpleStringProperty();
@SerializedName("proxyPort")
private String proxyPort = null;
private StringProperty proxyPort = new SimpleStringProperty();
@SerializedName("proxyUserName")
private String proxyUser = null;
private StringProperty proxyUser = new SimpleStringProperty();
@SerializedName("proxyPassword")
private String proxyPass = null;
private StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("theme")
private String theme = null;
@SerializedName("java")
private List<JavaVersion> java = null;
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
@SerializedName("localization")
private String localization;
private StringProperty localization = new SimpleStringProperty();
@SerializedName("downloadtype")
private int downloadType = 0;
private IntegerProperty downloadType = new SimpleIntegerProperty(1);
@SerializedName("configurations")
private Map<String, Profile> configurations = new TreeMap<>();
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private List<Map<Object, Object>> accounts = new LinkedList<>();
private ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private String selectedAccount = "";
private StringProperty selectedAccount = new SimpleStringProperty("");
@SerializedName("fontFamily")
private String fontFamily = "Consolas";
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
@SerializedName("fontSize")
private double fontSize = 12;
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@SerializedName("logLines")
private int logLines = 100;
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("authlibInjectorServerURLs")
private Set<String> authlibInjectorServerURLs = 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 fromJson(this.toJson());
}
// Getters & Setters & Properties
public String getSelectedProfile() {
return selectedProfile;
return selectedProfile.get();
}
public void setSelectedProfile(String selectedProfile) {
this.selectedProfile = selectedProfile;
Settings.INSTANCE.save();
this.selectedProfile.set(selectedProfile);
}
public String getBackgroundImage() {
return backgroundImage;
public StringProperty selectedProfileProperty() {
return selectedProfile;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage = backgroundImage;
Settings.INSTANCE.save();
public EnumBackgroundImage getBackgroundImageType() {
return backgroundImageType.get();
}
public int getBackgroundImageType() {
public void setBackgroundImageType(EnumBackgroundImage backgroundImageType) {
this.backgroundImageType.set(backgroundImageType);
}
public ObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
return backgroundImageType;
}
public void setBackgroundImageType(int backgroundImageType) {
this.backgroundImageType = backgroundImageType;
Settings.INSTANCE.save();
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;
return commonDirectory.get();
}
public void setCommonDirectory(String commonDirectory) {
this.commonDirectory = commonDirectory;
Settings.INSTANCE.save();
this.commonDirectory.set(commonDirectory);
}
public boolean isHasProxy() {
return hasProxy;
public StringProperty commonDirectoryProperty() {
return commonDirectory;
}
public boolean hasProxy() {
return hasProxy.get();
}
public void setHasProxy(boolean hasProxy) {
this.hasProxy = hasProxy;
Settings.INSTANCE.save();
this.hasProxy.set(hasProxy);
}
public boolean isHasProxyAuth() {
return hasProxyAuth;
public BooleanProperty hasProxyProperty() {
return hasProxy;
}
public boolean hasProxyAuth() {
return hasProxyAuth.get();
}
public void setHasProxyAuth(boolean hasProxyAuth) {
this.hasProxyAuth = hasProxyAuth;
Settings.INSTANCE.save();
this.hasProxyAuth.set(hasProxyAuth);
}
public int getProxyType() {
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 void setProxyType(int proxyType) {
this.proxyType = proxyType;
Settings.INSTANCE.save();
}
public String getProxyHost() {
return proxyHost;
return proxyHost.get();
}
public void setProxyHost(String proxyHost) {
this.proxyHost = proxyHost;
Settings.INSTANCE.save();
this.proxyHost.set(proxyHost);
}
public StringProperty proxyHostProperty() {
return proxyHost;
}
public String getProxyPort() {
return proxyPort;
return proxyPort.get();
}
public void setProxyPort(String proxyPort) {
this.proxyPort = proxyPort;
Settings.INSTANCE.save();
this.proxyPort.set(proxyPort);
}
public StringProperty proxyPortProperty() {
return proxyPort;
}
public String getProxyUser() {
return proxyUser;
return proxyUser.get();
}
public void setProxyUser(String proxyUser) {
this.proxyUser = proxyUser;
Settings.INSTANCE.save();
this.proxyUser.set(proxyUser);
}
public StringProperty proxyUserProperty() {
return proxyUser;
}
public String getProxyPass() {
return proxyPass;
return proxyPass.get();
}
public void setProxyPass(String proxyPass) {
this.proxyPass = proxyPass;
Settings.INSTANCE.save();
this.proxyPass.set(proxyPass);
}
public String getTheme() {
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 void setTheme(String theme) {
this.theme = theme;
Settings.INSTANCE.save();
}
public List<JavaVersion> getJava() {
return java;
}
public void setJava(List<JavaVersion> java) {
this.java = java;
Settings.INSTANCE.save();
}
public String getLocalization() {
return localization;
return localization.get();
}
public void setLocalization(String localization) {
this.localization = localization;
Settings.INSTANCE.save();
this.localization.set(localization);
}
public StringProperty localizationProperty() {
return localization;
}
public int getDownloadType() {
return downloadType;
return downloadType.get();
}
public void setDownloadType(int downloadType) {
this.downloadType = downloadType;
Settings.INSTANCE.save();
this.downloadType.set(downloadType);
}
public Map<String, Profile> getConfigurations() {
public IntegerProperty downloadTypeProperty() {
return downloadType;
}
public ObservableMap<String, Profile> getConfigurations() {
return configurations;
}
public void setConfigurations(Map<String, Profile> configurations) {
this.configurations = configurations;
Settings.INSTANCE.save();
}
public List<Map<Object, Object>> getAccounts() {
public ObservableList<Map<Object, Object>> getAccounts() {
return accounts;
}
public void setAccounts(List<Map<Object, Object>> accounts) {
this.accounts = accounts;
Settings.INSTANCE.save();
}
public String getSelectedAccount() {
return selectedAccount;
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount = selectedAccount;
Settings.INSTANCE.save();
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public String getFontFamily() {
return fontFamily;
return fontFamily.get();
}
public void setFontFamily(String fontFamily) {
this.fontFamily = fontFamily;
Settings.INSTANCE.save();
this.fontFamily.set(fontFamily);
}
public StringProperty fontFamilyProperty() {
return fontFamily;
}
public double getFontSize() {
return fontSize;
return fontSize.get();
}
public void setFontSize(double fontSize) {
this.fontSize = fontSize;
Settings.INSTANCE.save();
this.fontSize.set(fontSize);
}
public DoubleProperty fontSizeProperty() {
return fontSize;
}
public int getLogLines() {
return logLines;
return logLines.get();
}
public void setLogLines(int logLines) {
this.logLines = logLines;
this.logLines.set(logLines);
}
public Set<String> getAuthlibInjectorServerURLs() {
return authlibInjectorServerURLs;
public IntegerProperty logLinesProperty() {
return logLines;
}
public void setAuthlibInjectorServerURLs(Set<String> authlibInjectorServerURLs) {
this.authlibInjectorServerURLs = authlibInjectorServerURLs;
Settings.INSTANCE.save();
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 <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.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);
}
}
}
@@ -23,13 +23,12 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import org.jackhuang.hmcl.util.Lang;
import java.util.Arrays;
import java.util.List;
public final class DownloadProviders {
private DownloadProviders() {}
public static final List<DownloadProvider> DOWNLOAD_PROVIDERS = Lang.immutableListOf(new MojangDownloadProvider(Settings.INSTANCE.getLocale() == Locales.ZH_CN), BMCLAPIDownloadProvider.INSTANCE, CurseCDNDownloadProvider.INSTANCE);
public static final List<DownloadProvider> DOWNLOAD_PROVIDERS = Lang.immutableListOf(new MojangDownloadProvider(), BMCLAPIDownloadProvider.INSTANCE, CurseCDNDownloadProvider.INSTANCE);
public static DownloadProvider getDownloadProvider(int index) {
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(DOWNLOAD_PROVIDERS.get(0));
@@ -19,11 +19,15 @@ 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;
import org.jackhuang.hmcl.util.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.ImmediateStringProperty;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.io.File;
import java.lang.reflect.Type;
@@ -82,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) {
@@ -108,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;
@@ -146,10 +161,20 @@ public final class Profile {
vs.setUsesGlobal(true);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("gameDir", getGameDir())
.append("name", getName())
.append("useRelativePath", isUseRelativePath())
.toString();
}
public void addPropertyChangedListener(InvalidationListener listener) {
nameProperty.addListener(listener);
globalProperty.addListener(listener);
gameDirProperty.addListener(listener);
useRelativePathProperty.addListener(listener);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -166,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;
}
@@ -178,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.Main;
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 Main.i18n("profile.default");
case Settings.HOME_PROFILE:
return Main.i18n("profile.home");
case Profiles.DEFAULT_PROFILE:
return i18n("profile.default");
case Profiles.HOME_PROFILE:
return i18n("profile.home");
default:
return profile.getName();
}
@@ -0,0 +1,117 @@
/*
* 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.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,148 +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.scene.text.Font;
import org.jackhuang.hmcl.Main;
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.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
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)
.setPrettyPrinting().create();
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
public static final File SETTINGS_FILE = new File("hmcl.json").getAbsoluteFile();
public static final Settings INSTANCE = new Settings();
private Settings() {}
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final Config SETTINGS = initSettings();
private final boolean firstLaunch;
private Map<String, Account> accounts = new HashMap<>();
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()));
{
loadProxy();
private Settings() {
firstLaunch = CONFIG.isFirstLaunch();
CONFIG.setFirstLaunch(false);
for (Iterator<Map<Object, Object>> iterator = SETTINGS.getAccounts().iterator(); iterator.hasNext(); ) {
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(Lang.get(settings, "type", String.class).orElse(""));
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);
}
checkAuthlibInjectorServerURLs();
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.getAccounts().clear();
for (Account account : accounts.values()) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
SETTINGS.getAccounts().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.getCommonDirectory()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setCommonDirectory(get());
}
};
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.getLocalization());
private Locales.SupportedLocale locale = Locales.getLocaleByName(CONFIG.getLocalization());
public Locales.SupportedLocale getLocale() {
return locale;
@@ -166,147 +125,41 @@ public class Settings {
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
SETTINGS.setLocalization(Locales.getNameByLocale(locale));
}
private Proxy proxy = Proxy.NO_PROXY;
public Proxy getProxy() {
return proxy;
}
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.getProxyType());
public Proxy.Type getProxyType() {
return proxyType;
}
public void setProxyType(Proxy.Type proxyType) {
this.proxyType = proxyType;
SETTINGS.setProxyType(Proxies.PROXIES.indexOf(proxyType));
loadProxy();
}
public String getProxyHost() {
return SETTINGS.getProxyHost();
}
public void setProxyHost(String proxyHost) {
SETTINGS.setProxyHost(proxyHost);
}
public String getProxyPort() {
return SETTINGS.getProxyPort();
}
public void setProxyPort(String proxyPort) {
SETTINGS.setProxyPort(proxyPort);
}
public String getProxyUser() {
return SETTINGS.getProxyUser();
}
public void setProxyUser(String proxyUser) {
SETTINGS.setProxyUser(proxyUser);
}
public String getProxyPass() {
return SETTINGS.getProxyPass();
}
public void setProxyPass(String proxyPass) {
SETTINGS.setProxyPass(proxyPass);
}
public boolean hasProxy() { return SETTINGS.isHasProxy(); }
public void setHasProxy(boolean hasProxy) { SETTINGS.setHasProxy(hasProxy); }
public boolean hasProxyAuth() { return SETTINGS.isHasProxyAuth(); }
public void setHasProxyAuth(boolean hasProxyAuth) { SETTINGS.setHasProxyAuth(hasProxyAuth); }
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.getFontFamily(), SETTINGS.getFontSize());
return Font.font(CONFIG.getFontFamily(), CONFIG.getFontSize());
}
public void setFont(Font font) {
SETTINGS.setFontFamily(font.getFamily());
SETTINGS.setFontSize(font.getSize());
CONFIG.setFontFamily(font.getFamily());
CONFIG.setFontSize(font.getSize());
}
public int getLogLines() {
return Math.max(SETTINGS.getLogLines(), 100);
return Math.max(CONFIG.getLogLines(), 100);
}
public void setLogLines(int logLines) {
SETTINGS.setLogLines(logLines);
CONFIG.setLogLines(logLines);
}
/****************************************
* AUTHLIB INJECTORS *
****************************************/
public Set<String> getAuthlibInjectorServerURLs() {
return SETTINGS.getAuthlibInjectorServerURLs();
}
public void removeAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().remove(serverURL);
checkAuthlibInjectorAccounts();
save();
}
public void addAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().add(serverURL);
save();
}
private void checkAuthlibInjectorServerURLs() {
if (SETTINGS.getAuthlibInjectorServerURLs() == null)
SETTINGS.setAuthlibInjectorServerURLs(new HashSet<>());
}
private void checkAuthlibInjectorAccounts() {
for (Account account : getAccounts()) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount injectorAccount = (AuthlibInjectorAccount) account;
if (!SETTINGS.getAuthlibInjectorServerURLs().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);
}
/****************************************
@@ -314,21 +167,21 @@ public class Settings {
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(SETTINGS.getDownloadType());
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.setDownloadType(index);
CONFIG.setDownloadType(index);
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.getSelectedAccount())) {
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", null) {
@Override
public Account get() {
Account a = super.get();
@@ -350,7 +203,7 @@ public class Settings {
public void invalidated() {
super.invalidated();
SETTINGS.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
CONFIG.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
}
};
@@ -368,6 +221,12 @@ 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));
}
public Account getAccount(String name, String character) {
@@ -379,88 +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);
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.getBackgroundImage()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setBackgroundImage(get());
}
};
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.getBackgroundImageType())) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setBackgroundImageType(get().ordinal());
}
};
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.getTheme()).orElse(Theme.BLUE)) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setTheme(get().getName().toLowerCase());
}
};
public Theme getTheme() {
return theme.get();
}
public void setTheme(Theme theme) {
this.theme.set(theme);
}
public ImmediateObjectProperty<Theme> themeProperty() {
return theme;
}
/****************************************
* PROFILES *
****************************************/
@@ -468,16 +264,18 @@ public class Settings {
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(getProfileMap().keySet().stream().findFirst().get());
if (!hasProfile(CONFIG.getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
CONFIG.setSelectedProfile(selectedProfile);
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(SETTINGS.getSelectedProfile());
return getProfile(CONFIG.getSelectedProfile());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(selectedProfile.getName());
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), CONFIG.getSelectedProfile())) {
CONFIG.setSelectedProfile(selectedProfile.getName());
Schedulers.computation().schedule(this::onProfileChanged);
}
}
@@ -494,7 +292,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.getConfigurations();
return CONFIG.getConfigurations();
}
public Collection<Profile> getProfiles() {
@@ -506,10 +304,9 @@ public class Settings {
throw new IllegalArgumentException("Profile's name is empty");
getProfileMap().put(ver.getName(), ver);
Schedulers.computation().schedule(this::onProfileLoading);
ver.nameProperty().setChangedListener(this::profileNameChanged);
save();
}
public void deleteProfile(Profile profile) {
@@ -524,14 +321,18 @@ public class Settings {
private void checkProfileMap() {
if (getProfileMap().isEmpty()) {
getProfileMap().put(DEFAULT_PROFILE, new Profile(DEFAULT_PROFILE));
getProfileMap().put(HOME_PROFILE, new Profile(HOME_PROFILE, Main.MINECRAFT_DIRECTORY));
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);
}
}
private void onProfileChanged() {
EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(this, getSelectedProfile()));
getSelectedProfile().getRepository().refreshVersionsAsync().start();
EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(SETTINGS, getSelectedProfile()));
}
private void profileNameChanged(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {
@@ -543,7 +344,11 @@ public class Settings {
* Invoked by loading GUI phase.
*/
public void onProfileLoading() {
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(SETTINGS));
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
onProfileChanged();
}
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
}
@@ -17,11 +17,18 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
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;
@@ -29,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;
@@ -57,15 +66,25 @@ public class Theme {
return name.startsWith("#");
}
public boolean isLight() {
return Color.web(color).grayscale().getRed() >= 0.5;
}
public Color getForegroundColor() {
return isLight() ? Color.BLACK : Color.WHITE;
}
public String[] getStylesheets() {
String name = isCustom() ? BLUE.getName() : this.name;
String css = Theme.class.getResource("/assets/css/" + name + ".css").toExternalForm();
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css")).replace("%base-color%", color));
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet", e);
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[]{
@@ -86,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);
@@ -104,5 +119,27 @@ public class Theme {
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> CONFIG.getTheme().getForegroundColor(), CONFIG.themeProperty());
}
public static ObjectBinding<Color> blackFillBinding() {
return Bindings.createObjectBinding(() -> Color.BLACK);
}
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);
}
}
}
@@ -19,14 +19,18 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
/**
*
@@ -87,6 +91,20 @@ public final class VersionSetting {
javaProperty.set(java);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
* It's used to determine which JRE to use when multiple JREs match the selected Java version.
*/
public String getDefaultJavaPath() {
return defaultJavaPathProperty.get();
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
@@ -444,12 +462,18 @@ public final class VersionSetting {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
JavaVersion c = JavaVersion.getJREs().get(getJava());
if (c == null) {
List<JavaVersion> matchedJava = JavaVersion.getJREs().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
setJava("Default");
return JavaVersion.fromCurrentEnvironment();
} else
return c;
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
}
@@ -475,6 +499,7 @@ public final class VersionSetting {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
@@ -482,8 +507,8 @@ public final class VersionSetting {
return new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Main.TITLE)
.setProfileName(Main.TITLE)
.setVersionName(Launcher.TITLE)
.setProfileName(Launcher.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
@@ -494,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();
@@ -535,6 +560,7 @@ public final class VersionSetting {
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
return obj;
}
@@ -571,6 +597,7 @@ public final class VersionSetting {
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
return vs;
}
@@ -1,132 +0,0 @@
/*
* 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.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
public final class AccountItem extends StackPane {
private final Account account;
@FXML
private Pane icon;
@FXML private VBox content;
@FXML private StackPane header;
@FXML private StackPane body;
@FXML private JFXButton btnDelete;
@FXML private JFXButton btnRefresh;
@FXML private Label lblUser;
@FXML private Label lblType;
@FXML private Label lblEmail;
@FXML private Label lblServer;
@FXML private Label lblCurrentAccount;
@FXML private JFXRadioButton chkSelected;
@FXML private JFXProgressBar pgsSkin;
@FXML private ImageView portraitView;
@FXML private HBox buttonPane;
public AccountItem(int i, Account account, ToggleGroup toggleGroup) {
this.account = account;
FXUtils.loadFXML(this, "/assets/fxml/account-item.fxml");
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -0.5, 1.0));
chkSelected.setToggleGroup(toggleGroup);
btnDelete.setGraphic(SVG.delete("black", 15, 15));
btnRefresh.setGraphic(SVG.refresh("black", 15, 15));
// create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
chkSelected.getProperties().put("account", account);
setSelected(Settings.INSTANCE.getSelectedAccount() == account);
lblUser.setText(account.getCharacter());
lblType.setText(AccountsPage.accountType(account));
lblEmail.setText(account.getUsername());
if (account instanceof AuthlibInjectorAccount) {
Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(((AuthlibInjectorAccount) account).getServerBaseURL()))
.subscribe(Schedulers.javafx(), variables -> lblServer.setText(variables.get("serverName")));
}
if (account instanceof YggdrasilAccount) {
btnRefresh.setOnMouseClicked(e -> {
pgsSkin.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), this::loadSkin);
});
AccountHelper.loadSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), this::loadSkin);
} else
loadSkin();
if (account instanceof OfflineAccount) { // Offline Account cannot be refreshed,
buttonPane.getChildren().remove(btnRefresh);
}
}
private void loadSkin() {
pgsSkin.setVisible(false);
portraitView.setViewport(AccountHelper.getViewport(4));
if (account instanceof YggdrasilAccount)
portraitView.setImage(AccountHelper.getSkin((YggdrasilAccount) account, 4));
else
portraitView.setImage(AccountHelper.getDefaultSkin(account, 4));
FXUtils.limitSize(portraitView, 32, 32);
}
public Account getAccount() {
return account;
}
public void setSelected(boolean selected) {
lblCurrentAccount.setVisible(selected);
chkSelected.setSelected(selected);
}
public void setOnDeleteButtonMouseClicked(EventHandler<? super MouseEvent> eventHandler) {
btnDelete.setOnMouseClicked(eventHandler);
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
@@ -28,6 +27,7 @@ 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;
@@ -41,7 +41,6 @@ public class AccountLoginPane extends StackPane {
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
private JFXDialog dialog;
public AccountLoginPane(Account oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
@@ -69,11 +68,11 @@ public class AccountLoginPane extends StackPane {
Object account = variable.get("login");
if (account instanceof AuthInfo) {
success.accept(((AuthInfo) account));
dialog.close();
fireEvent(new DialogCloseEvent());
} else if (account instanceof NoSelectedCharacterException) {
dialog.close();
fireEvent(new DialogCloseEvent());
} else if (account instanceof Exception) {
lblCreationWarning.setText(AccountsPage.accountException((Exception) account));
lblCreationWarning.setText(AddAccountPane.accountException((Exception) account));
}
progressBar.setVisible(false);
@@ -83,10 +82,6 @@ public class AccountLoginPane extends StackPane {
@FXML
private void onCancel() {
failed.run();
dialog.close();
}
public void setDialog(JFXDialog dialog) {
this.dialog = dialog;
fireEvent(new DialogCloseEvent());
}
}
@@ -0,0 +1,152 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.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.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 {
private final StringProperty title;
private final ObjectProperty<Runnable> onDelete = new SimpleObjectProperty<>(this, "onDelete");
private final VersionListItem item;
private final Account account;
@FXML
private Label lblType;
@FXML
private Label lblServer;
@FXML
private Label lblCharacter;
@FXML
private Label lblEmail;
@FXML
private BorderPane paneServer;
@FXML
private BorderPane paneEmail;
@FXML
private ComponentList componentList;
@FXML
private JFXButton btnDelete;
@FXML
private JFXButton btnRefresh;
@FXML
private JFXProgressBar progressBar;
public AccountPage(Account account, VersionListItem item) {
this.account = account;
this.item = item;
title = new SimpleStringProperty(this, "title", i18n("account") + " - " + account.getCharacter());
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
lblServer.setText(server.getName());
installTooltip(lblServer, server.getUrl());
} else {
componentList.removeChildren(paneServer);
if (account instanceof OfflineAccount) {
componentList.removeChildren(paneEmail);
}
}
btnDelete.setGraphic(SVG.delete(Theme.blackFillBinding(), 15, 15));
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
lblCharacter.setText(account.getCharacter());
lblType.setText(AddAccountPane.accountType(account));
lblEmail.setText(account.getUsername());
btnRefresh.setVisible(account instanceof YggdrasilAccount);
}
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
}
@FXML
private void onRefresh() {
if (account instanceof YggdrasilAccount) {
progressBar.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
if (isDependentsSucceeded) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
}
}).start();
}
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public Runnable getOnDelete() {
return onDelete.get();
}
public ObjectProperty<Runnable> onDeleteProperty() {
return onDelete;
}
public void setOnDelete(Runnable onDelete) {
this.onDelete.set(onDelete);
}
}
@@ -1,312 +0,0 @@
/*
* 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.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
public final class AccountsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("account"));
@FXML
private ScrollPane scrollPane;
@FXML private JFXMasonryPane masonryPane;
@FXML private JFXDialog dialog;
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private JFXProgressBar progressBar;
@FXML private Label lblAddInjectorServer;
@FXML private Hyperlink linkAddInjectorServer;
{
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
getChildren().remove(dialog);
dialog.setDialogContainer(this);
FXUtils.smoothScrolling(scrollPane);
cboType.getItems().setAll(Main.i18n("account.methods.offline"), Main.i18n("account.methods.yggdrasil"), Main.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);
linkAddInjectorServer.setVisible(newValue.intValue() == 2);
lblAddInjectorServer.setVisible(newValue.intValue() == 2);
});
cboType.getSelectionModel().select(0);
// These two lines can eliminate black, don't know why.
cboServers.getItems().setAll(new TwoLineListItem("", ""));
cboServers.getSelectionModel().select(0);
txtPassword.setOnAction(e -> onCreationAccept());
txtUsername.setOnAction(e -> onCreationAccept());
txtUsername.getValidators().add(new Validator(Main.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), account -> {
for (Node node : masonryPane.getChildren())
if (node instanceof AccountItem)
((AccountItem) node).setSelected(account == ((AccountItem) node).getAccount());
});
loadAccounts();
loadServers();
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
public void loadAccounts() {
List<Node> children = new LinkedList<>();
int i = 0;
ToggleGroup group = new ToggleGroup();
for (Account account : Settings.INSTANCE.getAccounts()) {
children.add(buildNode(++i, account, group));
}
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (newValue != null)
Settings.INSTANCE.setSelectedAccount((Account) newValue.getProperties().get("account"));
});
FXUtils.resetChildren(masonryPane, children);
Platform.runLater(() -> {
masonryPane.requestLayout();
scrollPane.requestLayout();
});
}
public void loadServers() {
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.map(serverURL -> new TwoLineListItem(Accounts.getAuthlibInjectorServerName(serverURL), serverURL))
.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 Node buildNode(int i, Account account, ToggleGroup group) {
AccountItem item = new AccountItem(i, account, group);
item.setOnDeleteButtonMouseClicked(e -> {
Settings.INSTANCE.deleteAccount(account);
Platform.runLater(this::loadAccounts);
});
return item;
}
@FXML
private void addNewAccount() {
txtUsername.setText("");
txtPassword.setText("");
lblCreationWarning.setText("");
dialog.show();
}
@FXML
private void onAddInjecterServer() {
Controllers.navigate(Controllers.getServersPage());
}
@FXML
private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText();
String password = txtPassword.getText();
String apiRoot = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).map(TwoLineListItem::getSubtitle).orElse(null);
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
AccountFactory<?> factory;
switch (type) {
case 0: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY); break;
case 1: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY); break;
case 2: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY); break;
default: throw new Error();
}
return factory.create(new Selector(), username, password, apiRoot, Settings.INSTANCE.getProxy());
}).finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
dialog.close();
loadAccounts();
progressBar.setVisible(false);
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
dialog.close();
} else {
lblCreationWarning.setText(accountException(exception));
}
progressBar.setVisible(false);
}).start();
}
@FXML
private void onCreationCancel() {
dialog.close();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public static String accountException(Exception exception) {
if (exception instanceof InvalidCredentialsException) {
return Main.i18n("account.failed.invalid_credentials");
} else if (exception instanceof NoCharacterException) {
return Main.i18n("account.failed.no_charactor");
} else if (exception instanceof ServerDisconnectException) {
return Main.i18n("account.failed.connect_authentication_server");
} else if (exception instanceof InvalidTokenException) {
return Main.i18n("account.failed.invalid_token");
} else if (exception instanceof InvalidPasswordException) {
return Main.i18n("account.failed.invalid_password");
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return Main.i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return Main.i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return Main.i18n("account.methods.yggdrasil");
else throw new Error(Main.i18n("account.methods.no_method") + ": " + account);
}
private static class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
{
setStyle("-fx-padding: 8px;");
cancel.setText(Main.i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(Main.i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount))
return CharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
image = FXUtils.DEFAULT_ICON;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == FXUtils.DEFAULT_ICON)
portraitView.setImage(FXUtils.DEFAULT_ICON);
else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> Controllers.dialog(this));
try {
latch.await();
JFXUtilities.runInFX(Controllers::closeDialog);
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account);
}
}
}
}
@@ -0,0 +1,282 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
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.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.StackPane;
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.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.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept;
@FXML private SpinnerPane acceptPane;
public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
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();
cboType.getItems().setAll(i18n("account.methods.offline"), i18n("account.methods.yggdrasil"), i18n("account.methods.authlib_injector"));
cboType.getSelectionModel().select(0);
ReadOnlyIntegerProperty loginTypeIdProperty = cboType.getSelectionModel().selectedIndexProperty();
txtPassword.visibleProperty().bind(loginTypeIdProperty.isNotEqualTo(0));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
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));
}
/**
* 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() {
if (btnAccept.isDisabled())
return;
String username = txtUsername.getText();
String password = txtPassword.getText();
Object addtionalData;
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() {
fireEvent(new DialogCloseEvent());
}
@FXML
private void onManageInjecterServers() {
fireEvent(new DialogCloseEvent());
Controllers.navigate(Controllers.getServersPage());
}
private class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
{
setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount))
return CharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = null;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == null) {
portraitView.setImage(Constants.DEFAULT_ICON.get());
} else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> Controllers.dialog(this));
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
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 NoCharacterException) {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
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().getName() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account 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 <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
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,18 +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;");
@@ -48,16 +49,16 @@ public final class AuthlibInjectorServerItem extends BorderPane {
right.setOnMouseClicked(e -> deleteCallback.accept(this));
right.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(right, Pos.CENTER);
right.setGraphic(SVG.close("black", 15, 15));
right.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(right);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(info.getServerName());
lblServerIp.setText(info.getServerIp());
lblServerName.setText(server.getName());
lblServerIp.setText(server.getUrl());
}
public AuthlibInjectorServerInfo getInfo() {
return info;
public AuthlibInjectorServer getServer() {
return server;
}
}
@@ -1,142 +1,70 @@
/*
* 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.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.Main;
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.NetworkUtils;
import java.util.Collection;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.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()
.map(serverURL -> new AuthlibInjectorServerItem(new AuthlibInjectorServerInfo(serverURL, Accounts.getAuthlibInjectorServerName(serverURL)), this::removeServer))
.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", () -> Objects.requireNonNull(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() {
@@ -17,22 +17,25 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
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 {
@@ -93,7 +96,9 @@ public final class Controllers {
public static void initialize(Stage stage) {
Controllers.stage = stage;
decorator = new Decorator(stage, getMainPage(), Main.TITLE, false, true);
stage.setOnCloseRequest(e -> Launcher.stopApplication());
decorator = new Decorator(stage, getMainPage(), Launcher.TITLE, false, true);
decorator.showPage(null);
leftPaneController = new LeftPaneController(decorator.getLeftPane());
@@ -103,24 +108,19 @@ 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);
stage.setMaxHeight(521);
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.setTitle(Main.TITLE);
stage.setTitle(Launcher.TITLE);
}
public static Region getDialogContent() {
return decorator.getDialog().getContent();
}
public static JFXDialog dialog(Region content) {
// TODO: temp fix
decorator.showDialog(new Region());
return decorator.showDialog(content);
public static void dialog(Region content) {
if (decorator != null)
decorator.showDialog(content);
}
public static void dialog(String text) {
@@ -136,28 +136,39 @@ public final class Controllers {
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, decorator.getDialog(), type, onAccept));
dialog(new MessageDialogPane(text, title, type, onAccept));
}
public static void confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, decorator.getDialog(), onAccept, onCancel));
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
}
public static void inputDialog(String text, Consumer<String> onResult) {
dialog(new InputDialogPane(text, decorator.getDialog(), onResult));
public static InputDialogPane inputDialog(String text, FutureCallback<String> onResult) {
InputDialogPane pane = new InputDialogPane(text, onResult);
dialog(pane);
return pane;
}
public static void taskDialog(TaskExecutor executor, String title, String subtitle, Runnable onCancel) {
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setSubtitle(subtitle);
pane.setExecutor(executor);
executor.start();
dialog(pane);
return pane;
}
public static void closeDialog() {
decorator.getDialog().close();
/**
* Use {@link DialogCloseEvent}
*/
public static void closeDialog(Region content) {
if (stage == null) // shut down
return;
decorator.closeDialog(content);
}
public static void navigate(Node node) {
@@ -168,7 +179,13 @@ public final class Controllers {
}
public static void showUpdate() {
getDecorator().showUpdate();
if (stage == null) // shut down
return;
getLeftPaneController().showUpdate();
}
public static boolean isStopped() {
return decorator == null;
}
public static void shutdown() {
@@ -27,7 +27,10 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.Launcher;
/**
* @author huangyuhui
@@ -36,10 +39,10 @@ public class CrashWindow extends Stage {
public CrashWindow(String text) {
Label lblCrash = new Label();
if (Main.UPDATE_CHECKER.isOutOfDate())
lblCrash.setText(Main.i18n("launcher.crash_out_dated"));
if (Launcher.UPDATE_CHECKER.isOutOfDate())
lblCrash.setText(i18n("launcher.crash_out_dated"));
else
lblCrash.setText(Main.i18n("launcher.crash"));
lblCrash.setText(i18n("launcher.crash"));
lblCrash.setWrapText(true);
TextArea textArea = new TextArea();
@@ -47,8 +50,8 @@ public class CrashWindow extends Stage {
textArea.setEditable(false);
Button btnContact = new Button();
btnContact.setText(Main.i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Main.CONTACT));
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
@@ -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(Main.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,23 +52,36 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Main;
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;
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),
@@ -73,7 +93,9 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private static final SVGGlyph close = Lang.apply(new SVGGlyph(0, "CLOSE", "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private final ObjectProperty<Runnable> onCloseButtonAction = new SimpleObjectProperty<>(Main::stopApplication);
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Decorator.class.getName() + ".dialog.closeListener";
private final ObjectProperty<Runnable> onCloseButtonAction;
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
private final Stage primaryStage;
@@ -83,10 +105,13 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging, dialogShown, maximized;
private boolean allowMove, isDragging, maximized;
private BoundingBox originalBox, maximizedBox;
private final TransitionHandler animationHandler;
private JFXDialog dialog;
private StackContainerPane dialogPane;
@FXML
private StackPane contentPlaceHolder;
@FXML
@@ -118,17 +143,15 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
@FXML
private JFXHamburger titleBurger;
@FXML
private JFXDialog dialog;
@FXML
private JFXButton btnMin;
@FXML
private JFXButton btnMax;
@FXML
private JFXButton btnClose;
@FXML
private HBox updatePane;
@FXML
private HBox navLeft;
@FXML
private ImageView welcomeView;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
@@ -142,14 +165,22 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml");
updatePane.setCursor(Cursor.HAND);
updatePane.setOnMouseClicked(event -> Main.UPDATE_CHECKER.checkOutdate());
onCloseButtonAction = new SimpleObjectProperty<>(this, "onCloseButtonAction", Launcher::stopApplication);
primaryStage.initStyle(StageStyle.UNDECORATED);
btnClose.setGraphic(close);
btnMin.setGraphic(minus);
btnMax.setGraphic(resizeMax);
close.fillProperty().bind(Theme.foregroundFillBinding());
minus.fillProperty().bind(Theme.foregroundFillBinding());
resizeMax.fillProperty().bind(Theme.foregroundFillBinding());
resizeMin.fillProperty().bind(Theme.foregroundFillBinding());
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), 15, 15));
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), 15, 15));
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), 15, 15));
lblTitle.setText(title);
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
@@ -158,9 +189,18 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
btnMax.fire();
});
dialog.setDialogContainer(drawerWrapper);
dialog.setOnDialogClosed(e -> dialogShown = false);
dialog.setOnDialogOpened(e -> dialogShown = true);
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);
@@ -177,76 +217,87 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
animationHandler = new TransitionHandler(contentPlaceHolder);
loadBackground();
setupBackground();
setupAuthlibInjectorDnD();
}
private void loadBackground() {
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))));
// ==== 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 static Image searchBackgroundImage(Image def, String customPath) {
Random random = new Random();
boolean loaded = false;
Image background = def;
private Image defaultBackground = new Image("/assets/img/background.jpg");
// custom path
if (StringUtils.isNotBlank(customPath)) {
/**
* 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 {
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 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) {
@@ -464,10 +515,6 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
public void showUpdate() {
updatePane.setVisible(true);
}
private void showCloseNavButton() {
navLeft.getChildren().add(closeNavButton);
}
@@ -477,6 +524,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
private void setContent(Node content, AnimationProducer animation) {
isWizardPageNow = false;
animationHandler.setContent(content, animation);
if (content instanceof Region) {
@@ -484,11 +532,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
FXUtils.setOverflowHidden((Region) content, true);
}
if (content instanceof Refreshable)
refreshNavButton.setVisible(true);
if (content != mainPage)
backNavButton.setVisible(true);
refreshNavButton.setVisible(content instanceof Refreshable);
backNavButton.setVisible(content != mainPage);
String prefix = category == null ? "" : category + " - ";
@@ -503,12 +548,15 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private String category;
private Node nowPage;
private boolean isWizardPageNow;
public Node getNowPage() {
return nowPage;
}
public void showPage(Node content) {
FXUtils.checkFxUserThread();
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
if (content != null)
contentPlaceHolder.getStyleClass().add("gray-background");
@@ -529,11 +577,40 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
public JFXDialog showDialog(Region content) {
dialog.setContent(content);
if (!dialogShown)
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialog == null) {
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialog.setContent(dialogPane);
dialog.setDialogContainer(drawerWrapper);
dialog.setOverlayClose(false);
dialog.show();
return dialog;
}
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()) {
dialog.close();
dialog = null;
dialogPane = null;
}
}
public void startWizard(WizardProvider wizardProvider) {
@@ -541,6 +618,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
this.category = category;
wizardController.setProvider(wizardProvider);
wizardController.onStart();
@@ -566,6 +645,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
contentPlaceHolder.getStyleClass().add("white-background");
setContent(page, nav.getAnimation().getAnimationProducer());
isWizardPageNow = true;
}
@FXML
@@ -581,7 +661,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
@FXML
private void onBack() {
if (wizardController.canPrev())
if (isWizardPageNow && wizardController.canPrev())
wizardController.onPrev(true);
else
onCloseNav();
@@ -621,11 +701,13 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
return wizardController;
}
public JFXDialog getDialog() {
return dialog;
}
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))));
}
}
@@ -37,12 +37,8 @@ public final class DialogController {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
Controllers.closeDialog();
}, () -> {
latch.countDown();
Controllers.closeDialog();
});
pane.setDialog(Controllers.dialog(pane));
}, latch::countDown);
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
@@ -17,12 +17,13 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -30,34 +31,52 @@ 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.Main;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.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;
import static org.jackhuang.hmcl.util.ReflectionHelper.call;
import static org.jackhuang.hmcl.util.ReflectionHelper.construct;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class FXUtils {
private FXUtils() {
}
public static void checkFxUserThread() {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX application thread; currentThread = "
+ Thread.currentThread().getName());
}
}
public static 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));
}
@@ -114,7 +133,7 @@ public final class FXUtils {
}
public static void removeListener(Node node, String key) {
Lang.cast(node.getProperties().get(key), ListenerPair.class)
tryCast(node.getProperties().get(key), ListenerPair.class)
.ifPresent(info -> {
info.unbind();
node.getProperties().remove(key);
@@ -184,18 +203,12 @@ public final class FXUtils {
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Main.RESOURCE_BUNDLE);
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
}
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
pane.getChildren().setAll(children);
}
public static void installTooltip(Node node, String tooltip) {
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
}
@@ -203,17 +216,22 @@ public final class FXUtils {
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
try {
// Java 8
call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false),
"install", node, tooltip);
} catch (Throwable e) {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
try {
// Java 9
call(tooltip, "setShowDelay", new Duration(openDelay));
call(tooltip, "setShowDuration", new Duration(visibleDelay));
call(tooltip, "setHideDelay", new Duration(closeDelay));
} catch (Throwable e2) {
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
@@ -322,7 +340,7 @@ public final class FXUtils {
*/
@SuppressWarnings("unchecked")
public static void unbindEnum(JFXComboBox<?> comboBox) {
ChangeListener listener = Lang.get(comboBox.getProperties(), "FXUtils.bindEnum.listener", ChangeListener.class).orElse(null);
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
@@ -368,7 +386,57 @@ public final class FXUtils {
});
}
public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png");
public static <T> T runInUIThread(Supplier<T> supplier) {
if (javafx.application.Platform.isFxApplicationThread()) {
return supplier.get();
} else {
CountDownLatch doneLatch = new CountDownLatch(1);
AtomicReference<T> reference = new AtomicReference<>();
Platform.runLater(() -> {
try {
reference.set(supplier.get());
} finally {
doneLatch.countDown();
}
});
try {
doneLatch.await();
} catch (InterruptedException var3) {
Thread.currentThread().interrupt();
}
return reference.get();
}
}
public static <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,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.game.GameVersion;
@@ -29,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;
@@ -89,7 +91,7 @@ public class InstallerController {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
Controllers.dialog("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.Main;
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(Main.i18n("archive.version") + ": " + version);
lblInstallerVersion.setText(i18n("archive.version") + ": " + version);
}
@FXML
@@ -19,85 +19,99 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import org.jackhuang.hmcl.Main;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.construct.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;
import java.util.LinkedList;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
private final VBox profilePane = new VBox();
private final VersionListItem accountItem = new VersionListItem("", "");
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
leftPane.startCategory(Main.i18n("account").toUpperCase())
.add(Lang.apply(new RipplerContainer(accountItem), rippler -> {
rippler.setOnMouseClicked(e -> Controllers.navigate(new AccountsPage()));
accountItem.setOnSettingsButtonClicked(() -> Controllers.navigate(new AccountsPage()));
}))
.startCategory(Main.i18n("launcher").toUpperCase())
.add(Lang.apply(new IconedItem(SVG.gear("black", 20, 20), Main.i18n("settings.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
}))
.add(new ClassTitle(Lang.apply(new BorderPane(), borderPane -> {
borderPane.setLeft(Lang.apply(new VBox(), vBox -> vBox.getChildren().setAll(new Text(Main.i18n("profile.title").toUpperCase()))));
JFXButton addProfileButton = new JFXButton();
addProfileButton.setGraphic(SVG.plus("black", 10, 10));
addProfileButton.getStyleClass().add("toggle-icon-tiny");
addProfileButton.setOnMouseClicked(e ->
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), i18n("settings.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
});
leftPane
.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(i18n("launcher").toUpperCase())
.add(launcherSettingsItem)
.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 ->
Controllers.getDecorator().showPage(new ProfilePage(null)));
borderPane.setRight(addProfileButton);
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), it -> {
if (it == null) {
accountItem.setVersionName(Main.i18n("account.missing"));
accountItem.setGameVersion(Main.i18n("message.unknown"));
} else {
accountItem.setVersionName(it.getCharacter());
accountItem.setGameVersion(AccountsPage.accountType(it));
}
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
onAccountsLoading();
}
if (it instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) it, 4);
accountItem.setImage(image, AccountHelper.getViewport(4));
} else
accountItem.setImage(AccountHelper.getDefaultSkin(it, 4), AccountHelper.getViewport(4));
private void addNewAccount() {
Controllers.dialog(new AddAccountPane());
}
private void onSelectedAccountChanged(Account newAccount) {
Platform.runLater(() -> {
for (Node node : accountPane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) {
boolean current = Objects.equals(node.getProperties().get("account"), newAccount);
((RipplerContainer) node).setSelected(current);
}
}
});
}
@@ -109,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 ? Main.i18n("profile.selected") : "");
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? i18n("profile.selected") : "");
}
}
});
@@ -120,7 +134,7 @@ public final class LeftPaneController {
for (Profile profile : Settings.INSTANCE.getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(() -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setOnMouseClicked(e -> Settings.INSTANCE.setSelectedProfile(profile));
ripplerContainer.getProperties().put("profile", profile.getName());
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
@@ -129,11 +143,80 @@ public final class LeftPaneController {
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
Account account = event.getAccount();
VersionListItem item = items.get(account);
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
items.clear();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
items.put(account, item);
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
ripplerContainer.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
Settings.INSTANCE.setSelectedAccount(account);
});
ripplerContainer.getProperties().put("account", account);
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
if (selectedAccount == account)
ripplerContainer.setSelected(true);
list.add(ripplerContainer);
}
if (Settings.INSTANCE.getAccounts().isEmpty()) {
RipplerContainer container = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
container.setOnMouseClicked(e -> addNewAccount());
list.add(container);
}
Platform.runLater(() -> accountPane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
}
private boolean checkedModpack = false;
private static boolean showNewAccount = true;
private void onRefreshedVersions(RefreshedVersionsEvent event) {
JFXUtilities.runInFX(() -> {
boolean flag = true;
HMCLGameRepository repository = (HMCLGameRepository) event.getSource();
if (!checkedModpack) {
checkedModpack = true;
@@ -142,27 +225,31 @@ public final class LeftPaneController {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
try {
AtomicReference<Region> region = new AtomicReference<>();
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
Controllers.taskDialog(ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
Controllers.closeDialog();
checkAccount();
})).executor(),
Main.i18n("modpack.installing"), "", null);
flag = false;
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
region.get().fireEvent(new DialogCloseEvent());
checkAccount();
})).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing"), ""));
executor.start();
showNewAccount = false;
} catch (UnsupportedModpackException ignore) {
}
}
}
}
if (flag)
if (showNewAccount) {
showNewAccount = false;
checkAccount();
}
});
}
private void checkAccount() {
public void checkAccount() {
if (Settings.INSTANCE.getAccounts().isEmpty())
Controllers.navigate(new AccountsPage());
addNewAccount();
}
}
@@ -30,7 +30,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.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(Main.i18n("logwindow.title"));
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -17,24 +17,23 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXMasonryPane;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -43,182 +42,205 @@ 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.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
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", Main.i18n("main_page"));
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("main_page"));
private Profile profile;
private String rightClickedVersion;
@FXML
private JFXButton btnRefresh;
@FXML
private StackPane contentPane;
@FXML
private JFXButton btnAdd;
@FXML
private JFXSpinner spinner;
@FXML
private JFXMasonryPane masonryPane;
@FXML
private JFXListView versionList;
private final JFXPopup versionPopup;
private ScrollPane scrollPane;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(() -> Platform.runLater(this::loadVersions));
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
loadingVersions();
versionPopup = new JFXPopup(versionList);
getChildren().remove(versionList);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
loadVersions((HMCLGameRepository) event.getSource());
});
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
// This will occupy 0.5s. Too slow!
JFXUtilities.runInFX(this::loadingVersions);
});
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(event -> {
this.profile = event.getProfile();
});
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), Main.i18n("install")));
FXUtils.installTooltip(btnAdd, Main.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, Main.i18n("button.refresh"));
FXUtils.installTooltip(btnRefresh, i18n("button.refresh"));
}
private Node buildNode(Profile profile, String version, String game) {
private String modifyVersion(String gameVersion, String version) {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
}
private Node buildNode(HMCLGameRepository repository, Version version, Callable<String> gameCallable) {
Profile profile = repository.getProfile();
String id = version.getId();
VersionItem item = new VersionItem();
item.setUpdate(profile.getRepository().isModpack(version));
item.setGameVersion(game);
item.setVersionName(version);
item.setUpdate(repository.isModpack(id));
Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> {
String game = vars.get("game");
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : profile.getRepository().getVersion(version).getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Main.i18n("install.installer.forge")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)forge", "").replace(game, "").trim(), "-"), "-")).append("\n");
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(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(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(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(Main.i18n("install.installer.liteloader")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)liteloader", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(Main.i18n("install.installer.optifine")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)optifine", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
}
item.setLibraries(libraries.toString());
item.setLibraries(libraries.toString());
});
item.setVersionName(id);
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.empty_username"));
Controllers.getLeftPaneController().checkAccount();
else
LauncherHelper.INSTANCE.launch(version, null);
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.empty_username"));
Controllers.dialog(i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
chooser.setInitialDirectory(profile.getRepository().getRunDirectory(version));
chooser.setTitle(Main.i18n("version.launch_script.save"));
if (repository.getRunDirectory(id).isDirectory())
chooser.setInitialDirectory(repository.getRunDirectory(id));
chooser.setTitle(i18n("version.launch_script.save"));
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(Main.i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(Main.i18n("extension.sh"), "*.sh"));
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(version, file);
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
}
});
item.setOnSettingsButtonClicked(e -> {
Controllers.getDecorator().showPage(Controllers.getVersionPage());
Controllers.getVersionPage().load(version, profile);
Controllers.getVersionPage().load(id, profile);
});
item.setOnUpdateButtonClicked(event -> {
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.i18n("modpack"), "*.zip"));
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(null);
AtomicReference<Region> region = new AtomicReference<>();
try {
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, version, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(version)))
.then(Task.of(Schedulers.javafx(), Controllers::closeDialog)).executor();
pane.setExecutor(executor);
pane.setTitle(Main.i18n("modpack.update"));
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id)))
.then(Task.of(Schedulers.javafx(), () -> region.get().fireEvent(new DialogCloseEvent()))).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.update"), ""));
executor.start();
Controllers.dialog(pane);
} catch (UnsupportedModpackException e) {
Controllers.dialog(Main.i18n("modpack.unsupported"), Main.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.dialog(Main.i18n("modpack.mismatched_type"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
Controllers.dialog(Main.i18n("modpack.invalid"), Main.i18n("message.error"), MessageBox.ERROR_MESSAGE);
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
region.get().fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
}
});
item.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.SECONDARY) {
rightClickedVersion = version;
versionList.getSelectionModel().select(-1);
JFXListView<String> versionList = new JFXListView<>();
JFXPopup versionPopup = new JFXPopup(versionList);
versionList.getStyleClass().add("option-list-view");
FXUtils.setLimitWidth(versionList, 150);
versionList.getItems().setAll(Lang.immutableListOf(
i18n("version.manage.rename"),
i18n("version.manage.remove"),
i18n("modpack.export"),
i18n("folder.game")
));
versionList.setOnMouseClicked(e -> {
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
VersionPage.renameVersion(profile, id);
break;
case 1:
VersionPage.deleteVersion(profile, id);
break;
case 2:
VersionPage.exportVersion(profile, id);
break;
case 3:
FXUtils.openFolder(repository.getRunDirectory(id));
break;
default:
break;
}
});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Main.i18n("login.empty_username"));
Controllers.dialog(i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(version, null);
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
}
});
File iconFile = profile.getRepository().getVersionIcon(version);
File iconFile = repository.getVersionIcon(id);
if (iconFile.exists())
item.setImage(new Image("file:" + iconFile.getAbsolutePath()));
return item;
}
public void onProfilesLoading() {
// TODO: Profiles
private void loadingVersions() {
getChildren().setAll(spinner);
masonryPane.getChildren().clear();
}
public void onProfileChanged(ProfileChangedEvent event) {
Platform.runLater(() -> loadVersions(event.getProfile()));
}
private void loadVersions() {
loadVersions(Settings.INSTANCE.getSelectedProfile());
}
private void loadVersions(Profile profile) {
this.profile = profile;
List<Node> children = new LinkedList<>();
List<Version> versions = new LinkedList<>(profile.getRepository().getVersions());
for (Version version : versions) {
children.add(buildNode(profile, version.getId(), GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version.getId())).orElse("Unknown")));
}
FXUtils.resetChildren(masonryPane, children);
}
@FXML
private void onVersionManagement() {
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
VersionPage.renameVersion(profile, rightClickedVersion);
break;
case 1:
VersionPage.deleteVersion(profile, rightClickedVersion);
break;
case 2:
VersionPage.exportVersion(profile, rightClickedVersion);
break;
case 3:
FXUtils.openFolder(profile.getRepository().getRunDirectory(rightClickedVersion));
break;
default:
throw new Error();
}
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile()) {
masonryPane.getChildren().setAll(children);
getChildren().setAll(contentPane);
}
});
}
public String getTitle() {
@@ -17,30 +17,32 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXSpinner;
import com.jfoenix.controls.JFXTabPane;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.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;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public final class ModController {
@FXML
@@ -68,21 +70,22 @@ public final class ModController {
});
rootPane.setOnDragDropped(event -> {
List<File> mods = event.getDragboard().getFiles();
Stream<File> stream = null;
if (mods != null)
stream = mods.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)));
if (stream != null && stream.findAny().isPresent()) {
stream.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
List<File> files = event.getDragboard().getFiles();
if (files != null) {
Collection<File> mods = files.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)))
.collect(Collectors.toList());
if (!mods.isEmpty()) {
mods.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
}
}
event.consume();
});
@@ -93,7 +96,7 @@ public final class ModController {
this.versionId = versionId;
Task.of(variables -> {
synchronized (ModController.this) {
Platform.runLater(() -> {
JFXUtilities.runInFX(() -> {
rootPane.getChildren().remove(contentPane);
spinner.setVisible(true);
});
@@ -121,25 +124,24 @@ public final class ModController {
list.add(item);
}
Platform.runLater(() -> {
rootPane.getChildren().add(contentPane);
spinner.setVisible(false);
});
variables.set("list", list);
}
}).finalized(Schedulers.javafx(), variables -> {
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModController.this)
modPane.getChildren().setAll(variables.<List<ModItem>>get("list"));
});
}, null).start();
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
rootPane.getChildren().add(contentPane);
spinner.setVisible(false);
if (isDependentsSucceeded)
FXUtils.onWeakChangeAndOperate(parentTab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue != null && newValue.getUserData() == ModController.this)
modPane.getChildren().setAll(variables.<List<ModItem>>get("list"));
});
}).start();
}
@FXML
private void onAdd() {
FileChooser chooser = new FileChooser();
chooser.setTitle(Main.i18n("mods.choose_mod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(Main.i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
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.
@@ -161,10 +163,10 @@ public final class ModController {
}).with(Task.of(Schedulers.javafx(), variables -> {
List<String> prompt = new LinkedList<>();
if (!succeeded.isEmpty())
prompt.add(Main.i18n("mods.add.success", String.join(", ", succeeded)));
prompt.add(i18n("mods.add.success", String.join(", ", succeeded)));
if (!failed.isEmpty())
prompt.add(Main.i18n("mods.add.failed", String.join(", ", failed)));
Controllers.dialog(String.join("\n", prompt), Main.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,13 +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.Main;
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;
@@ -39,17 +42,19 @@ public final class ModItem extends BorderPane {
setCenter(modItem);
JFXButton btnRemove = new JFXButton();
FXUtils.installTooltip(btnRemove, Main.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);
btnRemove.setGraphic(SVG.close("black", 15, 15));
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(btnRemove);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getFileName());
modItem.setSubtitle(info.getName() + ", " + Main.i18n("archive.version") + ": " + info.getVersion() + ", " + Main.i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + Main.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.Main;
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,37 +41,40 @@ 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.
*/
public ProfilePage(Profile profile) {
this.profile = profile;
String profileDisplayName = Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse("");
title = new SimpleStringProperty(this, "title",
profile == null ? Main.i18n("profile.new") : Main.i18n("profile") + " - " + profile.getName());
profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName);
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(""));
FXUtils.loadFXML(this, "/assets/fxml/profile.fxml");
txtProfileName.setText(Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse(""));
txtProfileName.setText(profileDisplayName);
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
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
@@ -84,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();
@@ -17,19 +17,21 @@
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
public final class SVG {
private SVG() {
}
private static Node createSVGPath(String d, String fill, double width, double height) {
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
path.setStyle("-fx-fill: " + fill + ";");
path.fillProperty().bind(fill);
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
@@ -41,87 +43,87 @@ public final class SVG {
// default fill: white, width: 20, height 20
public static Node gear(String fill, double width, double height) {
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
}
public static Node back(String fill, double width, double height) {
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
}
public static Node close(String fill, double width, double height) {
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
}
public static Node dotsVertical(String fill, double width, double height) {
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
}
public static Node delete(String fill, double width, double height) {
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
public static Node accountEdit(String fill, double width, double height) {
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
}
public static Node expand(String fill, double width, double height) {
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
}
public static Node collapse(String fill, double width, double height) {
public static Node collapse(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
}
public static Node navigate(String fill, double width, double height) {
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node launch(String fill, double width, double height) {
public static Node launch(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
}
public static Node script(String fill, double width, double height) {
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z", fill, width, height);
}
public static Node pencil(String fill, double width, double height) {
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
}
public static Node refresh(String fill, double width, double height) {
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
}
public static Node folderOpen(String fill, double width, double height) {
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
}
public static Node update(String fill, double width, double height) {
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
}
public static Node closeCircle(String fill, double width, double height) {
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z", fill, width, height);
}
public static Node checkCircle(String fill, double width, double height) {
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z", fill, width, height);
}
public static Node infoCircle(String fill, double width, double height) {
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", fill, width, height);
}
public static Node helpCircle(String fill, double width, double height) {
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z", fill, width, height);
}
public static Node alert(String fill, double width, double height) {
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
}
public static Node plus(String fill, double width, double height) {
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
@@ -18,6 +18,11 @@
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;
@@ -25,26 +30,31 @@ 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;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.FontComboBox;
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.Set;
import java.util.Objects;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("settings.launcher"));
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("settings.launcher"));
@FXML
private JFXTextField txtProxyHost;
@@ -77,11 +87,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private MultiFileItem backgroundItem;
@FXML
private MultiColorItem themeItem;
private StackPane themeColorPickerContainer;
@FXML
private JFXRadioButton chkNoProxy;
@FXML
private JFXRadioButton chkManualProxy;
private JFXCheckBox chkEnableProxy;
@FXML
private JFXRadioButton chkProxyHttp;
@FXML
@@ -98,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())));
@@ -140,102 +136,94 @@ 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, Main.i18n("update.tooltip"));
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
// background
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(Main.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
themeItem.loadChildren(Arrays.asList(
themeItem.createChildren(Main.i18n("color.blue"), Theme.BLUE),
themeItem.createChildren(Main.i18n("color.dark_blue"), Theme.DARK_BLUE),
themeItem.createChildren(Main.i18n("color.green"), Theme.GREEN),
themeItem.createChildren(Main.i18n("color.orange"), Theme.ORANGE),
themeItem.createChildren(Main.i18n("color.purple"), Theme.PURPLE),
themeItem.createChildren(Main.i18n("color.red"), Theme.RED)
));
if (Settings.INSTANCE.getTheme().isCustom())
themeItem.setColor(Color.web(Settings.INSTANCE.getTheme().getColor()));
themeItem.setToggleSelectedListener(newValue -> {
if (newValue.getUserData() != null) {
Settings.INSTANCE.setTheme((Theme) newValue.getUserData());
themeItem.setOnColorPickerChanged(null);
} else {
themeItem.setOnColorPickerChanged(color ->
Settings.INSTANCE.setTheme(Theme.custom(Theme.getColorDisplayName(color))));
}
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()));
CONFIG.setTheme(theme);
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
});
themeItem.getGroup().getToggles().stream().filter(it -> Settings.INSTANCE.getTheme() == it.getUserData() || Settings.INSTANCE.getTheme().isCustom() && themeItem.isCustomToggle(it)).findFirst().ifPresent(it -> it.setSelected(true));
Settings.INSTANCE.themeProperty().setChangedListenerAndOperate(it -> {
if (it.isCustom())
themeItem.setSubtitle(it.getName());
else
themeItem.setSubtitle(Main.i18n("color." + it.getName().toLowerCase()));
Controllers.getScene().getStylesheets().setAll(it.getStylesheets());
});
}
private void initBackgroundItemSubtitle() {
switch (Settings.INSTANCE.getBackgroundImageType()) {
case DEFAULT:
backgroundItem.setSubtitle(Main.i18n("launcher.background.default"));
break;
case CUSTOM:
backgroundItem.setSubtitle(Settings.INSTANCE.getBackgroundImage());
break;
}
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
}
public String getTitle() {
@@ -252,25 +240,25 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}
public void checkUpdate() {
btnUpdate.setVisible(Main.UPDATE_CHECKER.isOutOfDate());
btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate());
if (Main.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(Main.i18n("update.newest_version", Main.UPDATE_CHECKER.getNewVersion().toString()));
if (Launcher.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(Main.i18n("update.found"));
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else {
lblUpdateSub.setText(Main.i18n("update.latest"));
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(Main.i18n("update"));
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
}
}
@FXML
private void onUpdate() {
Main.UPDATE_CHECKER.checkOutdate();
Launcher.UPDATE_CHECKER.checkOutdate();
}
}
@@ -1,3 +1,20 @@
/*
* 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.ui;
import javafx.beans.property.SimpleStringProperty;
@@ -17,10 +17,13 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
@@ -28,11 +31,12 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Main;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Optional;
@@ -64,18 +68,119 @@ public final class VersionItem extends StackPane {
private EventHandler<? super MouseEvent> launchClickedHandler = null;
public VersionItem() {
FXUtils.loadFXML(this, "/assets/fxml/version-item.fxml");
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear("black", 15, 15));
btnUpdate.setGraphic(SVG.update("black", 15, 15));
btnLaunch.setGraphic(SVG.launch("black", 15, 15));
btnScript.setGraphic(SVG.script("black", 15, 15));
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 156);
FXUtils.installTooltip(btnSettings, Main.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Main.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Main.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Main.i18n("version.launch_script"));
content = new VBox();
{
header = new StackPane();
VBox.setVgrow(header, Priority.ALWAYS);
header.setPickOnBounds(false);
header.setPadding(new Insets(8));
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87);");
{
VBox headerContent = new VBox();
headerContent.setPadding(new Insets(8, 8, 0, 8));
{
lblVersionName = new Label();
lblVersionName.setStyle("-fx-font-size: 15;");
lblVersionName.setTextAlignment(TextAlignment.JUSTIFY);
lblVersionName.setWrapText(true);
headerContent.getChildren().add(lblVersionName);
lblGameVersion = new Label();
lblGameVersion.setStyle("-fx-font-size: 11;");
lblGameVersion.setTextAlignment(TextAlignment.JUSTIFY);
lblGameVersion.setWrapText(true);
headerContent.getChildren().add(lblGameVersion);
lblLibraries = new Label();
lblLibraries.setStyle("-fx-font-size: 10; -fx-text-fill: gray;");
lblLibraries.setTextAlignment(TextAlignment.JUSTIFY);
lblLibraries.setWrapText(true);
headerContent.getChildren().add(lblLibraries);
}
header.getChildren().add(headerContent);
}
content.getChildren().add(header);
body = new StackPane();
body.setStyle("-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;");
body.setMinHeight(40);
body.setPickOnBounds(false);
{
BorderPane bodyContent = new BorderPane();
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnSettings = new JFXButton();
btnSettings.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnSettings, 30);
FXUtils.setLimitHeight(btnSettings, 30);
hbox.getChildren().add(btnSettings);
btnUpdate = new JFXButton();
btnUpdate.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnUpdate, 30);
FXUtils.setLimitHeight(btnUpdate, 30);
hbox.getChildren().add(btnUpdate);
}
bodyContent.setLeft(hbox);
}
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnScript = new JFXButton();
btnScript.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnScript, 30);
FXUtils.setLimitHeight(btnScript, 30);
hbox.getChildren().add(btnScript);
btnLaunch = new JFXButton();
btnLaunch.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnLaunch, 30);
FXUtils.setLimitHeight(btnLaunch, 30);
hbox.getChildren().add(btnLaunch);
}
bodyContent.setRight(hbox);
}
body.getChildren().setAll(bodyContent);
}
content.getChildren().add(body);
}
getChildren().add(content);
icon = new StackPane();
StackPane.setAlignment(icon, Pos.TOP_RIGHT);
icon.setPickOnBounds(false);
{
iconView = new ImageView();
StackPane.setAlignment(iconView, Pos.CENTER_RIGHT);
StackPane.setMargin(iconView, new Insets(0, 12, 0, 0));
iconView.setImage(new Image("/assets/img/icon.png"));
icon.getChildren().add(iconView);
}
getChildren().add(icon);
}
public VersionItem() {
initializeComponents();
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear(Theme.blackFillBinding(), 15, 15));
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 15, 15));
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 15, 15));
btnScript.setGraphic(SVG.script(Theme.blackFillBinding(), 15, 15));
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);
@@ -17,11 +17,14 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
public final class VersionListItem extends StackPane {
@@ -33,7 +36,7 @@ public final class VersionListItem extends StackPane {
private Label lblGameVersion;
@FXML
private ImageView imageView;
private Runnable handler;
@FXML private JFXButton btnSettings;
public VersionListItem(String versionName) {
this(versionName, "");
@@ -48,13 +51,8 @@ public final class VersionListItem extends StackPane {
FXUtils.limitSize(imageView, 32, 32);
}
@FXML
private void onSettings() {
handler.run();
}
public void setOnSettingsButtonClicked(Runnable handler) {
this.handler = handler;
public void setOnSettingsButtonClicked(EventHandler<? super MouseEvent> handler) {
btnSettings.setOnMouseClicked(handler);
}
public void setVersionName(String versionName) {
@@ -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.Main;
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, Main.i18n("version.manage.remove"));
FXUtils.installTooltip(btnBrowseMenu, Main.i18n("settings.game.exploration"));
FXUtils.installTooltip(btnManagementMenu, Main.i18n("settings.game.management"));
FXUtils.installTooltip(btnExport, Main.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(Main.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(Main.i18n("version.manage.remove.confirm", version), Main.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(Main.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), Main.i18n("modpack.wizard"));
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), i18n("modpack.wizard"));
}
}
@@ -17,17 +17,19 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.image.Image;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -37,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.Collection;
import java.util.Optional;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -50,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;
@@ -77,33 +80,35 @@ public final class VersionSettingsController {
@FXML
private void initialize() {
lblPhysicalMemory.setText(Main.i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
lblPhysicalMemory.setText(i18n("settings.physical_memory") + ": " + OperatingSystem.TOTAL_MEMORY + "MB");
FXUtils.smoothScrolling(scroll);
Task.of(variables -> variables.set("list", JavaVersion.getJREs().values().stream().map(javaVersion ->
javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion)
).collect(Collectors.toList()))).subscribe(Schedulers.javafx(), variables ->
javaItem.loadChildren(variables.<Collection<Node>>get("list"))
);
Task.of(variables -> variables.set("list", JavaVersion.getJREs()))
.subscribe(Schedulers.javafx(), variables -> {
javaItem.loadChildren(
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
});
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
gameDirItem.loadChildren(Arrays.asList(
gameDirItem.createChildren(Main.i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(Main.i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
gameDirItem.createChildren(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(Main.i18n("settings.type.global"), true),
globalItem.createChildren(Main.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;
@@ -112,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());
@@ -127,10 +133,17 @@ public final class VersionSettingsController {
FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty());
FXUtils.unbindBoolean(chkNoGameCheck, lastVersionSetting.notCheckGameProperty());
FXUtils.unbindBoolean(chkNoCommon, lastVersionSetting.noCommonProperty());
FXUtils.unbindBoolean(chkNoJVMArgs, lastVersionSetting.noJVMArgsProperty());
FXUtils.unbindBoolean(chkShowLogs, lastVersionSetting.showLogsProperty());
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());
@@ -145,32 +158,21 @@ public final class VersionSettingsController {
FXUtils.bindBoolean(chkFullscreen, versionSetting.fullscreenProperty());
FXUtils.bindBoolean(chkNoGameCheck, versionSetting.notCheckGameProperty());
FXUtils.bindBoolean(chkNoCommon, versionSetting.noCommonProperty());
FXUtils.bindBoolean(chkNoJVMArgs, versionSetting.noJVMArgsProperty());
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(versionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.isCustomToggle(newValue)) {
versionSetting.setJava("Custom");
versionSetting.setDefaultJavaPath(null);
} else {
versionSetting.setJava(((JavaVersion) newValue.getUserData()).getVersion());
JavaVersion java = (JavaVersion) newValue.getUserData();
versionSetting.setJava(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
}
});
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
@@ -180,6 +182,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
@@ -189,7 +193,7 @@ public final class VersionSettingsController {
});
versionSetting.usesGlobalProperty().setChangedListenerAndOperate(it ->
globalItem.setSubtitle(Main.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,9 +208,44 @@ public final class VersionSettingsController {
lastVersionSetting = versionSetting;
initializeSelectedJava();
loadIcon();
}
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !javaItemsLoaded /* JREs are still being loaded */) {
return;
}
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;
}
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);
;
}
}
private void initJavaSubtitle(VersionSetting versionSetting) {
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
@@ -218,18 +257,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(Main.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);
@@ -247,7 +278,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,9 +19,11 @@ 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;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
public final class TransitionHandler implements AnimationHandler {
@@ -36,6 +38,12 @@ public final class TransitionHandler implements AnimationHandler {
public TransitionHandler(StackPane view) {
this.view = view;
currentNode = view.getChildren().stream().findFirst().orElse(null);
// prevent content overflow
Rectangle clip = new Rectangle();
clip.widthProperty().bind(view.widthProperty());
clip.heightProperty().bind(view.heightProperty());
view.setClip(clip);
}
@Override
@@ -71,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();
}
@@ -18,11 +18,13 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import org.jackhuang.hmcl.util.Lang;
/**
* @author huangyuhui
@@ -48,6 +50,13 @@ public class ClassTitle extends StackPane {
getStyleClass().add("class-title");
}
public ClassTitle(String text, Node rightNode) {
this(Lang.apply(new BorderPane(), borderPane -> {
borderPane.setLeft(Lang.apply(new VBox(), vBox -> vBox.getChildren().setAll(new Text(text))));
borderPane.setRight(rightNode);
}));
}
public Node getContent() {
return content;
}
@@ -60,9 +60,14 @@ public class ComponentList extends StackPane {
child.getStyleClass().add("options-list-item-ahead");
else
child.getStyleClass().add("options-list-item");
child.getProperties().put("node", node);
vbox.getChildren().add(child);
}
public void removeChildren(Node node) {
vbox.getChildren().removeIf(node1 -> node1.getProperties().get("node") == node);
}
public String getTitle() {
return title.get();
}
@@ -31,6 +31,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -77,7 +78,7 @@ public class ComponentListCell extends StackPane {
StackPane groupNode = new StackPane();
groupNode.getStyleClass().add("options-list-item-header");
Node expandIcon = SVG.expand("black", 10, 10);
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
@@ -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,45 +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.Main;
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("black", 15, 15));
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
FXUtils.installTooltip(right, Main.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();
@@ -75,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();
}
@@ -120,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);
}
}
@@ -21,21 +21,19 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Paint;
public class IconedItem extends RipplerContainer {
private final Node icon;
private final String text;
public IconedItem(Node icon, String text) {
super(createHBox(icon, text));
this.icon = icon;
this.text = text;
}
private static HBox createHBox(Node icon, String text) {
HBox hBox = new HBox();
icon.setMouseTransparent(true);
Label textLabel = new Label(text);
textLabel.setId("label");
textLabel.setAlignment(Pos.CENTER);
textLabel.setMouseTransparent(true);
hBox.getChildren().addAll(icon, textLabel);
@@ -43,4 +41,12 @@ public class IconedItem extends RipplerContainer {
hBox.setAlignment(Pos.CENTER_LEFT);
return hBox;
}
public void setText(String text) {
((Label) lookup("#label")).setText(text);
}
public void setTextFill(Paint paint) {
((Label) lookup("#label")).setTextFill(paint);
}
}
@@ -15,7 +15,10 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
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;
@@ -36,11 +39,11 @@ public final class ImagePickerItem extends BorderPane {
imageView.setPreserveRatio(true);
selectButton = new JFXButton();
selectButton.setGraphic(SVG.pencil("black", 15, 15));
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, Main.i18n("button.edit"));
FXUtils.installTooltip(selectButton, i18n("button.edit"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton);
@@ -18,18 +18,15 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
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 org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
import org.jackhuang.hmcl.util.FutureCallback;
public class InputDialogPane extends StackPane {
private final String text;
private final JFXDialog dialog;
@FXML
private JFXButton acceptButton;
@@ -39,17 +36,28 @@ public class InputDialogPane extends StackPane {
private JFXTextField textField;
@FXML
private Label content;
@FXML
private Label lblCreationWarning;
public InputDialogPane(String text, JFXDialog dialog, Consumer<String> onResult) {
this.text = text;
this.dialog = dialog;
public InputDialogPane(String text, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
cancelButton.setOnMouseClicked(e -> dialog.close());
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> {
onResult.accept(textField.getText());
dialog.close();
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,16 +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.Main;
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 = Main.i18n("message.info");
private static final String TITLE = i18n("message.info");
/**
* User Operation: Yes
@@ -123,30 +126,32 @@ public final class MessageBox {
}
public static int confirm(String message, String title, int option) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
default:
throw new IllegalArgumentException("Unrecognized message box option " + option);
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unrecognized button type:" + buttonType.get());
return FXUtils.runInUIThread(() -> {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(message);
switch (option) {
case YES_NO_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
break;
case YES_NO_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
break;
case OK_CANCEL_OPTION:
alert.getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
break;
default:
throw new IllegalArgumentException("Unrecognized message box option " + option);
}
Optional<ButtonType> buttonType = alert.showAndWait();
if (!buttonType.isPresent()) return CLOSED_OPTION;
else if (buttonType.get() == ButtonType.OK) return OK_OPTION;
else if (buttonType.get() == ButtonType.YES) return YES_OPTION;
else if (buttonType.get() == ButtonType.NO) return NO_OPTION;
else if (buttonType.get() == ButtonType.CANCEL) return CANCEL_OPTION;
else throw new IllegalStateException("Unexpected button type:" + buttonType.get());
});
}
public static Optional<String> input(String message) {
@@ -18,19 +18,19 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
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;
public final class MessageDialogPane extends StackPane {
private boolean closingDialog = true;
@FXML
private JFXButton acceptButton;
@@ -45,7 +45,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, JFXDialog dialog, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, int type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -53,8 +53,7 @@ public final class MessageDialogPane extends StackPane {
content.setText(text);
acceptButton.setOnMouseClicked(e -> {
if (closingDialog)
dialog.close();
fireEvent(new DialogCloseEvent());
Optional.ofNullable(onAccept).ifPresent(Runnable::run);
});
@@ -62,42 +61,37 @@ public final class MessageDialogPane extends StackPane {
switch (type) {
case MessageBox.INFORMATION_MESSAGE:
graphic.setGraphic(SVG.infoCircle("black", 40, 40));
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.ERROR_MESSAGE:
graphic.setGraphic(SVG.closeCircle("black", 40, 40));
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.FINE_MESSAGE:
graphic.setGraphic(SVG.checkCircle("black", 40, 40));
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.WARNING_MESSAGE:
graphic.setGraphic(SVG.alert("black", 40, 40));
graphic.setGraphic(SVG.alert(Theme.blackFillBinding(), 40, 40));
break;
case MessageBox.QUESTION_MESSAGE:
graphic.setGraphic(SVG.helpCircle("black", 40, 40));
graphic.setGraphic(SVG.helpCircle(Theme.blackFillBinding(), 40, 40));
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
}
public MessageDialogPane(String text, String title, JFXDialog dialog, Runnable onAccept, Runnable onCancel) {
this(text, title, dialog, MessageBox.QUESTION_MESSAGE, onAccept);
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageBox.QUESTION_MESSAGE, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
if (closingDialog)
dialog.close();
fireEvent(new DialogCloseEvent());
Optional.ofNullable(onCancel).ifPresent(Runnable::run);
});
acceptButton.setText(Main.i18n("button.yes"));
cancelButton.setText(Main.i18n("button.no"));
acceptButton.setText(i18n("button.yes"));
cancelButton.setText(i18n("button.no"));
actions.getChildren().add(cancelButton);
}
public void disableClosingDialog() {
closingDialog = false;
}
}
@@ -5,26 +5,25 @@ import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Main;
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", Main.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Main.i18n("selector.choose_file"));
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", 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();
@@ -32,26 +32,26 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.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", Main.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Main.i18n("selector.choose_file"));
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", 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 SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
@@ -69,7 +69,7 @@ public class MultiFileItem extends ComponentList {
BorderPane.setAlignment(txtCustom, Pos.CENTER_RIGHT);
btnSelect.setGraphic(SVG.folderOpen("black", 15, 15));
btnSelect.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
btnSelect.setOnMouseClicked(e -> {
if (isDirectory()) {
DirectoryChooser chooser = new DirectoryChooser();
@@ -111,10 +111,6 @@ public class MultiFileItem extends ComponentList {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
});
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
}
public Node createChildren(String title) {
@@ -152,7 +148,7 @@ public class MultiFileItem extends ComponentList {
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(Main.i18n(getChooserTitle()));
chooser.setTitle(i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
@@ -229,16 +225,4 @@ public class MultiFileItem extends ComponentList {
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public String getTooltip() {
return tooltip.get();
}
public StringProperty tooltipProperty() {
return tooltip;
}
public void setTooltip(String tooltip) {
this.tooltip.set(tooltip);
}
}
@@ -0,0 +1,61 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import 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);
}
}
@@ -0,0 +1,49 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Stack;
public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
public void push(Node node) {
stack.push(node);
getChildren().setAll(node);
Logging.LOG.info(this + " " + stack);
}
public void pop(Node node) {
boolean flag = stack.remove(node);
if (stack.isEmpty())
getChildren().setAll();
else
getChildren().setAll(stack.peek());
Logging.LOG.info(this + " " + stack + ", removed: " + flag + ", object: " + node);
}
public boolean isEmpty() {
return stack.isEmpty();
}
}
@@ -23,15 +23,17 @@ import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
import java.util.function.Consumer;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Runnable onCancel;
private Consumer<Region> onCancel;
@FXML
private JFXProgressBar progressBar;
@@ -44,20 +46,22 @@ public class TaskExecutorDialogPane extends StackPane {
@FXML
private TaskListPane taskListPane;
public TaskExecutorDialogPane(Runnable cancel) {
public TaskExecutorDialogPane(Consumer<Region> cancel) {
FXUtils.loadFXML(this, "/assets/fxml/task-dialog.fxml");
setCancel(cancel);
btnCancel.setOnMouseClicked(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
onCancel.run();
onCancel.accept(this);
});
}
public void setExecutor(TaskExecutor executor) {
this.executor = executor;
taskListPane.setExecutor(executor);
if (executor != null)
taskListPane.setExecutor(executor);
}
public StringProperty titleProperty() {
@@ -91,7 +95,7 @@ public class TaskExecutorDialogPane extends StackPane {
progressBar.setProgress(progress);
}
public void setCancel(Runnable onCancel) {
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;
JFXUtilities.runInFX(() -> btnCancel.setDisable(onCancel == null));
@@ -19,13 +19,14 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.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;
@@ -33,12 +34,12 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
@Override
default void handleTask(Map<String, Object> settings, Task task) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(() -> {
Controllers.closeDialog();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(it -> {
it.fireEvent(new DialogCloseEvent());
Controllers.navigate(null);
});
pane.setTitle(Main.i18n("message.doing"));
pane.setTitle(i18n("message.doing"));
pane.setProgress(Double.MAX_VALUE);
if (settings.containsKey("title")) {
Object title = settings.get("title");
@@ -60,20 +61,24 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
JFXUtilities.runInFX(() -> Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null)));
else if (!settings.containsKey("forbid_success_message"))
JFXUtilities.runInFX(() -> Controllers.dialog(Main.i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null)));
} else {
if (executor.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(executor.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
JFXUtilities.runInFX(() -> Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null)));
else if (!settings.containsKey("forbid_failure_message"))
JFXUtilities.runInFX(() -> Controllers.dialog(appendix, Main.i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null)));
}
JFXUtilities.runInFX(() -> {
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(i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null));
} else {
if (executor.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(executor.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
}
});
}
});
pane.setExecutor(executor);
@@ -23,7 +23,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameAssetRefreshTask;
@@ -31,13 +31,11 @@ import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.CurseInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.HashMap;
import java.util.Map;
@@ -60,32 +58,34 @@ public final class TaskListPane extends StackPane {
}
@Override
public void onReady(Task task) {
public void onRunning(Task task) {
if (!task.getSignificance().shouldShow())
return;
if (task instanceof GameAssetRefreshTask) {
task.setName(Main.i18n("assets.download"));
task.setName(i18n("assets.download"));
} else if (task instanceof GameAssetDownloadTask) {
task.setName(Main.i18n("assets.download_all"));
task.setName(i18n("assets.download_all"));
} else if (task instanceof ForgeInstallTask) {
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.forge")));
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.liteloader")));
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
} else if (task instanceof OptiFineInstallTask) {
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.optifine")));
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
} else if (task instanceof CurseCompletionTask) {
task.setName(Main.i18n("modpack.type.curse.completion"));
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
task.setName(i18n("modpack.installing"));
} else if (task instanceof CurseInstallTask) {
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.curse")));
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.multimc")));
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.hmcl")));
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof HMCLModpackExportTask) {
task.setName(Main.i18n("modpack.export"));
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(Main.i18n("modpack.scan"));
task.setName(i18n("modpack.scan"));
}
ProgressListNode node = new ProgressListNode(task);
@@ -1,7 +1,6 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
@@ -17,10 +16,6 @@ public class URLValidator extends ValidatorBase {
super();
}
public URLValidator(@NamedArg("message") String message) {
super(message);
}
public ObservableList<String> getProtocols() {
return protocols;
}
@@ -22,17 +22,18 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
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 static org.jackhuang.hmcl.Main.i18n;
import java.util.Optional;
class AdditionalInstallersPage extends StackPane implements WizardPage {
private final InstallerWizardProvider provider;
@@ -98,29 +99,33 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Main.i18n("settings.tabs.installers");
return i18n("settings.tabs.installers");
}
private String getVersion(String id) {
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion<?>) it).map(RemoteVersion::getSelfVersion).orElse(null);
}
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(Main.i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
btnForge.setDisable(provider.getForge() != null);
if (provider.getForge() != null || controller.getSettings().containsKey("forge"))
lblForge.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), controller.getSettings().get("forge")));
lblForge.setText(i18n("install.installer.version", i18n("install.installer.forge")) + ": " + Lang.nonNull(provider.getForge(), getVersion("forge")));
else
lblForge.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.forge")));
lblForge.setText(i18n("install.installer.not_installed", i18n("install.installer.forge")));
btnLiteLoader.setDisable(provider.getLiteLoader() != null);
if (provider.getLiteLoader() != null || controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), controller.getSettings().get("liteloader")));
lblLiteLoader.setText(i18n("install.installer.version", i18n("install.installer.liteloader")) + ": " + Lang.nonNull(provider.getLiteLoader(), getVersion("liteloader")));
else
lblLiteLoader.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.liteloader")));
lblLiteLoader.setText(i18n("install.installer.not_installed", i18n("install.installer.liteloader")));
btnOptiFine.setDisable(provider.getOptiFine() != null);
if (provider.getOptiFine() != null || controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), controller.getSettings().get("optifine")));
lblOptiFine.setText(i18n("install.installer.version", i18n("install.installer.optifine")) + ": " + Lang.nonNull(provider.getOptiFine(), getVersion("optifine")));
else
lblOptiFine.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.optifine")));
lblOptiFine.setText(i18n("install.installer.not_installed", i18n("install.installer.optifine")));
}
@@ -18,9 +18,10 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile;
@@ -29,11 +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.Main.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;
@@ -48,16 +49,16 @@ public final class DownloadWizardProvider implements WizardProvider {
GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name"));
builder.gameVersion((String) settings.get("game"));
builder.gameVersion(((RemoteVersion<?>) settings.get("game")).getGameVersion());
if (settings.containsKey("forge"))
builder.version("forge", (String) settings.get("forge"));
builder.version((RemoteVersion<?>) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version("liteloader", (String) settings.get("liteloader"));
builder.version((RemoteVersion<?>) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version("optifine", (String) settings.get("optifine"));
builder.version((RemoteVersion<?>) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions());
}
@@ -66,9 +67,9 @@ public final class DownloadWizardProvider implements WizardProvider {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null;
File selected = Lang.get(settings, ModpackPage.MODPACK_FILE, File.class).orElse(null);
Modpack modpack = Lang.get(settings, ModpackPage.MODPACK_CURSEFORGE_MANIFEST, Modpack.class).orElse(null);
String name = Lang.get(settings, ModpackPage.MODPACK_NAME, String.class).orElse(null);
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
if (selected == null || modpack == null || name == null) return null;
return ModpackHelper.getInstallTask(profile, selected, name, modpack);
@@ -76,8 +77,8 @@ public final class DownloadWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", Main.i18n("install.success"));
settings.put("failure_message", Main.i18n("install.failed"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
switch (Lang.parseInt(settings.get(InstallTypePage.INSTALL_TYPE), -1)) {
case 0: return finishVersionDownloadingAsync(settings);
@@ -100,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,23 +20,24 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXListView;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.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;
public final class InstallTypePage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML private JFXListView<Object> list;
public InstallTypePage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/dltype.fxml");
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
controller.getSettings().put(INSTALL_TYPE, list.getSelectionModel().getSelectedIndex());
controller.onNext();
});
@@ -49,7 +50,7 @@ public final class InstallTypePage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Main.i18n("install.select");
return i18n("install.select");
}
public static final String INSTALL_TYPE = "INSTALL_TYPE";
@@ -18,13 +18,15 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
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;
@@ -79,19 +81,19 @@ public final class InstallerWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", Main.i18n("install.success"));
settings.put("failure_message", Main.i18n("install.failed"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
Task ret = Task.empty();
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync(gameVersion, version, "forge", (String) settings.get("forge")));
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync(gameVersion, version, "liteloader", (String) settings.get("liteloader")));
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync(gameVersion, version, "optifine", (String) settings.get("optifine")));
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -23,21 +23,21 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.Validator;
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;
public class InstallersPage extends StackPane implements WizardPage {
private final WizardController controller;
private final GameRepository repository;
private final DownloadProvider downloadProvider;
@FXML
private VBox list;
@@ -71,56 +71,58 @@ public class InstallersPage extends StackPane implements WizardPage {
public InstallersPage(WizardController controller, GameRepository repository, DownloadProvider downloadProvider) {
this.controller = controller;
this.repository = repository;
this.downloadProvider = downloadProvider;
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
String gameVersion = (String) controller.getSettings().get("game");
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(Main.i18n("install.new_game.already_exists"));
hasVersion.setMessage(i18n("install.new_game.already_exists"));
txtName.getValidators().add(hasVersion);
txtName.textProperty().addListener(e -> btnInstall.setDisable(!txtName.validate()));
txtName.setText(gameVersion);
btnForge.setOnMouseClicked(e -> {
controller.getSettings().put(INSTALLER_TYPE, 0);
controller.onNext(new VersionsPage(controller, Main.i18n("install.installer.choose", Main.i18n("install.installer.forge")), gameVersion, downloadProvider, "forge", () -> controller.onPrev(false)));
controller.onNext(new VersionsPage(controller, 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, Main.i18n("install.installer.choose", Main.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, Main.i18n("install.installer.choose", Main.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 Main.i18n("install.new_game");
return i18n("install.new_game");
}
private String getVersion(String id) {
return ((RemoteVersion<?>) controller.getSettings().get(id)).getSelfVersion();
}
@Override
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(Main.i18n("install.new_game.current_game_version") + ": " + controller.getSettings().get("game"));
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + getVersion("game"));
if (controller.getSettings().containsKey("forge"))
lblForge.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.forge")) + ": " + controller.getSettings().get("forge"));
lblForge.setText(i18n("install.installer.version", i18n("install.installer.forge")) + ": " + getVersion("forge"));
else
lblForge.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.forge")));
lblForge.setText(i18n("install.installer.not_installed", i18n("install.installer.forge")));
if (controller.getSettings().containsKey("liteloader"))
lblLiteLoader.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.liteloader")) + ": " + controller.getSettings().get("liteloader"));
lblLiteLoader.setText(i18n("install.installer.version", i18n("install.installer.liteloader")) + ": " + getVersion("liteloader"));
else
lblLiteLoader.setText(Main.i18n("install.installer.not_installed", Main.i18n("install.installer.liteloader")));
lblLiteLoader.setText(i18n("install.installer.not_installed", i18n("install.installer.liteloader")));
if (controller.getSettings().containsKey("optifine"))
lblOptiFine.setText(Main.i18n("install.installer.version", Main.i18n("install.installer.optifine")) + ": " + controller.getSettings().get("optifine"));
lblOptiFine.setText(i18n("install.installer.version", i18n("install.installer.optifine")) + ": " + getVersion("optifine"));
else
lblOptiFine.setText(Main.i18n("install.installer.not_installed", Main.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.Main;
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(Main.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.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(Main.i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(Main.i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
new Validator(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(Main.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(Main.i18n("modpack.wizard.step.3"));
stage.setTitle(i18n("modpack.wizard.step.3"));
stage.showAndWait();
}
}
@Override
public String getTitle() {
return Main.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";
}
@@ -40,14 +40,13 @@ import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public final class VersionsPage extends StackPane implements WizardPage, Refreshable {
private final WizardController controller;
private final String gameVersion;
private final DownloadProvider downloadProvider;
private final String libraryId;
private final Runnable callback;
private final String title;
@FXML
@@ -72,12 +71,10 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private TaskExecutor executor;
public VersionsPage(WizardController controller, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.controller = controller;
this.title = title;
this.gameVersion = gameVersion;
this.downloadProvider = downloadProvider;
this.libraryId = libraryId;
this.callback = callback;
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
@@ -92,8 +89,10 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
chkSnapshot.selectedProperty().addListener(listener);
chkOld.selectedProperty().addListener(listener);
list.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
controller.getSettings().put(libraryId, newValue.getRemoteVersion().getSelfVersion());
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
controller.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem().getRemoteVersion());
callback.run();
});
refresh();
@@ -114,6 +113,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
}
else return true;
})
.filter(Objects::nonNull)
.sorted()
.map(VersionsPageItem::new).collect(Collectors.toList());
}
@@ -23,12 +23,15 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
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;
/**
* @author huangyuhui
@@ -47,7 +50,7 @@ public final class VersionsPageItem extends StackPane {
private StackPane imageViewContainer;
public VersionsPageItem(RemoteVersion<?> remoteVersion) {
this.remoteVersion = remoteVersion;
this.remoteVersion = Objects.requireNonNull(remoteVersion);
FXUtils.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
lblSelfVersion.setText(remoteVersion.getSelfVersion());
@@ -55,15 +58,15 @@ public final class VersionsPageItem extends StackPane {
if (remoteVersion.getTag() instanceof GameRemoteVersionTag) {
switch (((GameRemoteVersionTag) remoteVersion.getTag()).getType()) {
case RELEASE:
lblGameVersion.setText(Main.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(Main.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(Main.i18n("version.game.old"));
lblGameVersion.setText(i18n("version.game.old"));
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
}
@@ -18,17 +18,27 @@
package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
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.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.ZipEngine;
import java.io.File;
import java.nio.file.Files;
import java.util.Collection;
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;
@@ -46,16 +56,65 @@ public final class ExportWizardProvider implements WizardProvider {
public Object finish(Map<String, Object> settings) {
@SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER) && launcherJar != null;
return new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(
(String) settings.get(ModpackInfoPage.MODPACK_NAME),
(String) settings.get(ModpackInfoPage.MODPACK_AUTHOR),
(String) settings.get(ModpackInfoPage.MODPACK_VERSION),
null,
(String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION),
null
), (File) settings.get(ModpackInfoPage.MODPACK_FILE));
return new Task() {
Task dependency = null;
@Override
public void execute() throws Exception {
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(
(String) settings.get(ModpackInfoPage.MODPACK_NAME),
(String) settings.get(ModpackInfoPage.MODPACK_AUTHOR),
(String) settings.get(ModpackInfoPage.MODPACK_VERSION),
null,
(String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION),
null
), tempModpack);
if (includeLauncher) {
dependency = dependency.then(Task.of(() -> {
boolean flag = true;
try (ZipEngine zip = new ZipEngine(modpackFile)) {
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();
if (bg.isDirectory())
zip.putDirectory(bg);
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
}));
}
}
@Override
public Collection<? extends Task> getDependencies() {
return Collections.singleton(dependency);
}
};
}
@Override
@@ -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.Main;
import org.jackhuang.hmcl.game.ModAdviser;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -33,16 +33,17 @@ import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
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.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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
*/
@@ -66,6 +67,9 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
}
private CheckBoxTreeItem<String> getTreeItem(File file, String basePath) {
if (!file.exists())
return null;
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
if (basePath.length() > "minecraft/".length()) {
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (file.isDirectory() ? "/" : ""), file.isDirectory());
@@ -147,23 +151,23 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
@Override
public String getTitle() {
return Main.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 = Lang.mapOf(
new Pair<>("minecraft/servers.dat", Main.i18n("modpack.files.servers_dat")),
new Pair<>("minecraft/saves", Main.i18n("modpack.files.saves")),
new Pair<>("minecraft/mods", Main.i18n("modpack.files.mods")),
new Pair<>("minecraft/config", Main.i18n("modpack.files.config")),
new Pair<>("minecraft/liteconfig", Main.i18n("modpack.files.liteconfig")),
new Pair<>("minecraft/resourcepacks", Main.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/resources", Main.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/options.txt", Main.i18n("modpack.files.options_txt")),
new Pair<>("minecraft/optionsshaders.txt", Main.i18n("modpack.files.optionsshaders_txt")),
new Pair<>("minecraft/mods/VoxelMods", Main.i18n("modpack.files.mods.voxelmods")),
new Pair<>("minecraft/dumps", Main.i18n("modpack.files.dumps")),
new Pair<>("minecraft/blueprints", Main.i18n("modpack.files.blueprints")),
new Pair<>("minecraft/scripts", Main.i18n("modpack.files.scripts"))
private static final Map<String, String> TRANSLATION = mapOf(
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"))
);
}
@@ -26,15 +26,17 @@ import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Settings;
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;
import java.util.Map;
import java.util.Optional;
@@ -45,12 +47,16 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@FXML
private JFXTextField txtModpackName;
@FXML
private JFXTextField txtModpackAuthor;@FXML
private JFXTextField txtModpackVersion;@FXML
private JFXTextField txtModpackAuthor;
@FXML
private JFXTextField txtModpackVersion;
@FXML
private JFXTextArea txtModpackDescription;
@FXML
private JFXToggleButton chkIncludeLauncher;@FXML
private JFXButton btnNext;@FXML
private JFXToggleButton chkIncludeLauncher;
@FXML
private JFXButton btnNext;
@FXML
private ScrollPane scroll;
public ModpackInfoPage(WizardController controller, String version) {
@@ -63,6 +69,10 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
txtModpackVersion.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.setText(Optional.ofNullable(Settings.INSTANCE.getSelectedAccount()).map(Account::getUsername).orElse(""));
lblVersionName.setText(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();
if (launcherJar == null)
chkIncludeLauncher.setDisable(true);
}
private void checkValidation() {
@@ -72,8 +82,8 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@FXML
private void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(Main.i18n("modpack.wizard.step.initialization.save"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Main.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);
@@ -100,7 +110,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@Override
public String getTitle() {
return Main.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);
@@ -18,25 +18,27 @@
package org.jackhuang.hmcl.upgrade;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.Main;
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.Schedulers;
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.net.*;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
@@ -49,22 +51,24 @@ import java.util.zip.GZIPInputStream;
*/
public class AppDataUpgrader extends IUpgrader {
private boolean launchNewerVersion(List<String> args, File jar) throws IOException, PrivilegedActionException {
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) {
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> {
new URLClassLoader(new URL[]{jar.toURI().toURL()},
ClassLoader.getSystemClassLoader().getParent()).loadClass(mainClass)
.getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
return null;
});
return true;
if (mainClass == null)
throw new ClassNotFoundException("Main-Class not found in manifest");
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
ClassLoader pre = Thread.currentThread().getContextClassLoader();
try {
Logging.stop();
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
Thread.currentThread().setContextClassLoader(now);
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
} finally {
Logging.start(Launcher.LOG_DIRECTORY);
Thread.currentThread().setContextClassLoader(pre);
}
}
return false;
}
@Override
@@ -80,15 +84,18 @@ public class AppDataUpgrader extends IUpgrader {
String j = m.get("loc");
if (j != null) {
File jar = new File(j);
if (jar.exists() && launchNewerVersion(args, jar))
if (jar.exists()) {
launchNewerVersion(args, jar);
System.exit(0);
}
}
}
}
} catch (JsonParseException ex) {
f.delete();
} catch (IOException | PrivilegedActionException 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.
}
}
@@ -97,65 +104,66 @@ public class AppDataUpgrader extends IUpgrader {
if (!(ver instanceof IntVersionNumber))
return;
IntVersionNumber version = (IntVersionNumber) ver;
checker.requestDownloadLink().then(Task.of(Schedulers.javafx(), variables -> {
checker.requestDownloadLink().then(Task.of(variables -> {
Map<String, String> map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
if (MessageBox.confirm(Main.i18n("update.newest_version") + version.toString() + "\n"
+ Main.i18n("update.should_open_link"),
MessageBox.YES_NO_OPTION) == MessageBox.YES_OPTION)
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Controllers.dialog(Main.i18n("message.downloading"));
if (new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash).test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
Controllers.closeDialog();
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Controllers.dialog(Main.i18n("message.downloading"));
if (new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash).test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
Controllers.closeDialog();
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Main.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
if (url == null)
url = Main.PUBLISH;
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(Main.i18n("update.no_browser"));
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
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, 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(() -> region.get().fireEvent(new DialogCloseEvent()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
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, 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(() -> region.get().fireEvent(new DialogCloseEvent()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Launcher.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(i18n("update.no_browser"));
}
}
})).start();
}
public static class AppDataUpgraderPackGzTask extends Task {
public static final File BASE_FOLDER = Main.HMCL_DIRECTORY;
public static final File BASE_FOLDER = Launcher.HMCL_DIRECTORY;
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
public static File getSelf(String ver) {
@@ -177,7 +185,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Arrays.asList(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -187,7 +195,7 @@ public class AppDataUpgrader extends IUpgrader {
if (!FileUtils.makeDirectory(f.getParentFile()))
throw new IOException("Failed to make directories: " + f.getParent());
for (int i = 0; f.exists(); i++)
for (int i = 0; f.exists() && !f.delete(); i++)
f = new File(BASE_FOLDER, "HMCL-" + newestVersion + (i > 0 ? "-" + i : "") + ".jar");
if (!f.createNewFile())
throw new IOException("Failed to create new file: " + f);
@@ -205,7 +213,7 @@ public class AppDataUpgrader extends IUpgrader {
public static class AppDataUpgraderJarTask extends Task {
public static final File BASE_FOLDER = Main.getWorkingDirectory("hmcl");
public static final File BASE_FOLDER = OperatingSystem.getWorkingDirectory("hmcl");
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
public static File getSelf(String ver) {
@@ -227,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
@@ -17,19 +17,26 @@
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.Main;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
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.util.Charsets;
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;
import java.util.List;
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;
/**
*
* @author huangyuhui
@@ -51,8 +58,12 @@ public class NewFileUpgrader extends IUpgrader {
URL url = requestDownloadLink();
if (url == null) return;
File newf = new File(url.getFile());
Controllers.dialog(Main.i18n("message.downloading"));
if (new FileDownloadTask(url, newf).test()) {
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, i18n("message.downloading"), "", null)));
if (executor.test()) {
try {
new ProcessBuilder(newf.getCanonicalPath(), "--removeOldLauncher", getRealPath())
.directory(new File("").getAbsoluteFile())
@@ -62,7 +73,7 @@ public class NewFileUpgrader extends IUpgrader {
}
System.exit(0);
}
Controllers.closeDialog();
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
}
private static String getRealPath() {
@@ -70,7 +81,7 @@ public class NewFileUpgrader extends IUpgrader {
File file = new File(realPath);
realPath = file.getAbsolutePath();
try {
realPath = java.net.URLDecoder.decode(realPath, Charsets.DEFAULT_CHARSET.name());
realPath = java.net.URLDecoder.decode(realPath, UTF_8.name());
} catch (Exception e) {
e.printStackTrace();
}
@@ -18,18 +18,23 @@
package org.jackhuang.hmcl.upgrade;
import com.google.gson.JsonSyntaxException;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.OutOfDateEvent;
import org.jackhuang.hmcl.task.GetTask;
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;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
@@ -65,20 +70,29 @@ public final class UpdateChecker {
*/
public TaskResult<VersionNumber> process(final boolean showMessage) {
return new TaskResult<VersionNumber>() {
GetTask http = new GetTask(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update.php?version=" + Launcher.VERSION));
@Override
public Collection<? extends Task> getDependents() {
return value == null ? Collections.singleton(http) : Collections.emptyList();
}
@Override
public void execute() throws Exception {
if (Main.VERSION.contains("@"))
if (isDevelopmentVersion(Launcher.VERSION)) {
LOG.info("Current version is a development version, skip updating");
return;
}
if (value == null) {
versionString = NetworkUtils.doGet(NetworkUtils.toURL("http://huangyuhui.duapp.com/hmcl/update.php?version=" + Main.VERSION));
versionString = http.getResult();
value = VersionNumber.asVersion(versionString);
}
if (value == null) {
Logging.LOG.warning("Unable to check update...");
LOG.warning("Unable to check update...");
if (showMessage)
MessageBox.show(Main.i18n("update.failed"));
MessageBox.show(i18n("update.failed"));
} else if (base.compareTo(value) < 0)
outOfDate = true;
if (outOfDate)
@@ -92,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!
@@ -113,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("http://huangyuhui.duapp.com/update_link.php?type=hmcl")), Map.class);
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);
}
@@ -134,7 +154,7 @@ public final class UpdateChecker {
public void checkOutdate() {
if (outOfDate)
if (EventBus.EVENT_BUS.fireEvent(new OutOfDateEvent(this, getNewVersion())) != Event.Result.DENY) {
Main.UPGRADER.download(this, getNewVersion());
Launcher.UPGRADER.download(this, getNewVersion());
}
}
}
@@ -18,15 +18,20 @@
package org.jackhuang.hmcl.util;
import javafx.application.Platform;
import org.jackhuang.hmcl.Main;
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,17 +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", Main.i18n("crash.NoClassDefFound"));
put("UnsatisfiedLinkError", Main.i18n("crash.user_fault"));
put("java.lang.NoClassDefFoundError", Main.i18n("crash.NoClassDefFound"));
put("java.lang.VerifyError", Main.i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", Main.i18n("crash.NoClassDefFound"));
put("java.lang.IncompatibleClassChangeError", Main.i18n("crash.NoClassDefFound"));
put("java.lang.ClassFormatError", Main.i18n("crash.NoClassDefFound"));
put("java.lang.OutOfMemoryError", "FUCKING MEMORY LIMIT!");
put("Trampoline", Main.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?");
}
};
@@ -55,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;
@@ -67,56 +74,56 @@ 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 {
StringBuilder builder = new StringBuilder();
builder.append("---- Hello Minecraft! Crash Report ----\n");
builder.append(" Version: " + Main.VERSION + "\n");
builder.append(" Time: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n");
builder.append(" Thread: ").append(t.toString()).append("\n");
builder.append("\n Content: \n ");
builder.append(stackTrace).append("\n\n");
builder.append("-- System Details --\n");
builder.append(" Operating System: ").append(System.getProperty("os.name")).append(' ').append(OperatingSystem.SYSTEM_VERSION).append("\n");
builder.append(" Java Version: ").append(System.getProperty("java.version")).append(", ").append(System.getProperty("java.vendor")).append("\n");
builder.append(" Java VM Version: ").append(System.getProperty("java.vm.name")).append(" (").append(System.getProperty("java.vm.info")).append("), ").append(System.getProperty("java.vm.vendor")).append("\n");
String text = builder.toString();
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
return;
Logging.LOG.log(Level.SEVERE, text);
if (CAUGHT_EXCEPTIONS.contains(stackTrace))
return;
CAUGHT_EXCEPTIONS.add(stackTrace);
if (checkThrowable(e) && !text.contains("OpenJDK")) {
String text = "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Launcher.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + t.toString() + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" 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";
LOG.log(Level.SEVERE, text);
if (checkThrowable(e)) {
Platform.runLater(() -> new CrashWindow(text).show());
if (!Main.UPDATE_CHECKER.isOutOfDate())
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<>();
map.put("crash_report", text);
map.put("version", Main.VERSION);
map.put("version", Launcher.VERSION);
map.put("log", Logging.getLogs());
try {
String response = NetworkUtils.doPost(NetworkUtils.toURL("http://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,12 +15,10 @@
* 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.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@@ -54,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 +1,5 @@
.root {
-fx-base-color: %base-color%;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: white;
-fx-base-text-fill: %font-color%;
}
Loaded 100 of 251 files, more files were not shown because too many files have changed in this diff. Show more