Compare commits

...
594 Commits
Author SHA1 Message Date
huanghongxun 340ee751e8 Suppress VersionNotFoundException 2018-10-14 19:10:04 +08:00
huanghongxun cf70f72f9c Fix #471 2018-10-14 19:04:27 +08:00
huanghongxun 16ea88a659 Fixed failing to download file when cached entry was removed by user 2018-10-14 19:04:21 +08:00
huanghongxun eaa6790013 Update welcome.png 2018-10-14 18:50:47 +08:00
huanghongxun af571d533d Maintain statement when no account 2018-10-14 12:45:52 +08:00
huanghongxun 004c3c588a Log navigation to catch bugs 2018-10-14 12:39:22 +08:00
huanghongxun 2a44167c22 Suppress NPE 2018-10-14 12:39:03 +08:00
huanghongxun cf6b735026 Removed JFXDrawer and JFXHamburger in JFoenix.jar 2018-10-14 12:20:03 +08:00
huanghongxun 5918b04b48 Prevent from calling register and fireEvent at the same time 2018-10-13 11:17:16 +08:00
huanghongxun 8004cb3a64 Change default red to darker one 2018-10-11 22:26:50 +08:00
huanghongxun 9981423927 Change separator fill in title bar and launch button to reduce color difference 2018-10-11 22:20:41 +08:00
huanghongxun 0e5250dfb3 Satisfy requirements metioned in #469 2018-10-11 22:06:34 +08:00
huanghongxun 2dc956b357 Fix request mojang server for skin info every time 2018-10-11 22:05:24 +08:00
huanghongxun b8bd0a08e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-11 14:05:35 +08:00
huanghongxun fb31eebcf2 Add a warning that forge 2760 is not compatible with liteloader 2018-10-11 14:04:52 +08:00
huanghongxun 3c8689a3f1 Close launching progress dialog when game crashed before opening window 2018-10-11 13:51:37 +08:00
huanghongxun 2957d66dd6 Add test game button in versions page 2018-10-11 13:41:11 +08:00
huanghongxun 6f6efe0874 Rename DecoratorControl to Decorator 2018-10-11 13:32:39 +08:00
huanghongxun 355fc04069 Drop modpack file to install 2018-10-11 13:32:11 +08:00
huanghongxun b4738f383b Fix not on FX thread 2018-10-11 12:59:06 +08:00
huanghongxun b12711969c Rewrite encode location 2018-10-10 16:37:04 +08:00
huanghongxun 94727353c4 Fix response code 400 when downloading mods from curse 2018-10-10 15:27:27 +08:00
huanghongxun 5eb7449b74 Delete old files if etag was updated 2018-10-10 01:36:31 +08:00
huanghongxun dae41dfb30 Add an entrance directly to add authlib-injector server 2018-10-09 22:15:46 +08:00
huanghongxun 07a701e572 Cache asset indexes by ETag cache 2018-10-09 20:22:54 +08:00
huanghongxun 5323aaed6d ETag caching 2018-10-09 20:16:16 +08:00
huanghongxun daf659373f Removed static login method 2018-10-08 21:23:01 +08:00
huanghongxun 0eb81c29df Update I18N 2018-10-08 01:31:02 +08:00
huanghongxun 8aea89c103 Login when launcher opened to reduce launching time 2018-10-08 01:27:48 +08:00
huanghongxun f1d97454c2 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-06 12:42:50 +08:00
huanghongxun ffb6a45e28 Remove toLowerCase 2018-10-06 12:42:30 +08:00
huanghongxun f0d1c9b361 Merge pull request #468 from yushijinhun/authlib-injector-ali
支持 authlib-injector API 地址指示
2018-10-06 12:38:04 +08:00
huanghongxun aedd4c290e Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-10-06 12:20:48 +08:00
huanghongxun 749ecfe37f Fix #467 2018-10-06 12:20:42 +08:00
yushijinhun c51da28e31 Add error message for authlib-injector download failure 2018-10-06 12:06:31 +08:00
yushijinhun f3dca75a10 Do not check for update of authlib-injector each time 2018-10-06 11:56:02 +08:00
yushijinhun c3dcb0292c Support -Dauthlibinjector.side=client argument
See yushijinhun/authlib-injector#16
2018-10-06 11:38:55 +08:00
yushijinhun 2a42ecda78 Support authlib-injector ALI
See yushijinhun/authlib-injector#18
2018-10-06 00:47:36 +08:00
huanghongxun 08be1896ee Forbid adding profiles with already existent name 2018-10-05 23:04:56 +08:00
huanghongxun 6b3152c4b5 Try to fix #462. 2018-10-05 22:27:19 +08:00
yushijinhun 19ea27dece emmm... Forgot to add DialogAware.java
Related: b9e26a4112
2018-10-04 12:48:29 +08:00
yushijinhun b9e26a4112 Add DialogAware & Improve add auth server dialog 2018-10-04 12:45:04 +08:00
yushijinhun a71aea8e43 Update translation: account.failed.server_response_malformed 2018-10-04 11:56:12 +08:00
yushijinhun 1b1f22b9fa Fix Java list display error on JDK10
如何重现:
1. 关闭当前选中版本的版本特定设置
2. 重启HMCL
3. 从主界面进入版本设置
4. 打开版本特定设置
5. 展开Java列表

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

原因未知。

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

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

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

本提交中的代码由于玄学原因,可以解决此问题。
2018-10-03 22:40:17 +08:00
yushijinhun 705a5659b6 Fix cursor doesn't change when hanging on proxy settings 2018-10-03 21:41:22 +08:00
yushijinhun 4121921aca Fix compile errors 2018-10-03 19:20:47 +08:00
yushijinhun 8cb4ca0e8b Remove Settings.font 2018-10-03 17:17:16 +08:00
yushijinhun 6205937a10 Refactor update channel selecting 2018-10-03 17:15:42 +08:00
yushijinhun 2f522b744f Add MappedProperty & reservedSelectedPropertyFor(CheckBox) 2018-10-03 17:09:12 +08:00
yushijinhun b8a84f831a Refactor proxy settings 2018-10-03 16:43:48 +08:00
yushijinhun 5f9490d780 Add SafeStringConverter & Refactor font settings 2018-10-03 16:38:21 +08:00
huanghongxun a1c5b87b55 Fix crash when no game installed 2018-10-03 15:55:38 +08:00
yushijinhun 641e308c53 Remove right graphic of Game List&Launcher Settings 2018-10-03 13:40:09 +08:00
yushijinhun 64bc70a15d Fix Account/VersionListItem can be unselected 2018-10-03 13:01:30 +08:00
yushijinhun acadbdb5c9 Fix potential NPE
Related: 3323ea2be0
2018-10-03 12:10:19 +08:00
yushijinhun 3323ea2be0 Remove JFXButton in AdvancedListItem 2018-10-03 12:02:17 +08:00
huanghongxun 45f55b2075 Adjust looking of LeftPaneController 2018-10-03 01:32:55 +08:00
yushijinhun 372005dae1 Partially revert 223c2bc7c7 2018-10-02 22:07:57 +08:00
yushijinhun 2c7ca6d354 Remove some methods in Lang 2018-10-02 22:01:42 +08:00
yushijinhun e21f315239 Fix VersionNumber.equals() and hashCode() may not be consistent 2018-10-02 21:25:29 +08:00
yushijinhun 223c2bc7c7 Change update.bubble.subtitle to 'Click here to upgrade' 2018-10-02 21:15:14 +08:00
yushijinhun 3facbba4ee Use databind for update bubble 2018-10-02 21:15:14 +08:00
yushijinhun b8bf56c8c3 Add pseudo class 'first' for 'options-list-item'
Related: caf85d2a92
2018-10-02 20:38:41 +08:00
huanghongxun 2fa08e0a70 Change default profile directory to ".minecraft" 2018-10-02 18:34:09 +08:00
huanghongxun 8e1e13058b Fix #463 2018-10-02 18:33:48 +08:00
huanghongxun 92520e46a3 Remove settings entrance in main page 2018-10-02 18:11:52 +08:00
huanghongxun 6ae6c9c971 Name changes 2018-10-02 18:11:13 +08:00
huanghongxun caf85d2a92 Fix showing a gray horizontal line in component list cell 2018-10-02 18:06:24 +08:00
huanghongxun 030b577dee Add an explicit entry to game list page 2018-10-02 12:44:36 +08:00
huanghongxun 83516ba679 Fix #461 2018-10-02 12:15:52 +08:00
huanghongxun c8f74ee8e3 Add test game in game list page 2018-10-02 11:47:53 +08:00
yushijinhun c50e41ab91 Use setItems in DatapackListPage 2018-10-01 21:01:43 +08:00
yushijinhun c03a0feb75 Refactor ProfileList 2018-10-01 20:59:11 +08:00
yushijinhun 7da5b8fbc8 Add AutomatedToggleGroup & Refactor AccountList 2018-10-01 20:39:00 +08:00
yushijinhun 83f7e61d37 Add selectedItemPropertyFor(ToggleGroup) 2018-10-01 20:03:57 +08:00
yushijinhun 86765275c8 Make SelectionModelSelectedItemProperty a util class 2018-10-01 15:38:12 +08:00
yushijinhun a01e15d841 Add MultiStepBinding&ReadWriteComposedProperty 2018-10-01 15:35:39 +08:00
yushijinhun 5e5db332d0 Remove 'Downloads' paragraph in readme, as GitHub Releases is available 2018-10-01 11:29:12 +08:00
huanghongxun 2de2f45124 Fix always showing horizontal scrollbar of game list popup in main page 2018-09-30 20:07:57 +08:00
huanghongxun b1b3eb9369 Fix always using custom common directory 2018-09-30 20:07:57 +08:00
yushijinhun 507665cd91 Fix #460 2018-09-30 19:45:39 +08:00
yushijinhun c779172cc2 Fix custom toggle is not selected when using customized java 2018-09-30 19:30:22 +08:00
yushijinhun 684813e131 Refactor Java lookup 2018-09-30 19:18:29 +08:00
huanghongxun 76ef47efc4 Suppress exceptions thrown when checking Java installation 2018-09-30 13:22:42 +08:00
huanghongxun 71a8fa0e66 Display platform of java installations 2018-09-28 19:30:41 +08:00
huanghongxun 48d98b2424 Conceal NPE 2018-09-26 19:07:09 +08:00
huanghongxun 5a0e17b121 Fix crash when user set common directory to an invalid path 2018-09-26 19:00:31 +08:00
huanghongxun 5f686f7205 Fix crash when modpack to be installed is malformed. 2018-09-26 13:13:44 +08:00
huanghongxun 518969d2f2 Fix JsonParseException 2018-09-26 13:13:21 +08:00
huanghongxun 01ff1327f7 Make head displayed more precisely 2018-09-25 12:30:21 +08:00
huanghongxun 4d366397a8 Merge pull request #457 from andylizi/add-helmet-support
支持在头像中显示头盔层
2018-09-24 17:28:17 +08:00
huanghongxun 80f79d20a5 Update message UI 2018-09-24 17:26:02 +08:00
huanghongxun 1611d11544 Fix crash when yggdrasil server gives response with malformed UUID 2018-09-24 17:24:13 +08:00
andylizi c8e8b25c85 Adjust faceOffset in skin preview 2018-09-24 15:34:48 +08:00
andylizi 92cfc3c1d3 Make skin preview more accurate 2018-09-24 13:47:59 +08:00
huanghongxun c4cebe72b4 Show bubble when updates available 2018-09-24 01:51:14 +08:00
andylizi 6d346a3ad6 Render helmet layer in skin preview. Fixed #456 2018-09-23 22:53:23 +08:00
huanghongxun 6e2696dea3 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-09-22 22:26:14 +08:00
huanghongxun ba67c968cf Change version selection list look 2018-09-22 22:25:53 +08:00
huanghongxun 332719f49a Fix closing page 2018-09-22 21:40:16 +08:00
yushijinhun fb18975c5c Fix param table in readme 2018-09-22 21:08:03 +08:00
yushijinhun 08a7c27353 Fix config file is checked before created 2018-09-22 21:05:50 +08:00
yushijinhun 4c9cb99a94 Add hmcl.self_integrity_check.disable option 2018-09-22 21:05:50 +08:00
huanghongxun cbecca5f55 Merge pull request #455 from yushijinhun/javafx
重构
2018-09-22 18:12:18 +08:00
yushijinhun 4a0831a155 Replace Properties with InvocationDispatcher & Make config saving async 2018-09-22 14:21:17 +08:00
yushijinhun fa38c53915 Remove Constants.SYSTEM_CHARSET 2018-09-22 12:54:15 +08:00
yushijinhun e7c2170ce3 Fix getSteve/AlexSkin ignores scaleRatio param 2018-09-22 12:53:02 +08:00
yushijinhun 9b69fe8c56 Remove Constants.DEFAULT_ICON 2018-09-22 12:52:30 +08:00
yushijinhun ed91771594 Remove truePredicate/falsePredicate/emptyConsumer 2018-09-22 12:39:57 +08:00
yushijinhun b33e014f71 Move GSON const to JsonUtils 2018-09-22 12:37:03 +08:00
yushijinhun 19632ac162 Move JsonUtils->gson, JarUtils->io, Properties->javafx 2018-09-22 12:33:18 +08:00
yushijinhun 74e3a9e2d1 Use http.agent to set useragent 2018-09-22 12:30:33 +08:00
yushijinhun b15291dc57 Refactor util package structure 2018-09-22 12:12:06 +08:00
huanghongxun 1b5d4c1b42 Improve UI details 2018-09-22 00:18:55 +08:00
huanghongxun 923924638e Add a close button to close all pages 2018-09-20 22:54:41 +08:00
huanghongxun 8b52c46664 Merge resources.zip to allow multiple datapack zip containing resources.zip 2018-09-20 00:12:41 +08:00
huanghongxun aa8c386cdd Support zip file in datapacks folder 2018-09-19 23:02:27 +08:00
huanghongxun bafe17107c Fix loading empty repository 2018-09-19 16:31:17 +08:00
huanghongxun 9c169fb560 Add a separator to separate launch button and popup button 2018-09-19 10:46:14 +08:00
huanghongxun 61ab8a6769 Fix NPE when first start 2018-09-19 00:20:32 +08:00
huanghongxun b1cd7057db Add a convenient entrance to launch other version 2018-09-19 00:16:21 +08:00
huanghongxun d426952d47 Fix Minecraft jar does not exist when renaming a version 2018-09-19 00:12:44 +08:00
huanghongxun 67b6a60382 Fix bugs with world. 2018-09-17 16:30:27 +08:00
huanghongxun 9d5da5a39d Try again when decorator is not initialized. 2018-09-17 16:28:52 +08:00
huanghongxun efb14572d6 Fix selected profile automatically changes to Home 2018-09-17 16:28:01 +08:00
huanghongxun bc3ae5cc7d Add link to hmcl documentation page 2018-09-16 14:12:52 +08:00
huanghongxun 9752dea0e1 Merge pull request #454 from yushijinhun/javafx
重构
2018-09-16 13:46:31 +08:00
huanghongxun 9afdc99157 Support single datapack zip 2018-09-16 13:15:54 +08:00
huanghongxun 07febc18d4 World and datapacks management 2018-09-16 12:38:15 +08:00
yushijinhun 68fdfb36fd Add selectedItemPropertyFor(ComboBox) 2018-09-16 00:52:03 +08:00
yushijinhun 09287f9c72 Rename WeakListenerHelper to WeakListenerHolder 2018-09-15 23:24:42 +08:00
yushijinhun 4a5ac1b899 Remove Settings.logLines 2018-09-15 23:18:59 +08:00
yushijinhun afad520669 Move ProxyManager.proxyProperty initialization to init() 2018-09-15 23:13:25 +08:00
yushijinhun 9950bef260 Change type of downloadType to string 2018-09-15 23:05:37 +08:00
Yuhui Huang 1753b4d27e Datapack resolving 2018-09-14 15:44:40 +08:00
Yellow Fish 2bfc85b18f Fixed NPE mentioned in #451 2018-09-13 01:13:20 +08:00
Yellow Fish 24f3dec453 Fixed #450 2018-09-12 21:37:27 +08:00
huangyuhui 4e6237c062 Fixed #448 2018-09-11 19:26:36 +08:00
huangyuhui 5cbde9cc33 Fix not on FX user thread 2018-09-09 19:38:14 +08:00
huangyuhui 25d79a94f8 Move specialize and globalize VersionSetting to HMCLGameRepository 2018-09-09 18:55:01 +08:00
huangyuhui a5997be9c4 Fix crash when selectedVersion set to empty string 2018-09-09 18:35:10 +08:00
huangyuhui c679667079 Fix crash when the game version is too long 2018-09-09 18:24:05 +08:00
huangyuhui 6b311c679d Fixed #446 2018-09-08 22:14:06 +08:00
huangyuhui 0f1f881367 Add BuildNumberOffset in gradle build config 2018-09-08 19:41:02 +08:00
huangyuhui 090665528f Improve UI 2018-09-08 19:34:51 +08:00
huangyuhui 387beb8484 Fix not saving profile configurations 2018-09-08 01:03:36 +08:00
huanghongxun daa1a38a63 Refactor ModListPage and InstallerListPage 2018-09-08 00:36:57 +08:00
huangyuhui d8a34aac1c Widen task dialog 2018-09-07 15:16:09 +08:00
huangyuhui ce9d882c9f Add total progress of task execution 2018-09-07 15:12:41 +08:00
huanghongxun 84c18c8693 Move cache repository to HMCLCore 2018-09-07 11:27:14 +08:00
huangyuhui 9360854150 Use ReadWriteLock instead of synchronized 2018-09-07 01:58:43 +08:00
huangyuhui ada63f1452 Fix not selecting the version just downloaded 2018-09-07 01:58:22 +08:00
huangyuhui c3cd0dd8a0 Refactor ProfileList & AccountList 2018-09-07 01:03:59 +08:00
huangyuhui 60143d5299 Refactor FileDownloadTask 2018-09-07 00:35:28 +08:00
huangyuhui 1019c891e2 Cache asset files 2018-09-06 17:47:44 +08:00
huangyuhui 1392ad413a Only cache verified files and copy cached files to current directory 2018-09-06 00:13:16 +08:00
huangyuhui 61aee90c8d Refactor DecoratorPage 2018-09-05 23:25:51 +08:00
huangyuhui 19725012fb Refactor AccountList and ProfileList 2018-09-05 23:20:20 +08:00
huangyuhui 94a71dc88f Fix unclosing file 2018-09-05 22:17:58 +08:00
huangyuhui d8cb93c36a Add Task.preExecute and Task.postExecute 2018-09-05 20:38:00 +08:00
huangyuhui 151097a884 Removed the text that notices user to enable disabling jvm args when using Java 9 2018-09-04 21:42:27 +08:00
huanghongxun e97bad88c6 Fix not syncing observable list 2018-09-04 14:25:18 +08:00
huangyuhui 9f494a35ec Add a button for deleting the game icon 2018-09-03 23:58:23 +08:00
huangyuhui d9d20c969a Refactor ComponentList 2018-09-03 23:42:42 +08:00
huangyuhui 7e08cfedd6 Add Constant Pool Scanner to open source code dependencies 2018-09-03 22:35:25 +08:00
huangyuhui 1145e31162 Extract IconedMenuItem 2018-09-03 22:30:22 +08:00
huangyuhui e4e521c2ef Change Controllers.navigate(null) to PageCloseEvent/onEnd 2018-09-03 21:27:39 +08:00
huangyuhui d01c9a425a Fix crash when removed all profiles 2018-09-03 18:38:40 +08:00
huangyuhui a9be348ddf Add prompt warning user that no game installed 2018-09-03 18:38:39 +08:00
huangyuhui 07079ee295 Fix image mis-positioned in AccountAdvancedListItem 2018-09-03 18:38:39 +08:00
huangyuhui eb1bab453b Cancel the option enabling game list in main page 2018-09-03 18:38:39 +08:00
huangyuhui fd3c496a3d Fix crash when first launch 2018-09-03 18:38:39 +08:00
huanghongxun 35e1e0bdd4 Change Java to 64bit automatically. 2018-09-03 17:33:45 +08:00
huangyuhui 28aa137dc1 Use iconedItem instead of ListView in popup menu 2018-09-03 10:58:45 +08:00
huangyuhui 16ee0a040a optimize import 2018-09-03 00:25:56 +08:00
huangyuhui 437391a860 Merge branch '3.2' into javafx 2018-09-03 00:11:16 +08:00
huangyuhui f61181438f allow displaying game list in main page 2018-09-03 00:10:21 +08:00
huangyuhui d74ab35279 Move package 2018-09-02 23:30:27 +08:00
huangyuhui bbc2b251dd Change the way to determine whether use global settings or the specific one 2018-09-02 23:24:32 +08:00
huangyuhui 222e39dc48 Remove decorator.fxml 2018-09-02 21:58:03 +08:00
huangyuhui f5b310d959 Move AdvancedListItem & Skin to new package 2018-09-02 21:57:25 +08:00
huangyuhui 5fb0035b2f Refactor decorator as well as navigation 2018-09-02 21:53:34 +08:00
huangyuhui b8793f7575 Prevent horizontal scroll bar in settings page 2018-09-02 21:52:45 +08:00
huangyuhui 5c5f091769 Fails game installing when unable to download game jar 2018-09-01 23:41:13 +08:00
huangyuhui cf6485aea8 Make exception message more understandable in FileDownloadTask 2018-09-01 23:38:32 +08:00
huangyuhui f2f0c96e62 Notice user to add authentication server 2018-09-01 23:38:05 +08:00
huangyuhui 64e7cc74b2 ProfileList 2018-09-01 00:36:18 +08:00
huangyuhui 531fd87ff3 Profiles 2018-09-01 00:19:25 +08:00
huangyuhui efa280b839 Move profile management to Profiles 2018-08-31 22:28:06 +08:00
huangyuhui b3d84c4f99 Refactor 2018-08-31 22:21:15 +08:00
huangyuhui 015a1e3482 Fix #441 2018-08-31 12:21:08 +08:00
huangyuhui 5efc8d6fa9 Refactor View and ViewModel to Control and Skin 2018-08-31 12:13:47 +08:00
huangyuhui bb1654828f Change "Run Directory" to "Working Directory" 2018-08-31 10:44:01 +08:00
huangyuhui 93cabde9f4 clean up HMCLCore 2018-08-30 22:20:18 +08:00
huangyuhui aeb5eb9129 clean up HMCL 2018-08-30 22:19:49 +08:00
huangyuhui e9e8b8a1ad Change contact URL 2018-08-30 22:15:12 +08:00
huangyuhui 08564e5ea2 Add account list view 2018-08-30 22:14:30 +08:00
huangyuhui 1695619ef5 Move account ui class to a new package 2018-08-30 22:13:53 +08:00
huangyuhui 8f2f57c543 cut off the version name if too long 2018-08-30 22:13:13 +08:00
huangyuhui 60454c6af3 New game list page 2018-08-30 22:12:39 +08:00
huangyuhui 40c7c72973 Move operations on version to new class 2 2018-08-30 22:11:58 +08:00
huangyuhui f9127272b1 Split download wizard into { new game, install modpack } 2018-08-30 22:11:49 +08:00
huangyuhui ec295664d9 Move operations on version to new class 2018-08-30 22:11:00 +08:00
huangyuhui ec2b9b19be Add an entrance to game list page 2018-08-30 22:09:54 +08:00
huangyuhui 1566e069ed Add selectedMinecraftVersion 2018-08-30 22:09:05 +08:00
huangyuhui 9c149f3529 Postpone loading fonts 2018-08-29 21:05:07 +08:00
huangyuhui 2f35baef39 Convert settings.fxml to Java code 2018-08-29 20:28:43 +08:00
huangyuhui 7c77c317df Improve finalizedTask log 2018-08-29 00:03:50 +08:00
huangyuhui ed3a04d531 Fix animation twinkling 2018-08-28 00:23:49 +08:00
huangyuhui c3ec7741dc Change "Game directory" to "Run directory" 2018-08-28 00:22:12 +08:00
huanghongxun f4f89fac1f Merge pull request #438 from yushijinhun/force-update
移除「更新一次 HMCL,电脑上所有 HMCL 都得到更新」的功能
2018-08-26 19:51:20 +08:00
yushijinhun 5262150d3c Prevent 2.x being updated to 3.x 2018-08-26 19:34:10 +08:00
yushijinhun b5f56eb2b5 Remove force update feature 2018-08-26 19:02:47 +08:00
huangyuhui 9a375fd0d1 Add hiding versions. Closes #437 2018-08-26 11:50:42 +08:00
huangyuhui 6ca4aeb5a2 Support 1.13 auto-installing 2018-08-26 11:48:27 +08:00
huangyuhui 8ed4a9a5f2 Fix not showing Optifine 1.13 alpha 2018-08-25 19:53:29 +08:00
huangyuhui a4578405cf Remove RandomUserAgent 2018-08-25 19:39:00 +08:00
huangyuhui 73f147b7e2 Update Chinese localization 2018-08-23 21:45:56 +08:00
huangyuhui 5c3d735315 Prevent HMCL 2.x updating 2018-08-23 21:45:47 +08:00
huanghongxun 200621996d Merge pull request #431 from pan93412/javafx
Update Chinese (Taiwan) Translation
2018-08-22 20:56:12 +08:00
byStarTW a912a0a07e Update Chinese (Taiwan) Translation 2018-08-22 20:54:49 +08:00
huangyuhui 4146ceb048 Fix not downloading the correct game version 2018-08-22 04:25:35 +08:00
huangyuhui ad568d2b9f Fix #429 2018-08-22 02:08:49 +08:00
huangyuhui c7bfb9a11d Suggest user to install latest Java 8 to avoid potentional game crashing 2018-08-21 21:12:05 +08:00
huangyuhui c9391181bd Display empty message when loading 1.13 forge version list 2018-08-21 19:31:04 +08:00
huangyuhui b08eb519bf Remove logging support which is meaningless 2018-08-21 11:48:19 +08:00
huangyuhui 47c4fc2f7e Fix #426 2018-08-20 11:53:02 +08:00
huangyuhui dfd177433a Fix not replacing tokens metioned in #426 2018-08-20 11:38:06 +08:00
huanghongxun 27381e9696 Merge pull request #425 from yushijinhun/yushijinhun/javafx
修复 UpdateChecker 潜在问题 & 弃用 ImmediateXXXProperty
2018-08-19 19:47:30 +08:00
yushijinhun 1c7827c53c Deprecate and partially remove ImmediateProperty 2018-08-19 15:55:18 +08:00
yushijinhun 9512bdb5d6 Remove UpdateChecker.updateChannel & Fix concurrent problems in update checking 2018-08-19 15:43:34 +08:00
huangyuhui 33d1f1d692 Not showing 'unknown' in mod info 2018-08-19 12:19:11 +08:00
huangyuhui c6b49545ff Fix #424 2018-08-19 12:13:39 +08:00
huangyuhui a4a833bfdb Fix auto-installing: missing checksums in game json 2018-08-19 11:58:02 +08:00
huangyuhui 2e1b6e4630 Fix not correctly loading remote game version list 2018-08-19 11:56:39 +08:00
huangyuhui 6911cfa387 try to move deleting version to trash 2018-08-19 11:34:07 +08:00
huangyuhui aa29f890de Log memory size and metaspace 2018-08-19 02:03:05 +08:00
huangyuhui 0f4e6a01bc Fix unable to install more than one libraries 2018-08-19 01:18:57 +08:00
huangyuhui 1d3d8d6710 Show OptiFine versions in dividually 2018-08-19 00:39:47 +08:00
huangyuhui 005e06c56f Always download Optifine from BMCLAPI 2018-08-19 00:35:37 +08:00
huangyuhui 0a7c32b3cb Friendly prompt warning user about independent version deletion 2018-08-18 22:40:08 +08:00
huangyuhui 82916a04ea Check if forge libraries' checksums are valid 2018-08-18 22:25:13 +08:00
huangyuhui ba2e419894 Fix #422 2018-08-18 22:09:16 +08:00
huangyuhui 0812c9025d Do not check game asset hash code for every launching 2018-08-18 02:00:00 +08:00
huangyuhui 4c59ed3ccf Fix installing MultiMC modpack 2018-08-18 01:12:22 +08:00
huangyuhui 43d8d96608 Add doublequotes when doublequotes exist in windows batch 2018-08-18 00:43:55 +08:00
huangyuhui 26a6a7d883 Add doublequotes when special characters exist in launch script. Closes #420 2018-08-18 00:32:37 +08:00
huangyuhui 2e9c237367 Use system proxy by default 2018-08-17 23:48:01 +08:00
huangyuhui 99d2816ea7 Suppress RuntimeException since -1 is possible 2018-08-17 22:11:46 +08:00
huangyuhui 7c3853b0e6 Not logging command line in ProcessStoppedEvent 2018-08-17 22:10:11 +08:00
huangyuhui 53aecf38e0 Support displaying Rift mod information 2018-08-17 22:09:37 +08:00
huangyuhui 41fab0a781 Log launching game version name 2018-08-17 21:56:52 +08:00
huangyuhui 3df35aa373 Translate ZipError into ZipException since ZipError is not IOException 2018-08-17 21:53:29 +08:00
huangyuhui 543eaa4f66 Force cancel using custom log4j config 2018-08-17 21:52:35 +08:00
huangyuhui 3a851b7f1a Fix unable to launch 1.13 with Rift 2018-08-17 21:51:25 +08:00
huangyuhui 089ae1e313 Suppress InterruptedException 2018-08-17 21:51:12 +08:00
huangyuhui bfe5802d02 Remove roboto font 2018-08-16 02:46:49 +08:00
huangyuhui 3ec6c97380 Replace 2.x with existent 3.x 2018-08-15 01:40:18 +08:00
huangyuhui e376da208d Fix duplicate JavaVersions in Windows. Closes #419 2018-08-14 10:44:24 +08:00
huangyuhui 3b02c5e4e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-08-14 00:10:48 +08:00
huangyuhui 1bcc79c415 Fix always trying to close launcher when game version cannot be obtained 2018-08-14 00:09:20 +08:00
huangyuhui 9e2cb25d01 Convert old offline account setings 2018-08-14 00:08:35 +08:00
yushijinhun 7e820ba2a5 Fix closeDialog() throws NPE 2018-08-13 22:18:18 +08:00
huanghongxun b079940cd9 Fix uniquing text2speech and its native library. Closes #418 2018-08-11 09:45:31 +08:00
huanghongxun 593ee24429 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-08-10 19:05:26 +08:00
huanghongxun f7a91bf10c Fix version setting not saved 2018-08-10 19:04:33 +08:00
huanghongxun dad7ac706f Add update channel selection 2018-08-10 12:53:49 +08:00
Haowei Wen 13c48ce78c Merge pull request #417 from tsl0922/osx-maximized
Mac 下恢复使用自定义窗口边框按钮
2018-08-10 08:26:56 +08:00
Shuanglei Tao 8ecbba1003 Remove native window border on osx 2018-08-09 23:41:56 +08:00
huanghongxun 8bf6a9ec9d Rename AdvancedListItem 2018-08-09 20:22:57 +08:00
huanghongxun 64092a4bd4 Merge branch 'yushijinhun-java' into javafx 2018-08-09 13:54:54 +08:00
yushijinhun 8e6bcd969e Refactor JavaVersion 2018-08-09 13:49:16 +08:00
yushijinhun 6777dd7ea7 Fix UnsupportedOperationException in java lookup
Mentioned in #412
2018-08-09 13:22:49 +08:00
yushijinhun f16885568e Make config files hidden 2018-08-09 12:56:03 +08:00
huanghongxun ab109bcfcc Merge pull request #414 from yushijinhun/javafx
Fixes FileSystemNotFoundException in createZipFileSystem
2018-08-09 11:41:27 +08:00
yushijinhun 17f71eaa21 Fixes FileSystemNotFoundException in createZipFileSystem 2018-08-09 11:34:38 +08:00
huanghongxun 3a294e354a ExeLauncher: Search Java in C:/Program Files 2018-08-08 21:16:57 +08:00
huanghongxun 54814181b9 Change xz compressing level to 6 since CI server does not have enough memory 2018-08-08 12:57:42 +08:00
huanghongxun 935c2bd6e5 Merge branch 'rewrite-upgrade' into javafx 2018-08-08 10:03:14 +08:00
huanghongxun a2b74b8e78 Fix getImplementationVersion returning null on Java 9. 2018-08-08 10:02:34 +08:00
huanghongxun 09730d74e0 Merge branch 'rewrite-upgrade' of https://github.com/yushijinhun/HMCL into rewrite-upgrade 2018-08-07 22:18:58 +08:00
huanghongxun 2bf03291ce Try Java8 for Minecraft 1.12.2 or older modded game 2018-08-07 22:10:34 +08:00
huanghongxun 93e5851167 Improve config compatibility with HMCL 2.x. Fix #411 2018-08-07 18:40:23 +08:00
huanghongxun 61fd581ef5 Hide the separator line between refreshNavButton and window buttons when refreshNavButton is hidden 2018-08-07 18:08:20 +08:00
huanghongxun 51afcf2dee Use ZipFileSystem instead of commons-compress 2018-08-07 11:53:05 +08:00
huanghongxun cadafe13e1 Remove IOUtils.closeQuietly 2018-08-07 11:53:05 +08:00
Haowei Wen 47031cb70e Merge pull request #413 from tsl0922/osx
修复在 OSX 下使用的一些问题
2018-08-06 18:36:53 +08:00
Shuanglei Tao b3e776b4bc Use native window border on osx (workaround for #403) 2018-08-06 14:17:41 +08:00
Shuanglei Tao e57a1a8da6 Make gradlew executable 2018-08-05 23:08:27 +08:00
yushijinhun 7f2a392a33 Use com.github.johnrengelman.shadow plugin 2018-08-05 11:06:50 +08:00
yushijinhun 055379f569 Add .pack.gz build back 2018-08-05 09:37:55 +08:00
yushijinhun 2b8843ca19 Replace .pack.gz with .pack.xz 2018-08-05 01:02:42 +08:00
yushijinhun ace830bced Refactor build.gradle 2018-08-05 00:42:05 +08:00
yushijinhun 3c2f232acb Rename PACK -> PACK_GZ 2018-08-04 23:11:31 +08:00
yushijinhun 86030e64ca Extract writeToStorage() 2018-08-04 23:10:14 +08:00
huanghongxun b1ce3c31f5 Build packed archive 2018-08-04 22:12:02 +08:00
huanghongxun 04553c2c56 Support packed update archive 2018-08-04 22:04:59 +08:00
yushijinhun bdb7afa860 Replace updateSource with updateChannel 2018-08-04 21:39:19 +08:00
yushijinhun 7096135cdf Change update source to hmcl.huangyuhui.net 2018-08-04 21:26:46 +08:00
yushijinhun be37014206 Use Paths.get(url.toURI()) in LocalVersion.current() 2018-08-04 21:17:52 +08:00
yushijinhun b78beb3305 Copy downloaded HMCL to .hmcl only after it's verified 2018-08-01 15:54:52 +08:00
yushijinhun 38f27d134d Change task.test() to executor.test() 2018-08-01 15:54:52 +08:00
yushijinhun 3b42ba9375 Add version query when checking for update 2018-08-01 15:54:52 +08:00
yushijinhun 4f52bb2ffe Add option hmcl.update_source.override 2018-07-31 19:41:50 +08:00
yushijinhun dd45e1b3db Add JAR integrity check 2018-07-31 19:39:21 +08:00
yushijinhun e38bfdfc73 Refactor build.gradle 2018-07-31 15:48:29 +08:00
yushijinhun 73caebff00 Remove unnecessary saveConfig() call 2018-07-31 14:33:03 +08:00
yushijinhun e0608cdf3e Remove unused files 2018-07-31 14:32:42 +08:00
yushijinhun c815fa80e8 Prevent /build being created 2018-07-31 14:06:54 +08:00
yushijinhun e91e1cab47 Remove coreCompile/Runtime.extendsFrom 2018-07-31 14:04:36 +08:00
yushijinhun b791878651 Remove generateSources task 2018-07-31 14:02:26 +08:00
yushijinhun 8192ffec03 Remove duplicated HMCLauncher.exe 2018-07-31 13:59:48 +08:00
yushijinhun afa6eb3bad Refactor build.gradle 2018-07-31 13:46:31 +08:00
yushijinhun 6f7d22518e Remove pack.gz build 2018-07-31 13:34:17 +08:00
yushijinhun 2663312dfb Auto-rename after upgrade 2018-07-31 13:28:39 +08:00
yushijinhun 1332517a9f Rewrite upgrade feature 2018-07-30 23:32:01 +08:00
yushijinhun 13e227851f Refactor IconedItem 2018-07-29 21:10:04 +08:00
yushijinhun e1b6c18017 Remove HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING 2018-07-29 19:07:48 +08:00
yushijinhun a88c8ed85f Extract class Metadata 2018-07-29 19:03:49 +08:00
yushijinhun 25a90fc481 Remove upgrade feature 2018-07-29 18:42:31 +08:00
huangyuhui 1371206323 Pack with exe launcher 2018-07-29 16:34:15 +08:00
huanghongxun 129f6bb5cc Merge pull request #407 from yushijinhun/config-permission
重构 Config
2018-07-29 14:44:23 +08:00
yushijinhun 545c17d052 Print current directory & hmcl directory when bootstrap 2018-07-29 14:27:50 +08:00
yushijinhun add00ddba1 Use configured locale as soon as possible 2018-07-29 14:21:43 +08:00
yushijinhun d9aeb0cc2b Use SupportedLocale in Config 2018-07-29 14:15:32 +08:00
yushijinhun 49e70e7195 Add ConfigHolder.isNewlyCreated() 2018-07-29 14:02:32 +08:00
yushijinhun f8abebf110 Remove isFirstLaunch 2018-07-29 13:45:22 +08:00
yushijinhun 8e2dfc7341 Show error dialog when hmcl.json is not accessible 2018-07-29 13:40:59 +08:00
yushijinhun e650ffffce Initialize ConfigHolder & Settings manually 2018-07-29 13:40:59 +08:00
yushijinhun d549a300ab Replace Settings.INSTANCE with Settings.instance() 2018-07-29 13:40:59 +08:00
yushijinhun 82a62e73c1 Replace CONFIG with config() 2018-07-29 13:40:57 +08:00
yushijinhun 45c7e0c231 Remove Settings.save() 2018-07-29 13:39:32 +08:00
huangyuhui b98a4b11a3 Fix #406 2018-07-29 13:31:18 +08:00
huangyuhui 484a103f27 Fix always showing the welcome page when upgrading from old versions 2018-07-29 13:25:15 +08:00
huangyuhui e31120be21 Friendly prompt that it cannot download some libraries successfully. 2018-07-22 18:50:44 +08:00
huangyuhui 247f02d423 Friendly prompt to ServerResponseMalformedException 2018-07-22 18:22:28 +08:00
huangyuhui 3da0c1d8e3 Abandon Launch4j which will not find Java inst in PATH or JAVA_HOME 2018-07-22 18:15:39 +08:00
huangyuhui c830bbc703 reorder version in main page 2018-07-21 21:58:46 +08:00
huangyuhui 7b619fef1d Fix always using BMCLAPIDownloadProvider 2018-07-21 20:24:54 +08:00
huanghongxun bac265a077 Merge pull request #399 from yushijinhun/refactor-account
重构账户部分
2018-07-21 19:50:46 +08:00
huanghongxun 39df0f8d43 Merge branch 'javafx' into refactor-account 2018-07-21 19:50:16 +08:00
huangyuhui 6af7f916ee Add an option to disable common directory globally 2018-07-21 19:45:59 +08:00
yushijinhun ae64097edf Automatically select the newly added account 2018-07-21 12:34:12 +08:00
yushijinhun cb6d8a88f1 Fix the old account isn't replaced when adding a same account 2018-07-21 12:31:08 +08:00
huangyuhui 12838994ad Reconstruct MultiFileItem 2018-07-21 11:39:41 +08:00
yushijinhun d30758a2a8 Throw NPE when selected server is null 2018-07-21 09:57:47 +08:00
yushijinhun b820aad358 Use MappedObservableList in AuthlibInjectorServersPage 2018-07-20 20:26:27 +08:00
yushijinhun 170189c344 Refactor accounts 2018-07-20 20:26:27 +08:00
yushijinhun f9e9c9d38b Use LinkedHashMap for mapOf 2018-07-20 20:26:27 +08:00
yushijinhun 088af981df Turn properties in SettingsPage from local variables into fields 2018-07-20 20:26:27 +08:00
yushijinhun 357fef8672 Call initProxy() manually 2018-07-20 20:26:27 +08:00
yushijinhun c38848ea0e Rename fields in Accounts 2018-07-20 20:26:27 +08:00
yushijinhun be8cd13b08 Throw IAE when getAccountType() receives an unexpected param 2018-07-20 20:26:27 +08:00
yushijinhun 487816ab74 Use AccountFactory in cboType 2018-07-20 20:26:27 +08:00
yushijinhun bcd9f8240c Extract account factories to static fields 2018-07-20 20:26:27 +08:00
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
andylizi 76c254ee98 Switch the download links to HTTPS 2018-02-28 17:23:49 +08:00
433 changed files with 18772 additions and 12657 deletions

No files matched your search

+14 -3
View File
@@ -4,7 +4,6 @@
hs_err_pid*
.gradle
.idea
*.lck
*.1
@@ -19,7 +18,19 @@ NVIDIA
/HMCL/build/
/HMCLCore/build/
# idea output
# idea
.idea
/out/
/HMCL/out/
/HMCLCore/out/
/HMCLCore/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
.classpath
.project
.settings
# netbeans
.nb-gradle
Binary file not shown.
Binary file not shown.
+110 -131
View File
@@ -1,163 +1,142 @@
import org.apache.tools.ant.filters.ReplaceTokens
import proguard.gradle.ProGuardTask
import java.security.MessageDigest
import java.util.jar.JarFile
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
if (!hasProperty('mainClass')) {
ext.mainClass = 'org.jackhuang.hmcl.Main'
plugins {
id 'com.github.johnrengelman.shadow' version '4.0.0'
id 'application'
}
def buildnumber = System.getenv("TRAVIS_BUILD_NUMBER")
if (buildnumber == null)
buildnumber = System.getenv("BUILD_NUMBER")
if (buildnumber == null)
buildnumber = "SNAPSHOT"
import java.nio.file.FileSystems
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
def versionroot = System.getenv("VERSION_ROOT")
if (versionroot == null)
versionroot = "3.0"
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
String mavenVersion = versionroot + '.' + buildnumber
version = mavenVersion
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
compile project(":HMCLCore")
compile rootProject.files("lib/JFoenix.jar")
}
task generateSources(type: Sync) {
from 'src/main/java'
into "$buildDir/generated-src"
filter(ReplaceTokens, tokens: [
'HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING': mavenVersion
])
def digest(String algorithm, byte[] bytes) {
return MessageDigest.getInstance(algorithm).digest(bytes)
}
compileJava.setSource "$buildDir/generated-src"
compileJava.dependsOn generateSources
def createChecksum(File file) {
def algorithm = "SHA-1"
def suffix = "sha1"
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
}
configurations {
coreCompile.extendsFrom compile
coreRuntime.extendsFrom runtime
def attachSignature(File jar) {
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
if (keyLocation == null)
return
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
def signer = Signature.getInstance("SHA512withRSA")
signer.initSign(privatekey)
new ZipFile(jar).withCloseable { zip ->
zip.stream()
.sorted(Comparator.comparing { it.name })
.filter { it.name != "META-INF/hmcl_signature" }
.forEach {
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
}
}
def signature = signer.sign()
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs ->
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature }
}
}
ext.packer = Pack200.newPacker()
packer.properties()["pack.effort"] = "9"
ext.unpacker = Pack200.newUnpacker()
def repack(File file) {
def packed = new ByteArrayOutputStream()
new JarFile(file).withCloseable { packer.pack(it, packed) }
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
}
jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
manifest {
attributes 'Created-By' : 'Copyright(c) 2013-2017 huangyuhui.',
'Main-Class' : mainClass,
'Multi-Release': "true"
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version
}
finalizedBy shadowJar
}
shadowJar {
classifier = null
exclude 'META-INF/maven/**'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
dependencies {
exclude(dependency('org.jetbrains:annotations'))
}
doLast {
new File("build/signed").mkdirs()
ant.signjar(signedjar: archivePath, jar: archivePath,
keystore: "HMCL.keystore", storepass: "123456",
alias: "HMCL")
repack(jar.archivePath)
attachSignature(jar.archivePath)
createChecksum(jar.archivePath)
}
}
def messageDigest = MessageDigest.getInstance("SHA1")
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
messageDigest.update(buf, 0, bytesRead)
def createExecutable(String suffix, String header) {
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
output.bytes = new File(project.projectDir, header).bytes
output << jar.archivePath.bytes
createChecksum(output)
}
task makePack(dependsOn: jar) {
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
doLast {
outputPath.newOutputStream().withCloseable { out ->
new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) }
}
def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0')
def fileEx = new File(project.buildDir, "libs/" + archivePath.getName() + ".sha1")
if (!fileEx.exists()) fileEx.createNewFile()
fileEx.append sha1Hex
createChecksum(outputPath)
}
}
processResources {
from(sourceSets.main.resources.srcDirs) {
exclude 'icon.icns'
}
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(6)).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packXz)
}
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: makePack) doLast {
def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz")
new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packGz)
}
task makePackGZ(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)+".pack.gz")
def os = new GZIPOutputStream(new FileOutputStream(loc))
Pack200.newPacker().pack new JarFile(makeExecutableinjar), os
os.close()
def messageDigest = MessageDigest.getInstance("SHA1")
loc.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
messageDigest.update(buf, 0, bytesRead)
}
def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0')
def fileEx = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz.sha1")
if (!fileEx.exists()) fileEx.createNewFile()
fileEx.append sha1Hex
task makeExecutables(dependsOn: jar) doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
}
task proguard(type: ProGuardTask, dependsOn: jar) {
ext {
def re = jar.classifier
injar = jar.archivePath
jar.classifier = 'optimized'
outjar = jar.archivePath
jar.classifier = re
}
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
configuration 'proguard.pro'
}
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 makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
-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>;
}
Binary file not shown.
@@ -21,20 +21,27 @@ 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.setting.ConfigHolder;
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 org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.util.Arrays;
import java.util.ResourceBundle;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
@@ -43,34 +50,46 @@ public final class Launcher extends Application {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
ConfigHolder.init();
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
// runLater to ensure ConfigHolder.init() finished initialization
Platform.runLater(() -> {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UpdateChecker.init();
primaryStage.show();
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
});
}
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
if (!FileUtils.makeDirectory(LOG_DIRECTORY))
System.out.println("Unable to create log directory " + LOG_DIRECTORY + ", log files cannot be generated.");
try {
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
Logging.start(LOG_DIRECTORY);
Logging.LOG.info("*** " + TITLE + " ***");
UPDATE_CHECKER.process(false)
.then(Task.of(Schedulers.javafx(), () -> {
if (UPDATE_CHECKER.isOutOfDate())
Controllers.showUpdate();
}))
.start();
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -79,68 +98,54 @@ public final class Launcher extends Application {
}
public static void stopApplication() {
JFXUtilities.runInFX(() -> {
stopWithoutPlatform();
Platform.exit();
});
}
public static void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit");
LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Logging.LOG.info("Shutting down executor services.");
Schedulers.shutdown();
Controllers.shutdown();
Platform.exit();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
public static void stopWithoutPlatform() {
LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
for (URL u : urls)
try {
File f = new File(u.toURI());
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
result.add(f);
} catch (URISyntaxException e) {
return null;
}
}
if (result.isEmpty())
return null;
else
return result;
}
public static String i18n(String key) {
try {
return RESOURCE_BUNDLE.getString(key);
} catch (Exception e) {
Logging.LOG.log(Level.SEVERE, "Cannot find key " + key + " in resource bundle", e);
return key;
}
}
public static 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 String i18n(String key, Object... formatArgs) {
return String.format(i18n(key), formatArgs);
}
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = getWorkingDirectory("hmcl");
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
public static final IUpgrader UPGRADER = new AppDataUpgrader();
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final String CONTACT = "http://huangyuhui.duapp.com/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
+82 -12
View File
@@ -17,24 +17,94 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.*;
import java.io.File;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
public static void main(String[] args) {
String currentDirectory = new File("").getAbsolutePath();
Logging.LOG.info("Current directory: " + currentDirectory);
if (currentDirectory.contains("!")) {
Logging.LOG.severe("Exclamation mark(!) is not allowed in the path where HMCL is in. Forcibly exit.");
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
String message = "Exclamation mark(!) is not allowed in the path where HMCL is in.\nThe path is " + currentDirectory;
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
} else
Launcher.main(args);
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
if (UpdateHandler.processArguments(args)) {
return;
}
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 {
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit(i18n("fatal.missing_javafx"));
}
}
private static void checkDSTRootCAX3() {
TrustManagerFactory tmf;
try {
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
} catch (NoSuchAlgorithmException | KeyStoreException e) {
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
// don't know what to do here
return;
}
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
return;
}
}
}
}
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
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);
}
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
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);
}
}
@@ -0,0 +1,36 @@
/*
* 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;
import org.jackhuang.hmcl.util.io.JarUtils;
/**
* Stores metadata about this application.
*/
public final class Metadata {
private Metadata() {}
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -1,61 +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.event;
import org.jackhuang.hmcl.auth.Account;
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 accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountLoadingEvent extends Event {
private final Collection<Account> accounts;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountLoadingEvent(Object source, Collection<Account> accounts) {
super(source);
this.accounts = Collections.unmodifiableCollection(accounts);
}
public Collection<Account> getAccounts() {
return accounts;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("accounts", accounts)
.toString();
}
}
@@ -1,55 +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.event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when the selected profile changed.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
* @author huangyuhui
*/
public final class ProfileChangedEvent extends Event {
private final Profile profile;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
* @param profile the new profile.
*/
public ProfileChangedEvent(Object source, Profile profile) {
super(source);
this.profile = profile;
}
public Profile getProfile() {
return profile;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("profile", profile)
.toString();
}
}
@@ -1,60 +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.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>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class ProfileLoadingEvent extends Event {
private final Collection<Profile> profiles;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
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();
}
}
@@ -17,64 +17,50 @@
*/
package org.jackhuang.hmcl.game;
import javafx.geometry.Rectangle2D;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
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;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
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(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
loadSkins(Proxy.NO_PROXY);
}
public static void loadSkins(Proxy proxy) {
for (Account account : Settings.INSTANCE.getAccounts()) {
for (Account account : Accounts.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 +68,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,38 +80,41 @@ 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);
}
public static Rectangle2D getViewport(double scaleRatio) {
double size = 8.0 * scaleRatio;
return new Rectangle2D(size, size, size, size);
public static Image getHead(Image skin, int scaleRatio) {
final int size = 8 * scaleRatio;
final int faceOffset = (int) Math.round(scaleRatio * 4d / 9d);
BufferedImage image = SwingFXUtils.fromFXImage(skin, null);
BufferedImage head = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = head.createGraphics();
g2d.drawImage(image, faceOffset, faceOffset, size - faceOffset, size - faceOffset,
size, size, size + size, size + size, null);
if (image.getHeight() > 32) {
g2d.drawImage(image, 0, 0, size, size,
40 * scaleRatio, 8 * scaleRatio, 48 * scaleRatio, 16 * scaleRatio, null);
}
return SwingFXUtils.toFXImage(head, null);
}
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, Proxy proxy, boolean refresh) {
public SkinLoadTask(YggdrasilAccount account, boolean refresh) {
this.account = account;
this.proxy = proxy;
this.refresh = refresh;
}
@@ -142,33 +133,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();
File file = getSkinFile(profile.getId());
if (!refresh && file.exists())
return;
Optional<Texture> texture = account.getSkin(profile);
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(profile.getName());
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;
File file = getSkinFile(account.getUUID());
if (!refresh && file.exists())
return;
Optional<Texture> texture = account.getSkin();
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(account.getCharacter());
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) {
@@ -180,18 +171,15 @@ public final class AccountHelper {
}
public static Image getSteveSkin(double scaleRatio) {
return scale("/assets/img/steve.png", 4);
return scale("/assets/img/steve.png", scaleRatio);
}
public static Image getAlexSkin(double scaleRatio) {
return scale("/assets/img/alex.png", 4);
return scale("/assets/img/alex.png", scaleRatio);
}
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
@@ -0,0 +1,30 @@
package org.jackhuang.hmcl.game;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.download.DefaultCacheRepository;
import java.nio.file.Paths;
public class HMCLCacheRepository extends DefaultCacheRepository {
private final StringProperty directory = new SimpleStringProperty();
public HMCLCacheRepository() {
directory.addListener((a, b, t) -> changeDirectory(Paths.get(t)));
}
public String getDirectory() {
return directory.get();
}
public StringProperty directoryProperty() {
return directory;
}
public void setDirectory(String directory) {
this.directory.set(directory);
}
public static final HMCLCacheRepository REPOSITORY = new HMCLCacheRepository();
}
@@ -1,47 +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.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.setting.Profile;
import java.net.Proxy;
/**
* @author huangyuhui
*/
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);
this.profile = profile;
}
@Override
public GameBuilder gameBuilder() {
return new HMCLGameBuilder(profile);
}
}
@@ -1,77 +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.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
/**
* @author huangyuhui
*/
public class HMCLGameDownloadTask extends Task {
private final Profile profile;
private final String gameVersion;
private final Version version;
private final List<Task> dependencies = new LinkedList<>();
public HMCLGameDownloadTask(Profile profile, String gameVersion, Version version) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
setSignificance(TaskSignificance.MINOR);
}
@Override
public List<Task> getDependencies() {
return dependencies;
}
@Override
public void execute() {
File jar = profile.getRepository().getVersionJar(version);
File cache = new File(Settings.INSTANCE.getCommonPath(), "jars/" + gameVersion + ".jar");
if (cache.exists())
try {
FileUtils.copyFile(cache, jar);
return;
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to copy cached Minecraft jar from " + cache + " to " + jar, e);
}
dependencies.add(new FileDownloadTask(
NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
cache,
profile.getDependency().getProxy(),
version.getDownloadInfo().getSha1()
).then(Task.of(v -> FileUtils.copyFile(cache, jar))));
}
}
@@ -17,12 +17,12 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import java.util.List;
import java.util.Map;
/**
* @author huangyuhui
@@ -42,10 +42,10 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
@Override
protected void appendJvmArgs(List<String> result) {
super.appendJvmArgs(result);
result.add("-Dminecraft.launcher.version=" + Launcher.VERSION);
result.add("-Dminecraft.launcher.brand=" + Launcher.NAME);
protected Map<String, String> getConfigurations() {
Map<String, String> res = super.getConfigurations();
res.put("${launcher_name}", Metadata.NAME);
res.put("${launcher_version}", Metadata.VERSION);
return res;
}
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
@@ -26,9 +27,8 @@ 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 org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -51,19 +51,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
return profile;
}
private boolean useSelf(String version, String assetId) {
VersionSetting vs = profile.getVersionSetting(version);
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists() || vs.isNoCommon();
}
@Override
public File getAssetDirectory(String version, String assetId) {
if (useSelf(version, assetId))
return super.getAssetDirectory(version, assetId);
else
return new File(Settings.INSTANCE.getCommonPath(), "assets");
}
@Override
public File getRunDirectory(String id) {
if (beingModpackVersions.contains(id) || isModpack(id))
@@ -79,17 +66,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
@Override
public File getLibraryFile(Version version, Library lib) {
VersionSetting vs = profile.getVersionSetting(version.getId());
File self = super.getLibraryFile(version, lib);
if (self.exists() || vs.isNoCommon())
return self;
else
return new File(Settings.INSTANCE.getCommonPath(), "libraries/" + lib.getPath());
}
@Override
protected void refreshVersionsImpl() {
versionSettings.clear();
@@ -169,14 +145,59 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
public File getVersionIcon(String id) {
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
}
public void saveVersionSetting(String id) {
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return new Image("/assets/img/grass.png");
Version version = getVersion(id);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else
return new Image("/assets/img/grass.png");
}
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;
}
}
/**
* Make version use self version settings instead of the global one.
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id);
if (vs == null)
vs = createVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
return vs;
}
public void globalizeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id);
if (vs != null)
vs.setUsesGlobal(true);
}
public boolean forbidsVersion(String id) {
@@ -17,12 +17,11 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ZipEngine;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.util.ArrayList;
@@ -31,67 +30,53 @@ import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
public class HMCLModpackExportTask extends Task {
private final DefaultGameRepository repository;
private final String version;
private final List<String> whitelist;
private final Modpack modpack;
private final File output;
private final String id;
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this(repository, version, whitelist, modpack, output, ID);
}
/**
* @param output mod pack file.
* @param version to locate version.json
*/
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output, String id) {
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this.repository = repository;
this.version = version;
this.whitelist = whitelist;
this.modpack = modpack;
this.output = output;
this.id = id;
onDone().register(event -> {
if (event.isFailed()) output.delete();
});
}
@Override
public String getId() {
return id;
}
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (ZipEngine zip = new ZipEngine(output)) {
zip.putDirectory(repository.getRunDirectory(version), (String pathName, Boolean isDirectory) -> {
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
if (path.isEmpty())
return true;
for (String s : blackList)
if (isDirectory) {
if (pathName.startsWith(s + "/"))
return null;
} else if (pathName.equals(s))
return null;
if (path.equals(s))
return false;
for (String s : whitelist)
if (pathName.equals(s + (isDirectory ? "/" : "")))
return "minecraft/" + pathName;
return null;
if (path.equals(s))
return true;
return false;
});
Version mv = repository.getResolvedVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
zip.putTextFile(Constants.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
}
public static final String ID = "zip_engine";
}
@@ -27,15 +27,14 @@ import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public final class HMCLModpackInstallTask extends Task {
private final File zipFile;
@@ -66,7 +65,7 @@ public final class HMCLModpackInstallTask extends Task {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = Constants.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
@@ -74,7 +73,7 @@ public final class HMCLModpackInstallTask extends Task {
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), config));
dependents.add(new ModpackInstallTask<>(zipFile, run, "/minecraft", it -> !"pack.json".equals(it), config));
}
@Override
@@ -90,9 +89,9 @@ public final class HMCLModpackInstallTask extends Task {
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = Constants.GSON.fromJson(json, Version.class).setJar(null);
Version version = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
dependencies.add(new VersionJsonSaveTask(repository, version));
dependencies.add(new MinecraftInstanceTask<>(zipFile, "minecraft/", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
dependencies.add(new MinecraftInstanceTask<>(zipFile, "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
public static final String MODPACK_TYPE = "HMCL";
@@ -19,10 +19,10 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
@@ -36,10 +36,10 @@ public final class HMCLModpackManager {
public static final List<String> MODPACK_BLACK_LIST = Lang.immutableListOf(
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"pack.json", "launcher.jar", "hmclmc.log", // HMCL
"pack.json", "launcher.jar", "hmclmc.log", "cache", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods
"logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
);
@@ -17,68 +17,111 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
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.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.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 org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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(){}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(() -> {});
private final Profile profile;
private final Account account;
private final String selectedVersion;
private File scriptFile;
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
public void launch(Profile profile, Account account, String selectedVersion, File scriptFile) {
if (account == null)
throw new IllegalArgumentException("No account");
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
this.account = Objects.requireNonNull(account);
this.selectedVersion = Objects.requireNonNull(selectedVersion);
this.setting = profile.getVersionSetting(selectedVersion);
this.launcherVisibility = setting.getLauncherVisibility();
this.showLogs = setting.isShowLogs();
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
showLogs = true;
}
public void launch() {
Logging.LOG.info("Launching game version: " + selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile)));
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(this::launch0);
});
} catch (InterruptedException ignore) {
}
});
}
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
GameRepository repository = profile.getRepository();
public void makeLaunchScript(File scriptFile) {
this.scriptFile = Objects.requireNonNull(scriptFile);
launch();
}
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = repository.getResolvedVersion(selectedVersion);
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> Controllers.dialog(launchingStepsPane))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES)))
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
if (setting.isNotCheckGame())
return null;
@@ -86,54 +129,67 @@ public final class LauncherHelper {
return dependencyManager.checkGameCompletionAsync(version);
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(new CurseCompletionTask(dependencyManager, selectedVersion))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(Launcher.i18n("account.methods"), variables -> {
.then(var -> {
try {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
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()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
DefaultLauncher launcher = variables.get("launcher");
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(Launcher.i18n("version.launch"));
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
} else {
return new LaunchTask<>(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(Launcher.i18n("version.launch_script"));
}).setName(i18n("version.launch_script"));
}
})
.then(Task.of(variables -> {
if (scriptFile == null) {
ManagedProcess process = variables.get(LaunchTask.LAUNCH_ID);
PROCESSES.add(process);
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(() -> {
launchingStepsPane.setCancel(it -> {
process.stop();
Controllers.closeDialog();
it.fireEvent(new DialogCloseEvent());
});
} else
Platform.runLater(() ->
Controllers.dialog(Launcher.i18n("version.launch_script.success", scriptFile.getAbsolutePath())));
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
.executor();
@@ -151,14 +207,36 @@ 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 ? Launcher.i18n("launch.failed") : Launcher.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) {
if (ex.getCause() instanceof FileNotFoundException)
message = i18n("modpack.type.curse.not_found");
else
message = i18n("modpack.type.curse.error");
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause());
} else {
message = StringUtils.getStackTrace(ex);
}
Controllers.dialog(message,
scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
}
}
});
}
launchingStepsPane.setExecutor(null);
@@ -169,64 +247,147 @@ 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;
boolean java8required = false;
boolean newJavaRequired = false;
// Without onAccept called, the launching operation will be terminated.
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(Launcher.i18n("launch.wrong_javadir"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
flag = true;
}
if (java.getParsedVersion() < JavaVersion.JAVA_8) {
Controllers.dialog(Launcher.i18n("launch.advice.newer_java"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
newJavaRequired = true;
setting.setJavaVersion(java8.get());
} else {
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.warning"), 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(Launcher.i18n("launch.advice.java9"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
suggest = false;
flag = true;
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
&& version.getMainClass().contains("launchwrapper")
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
if (java8.isPresent()) {
java8required = true;
setting.setJavaVersion(java8.get());
} else {
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 &&
org.jackhuang.hmcl.util.Platform.IS_64_BIT) {
Controllers.dialog(Launcher.i18n("launch.advice.different_platform"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51");
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
newJavaRequired = true;
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
flag = true;
}
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
final JavaVersion java32 = java;
// First find if same java version but whose platform is 64-bit installed.
Optional<JavaVersion> java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (!java64.isPresent()) {
final boolean java8requiredFinal = java8required, newJavaRequiredFinal = newJavaRequired;
// Then find if other java version which satisfies requirements installed.
java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> {
if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8;
if (newJavaRequiredFinal) return javaVersion.getParsedVersion() >= JavaVersion.JAVA_8;
return true;
})
.max(Comparator.comparing(JavaVersion::getVersionNumber));
}
if (java64.isPresent()) {
setting.setJavaVersion(java64.get());
} else {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
}
// 32-bit JVM cannot make use of too much memory.
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
Controllers.dialog(Launcher.i18n("launch.advice.too_large_memory_for_32bit"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(Launcher.i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (flag) {
if (suggest && Controllers.getDialogContent() instanceof MessageDialogPane)
((MessageDialogPane) Controllers.getDialogContent()).disableClosingDialog();
} else
// Cannot allocate too much memory exceeding free space.
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
// Forge 2760 and above will crash game with LiteLoader.
if (!flag) {
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it -> VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2760") >= 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
}
if (!flag)
onAccept.run();
}
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
}
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
Controllers.dialog(launchingStepsPane);
}
private void checkExit(LauncherVisibility v) {
switch (v) {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.getStage()::show);
break;
@@ -255,15 +416,7 @@ public final class LauncherHelper {
@Override
public void execute() throws Exception {
try {
setResult(supplier.get());
} catch (PermissionException e) {
throw new I18nException(Launcher.i18n("launch.failed.executable_permission"), e);
} catch (ProcessCreationException e) {
throw new I18nException(Launcher.i18n("launch.failed.creating_process") + e.getLocalizedMessage(), e);
} catch (NotDecompressingNativesException e) {
throw new I18nException(Launcher.i18n("launch.failed.decompressing_natives") + e.getLocalizedMessage(), e);
}
setResult(supplier.get());
}
@Override
@@ -283,7 +436,6 @@ public final class LauncherHelper {
private final VersionSetting setting;
private final Map<String, String> forbiddenTokens;
private final LauncherVisibility visibility;
private ManagedProcess process;
private boolean lwjgl;
private LogWindow logWindow;
@@ -298,13 +450,12 @@ 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();
logs = new LinkedList<>();
}
@@ -312,7 +463,7 @@ public final class LauncherHelper {
public void setProcess(ManagedProcess process) {
this.process = process;
if (setting.isShowLogs())
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
@@ -320,6 +471,39 @@ public final class LauncherHelper {
});
}
private void finishLaunch() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
}
});
break;
case CLOSE:
// Never come to here.
break;
case KEEP:
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
break;
case HIDE:
Platform.runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
}
});
break;
}
}
@Override
public synchronized void onLog(String log, Log4jLevel level) {
String newLog = log;
@@ -331,41 +515,25 @@ public final class LauncherHelper {
else
System.out.print(log);
logs.add(new Pair<>(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
logs.add(pair(log, level));
if (logs.size() > config().getLogLines())
logs.removeFirst();
if (setting.isShowLogs()) {
Lang.invoke(() -> {
if (showLogs) {
try {
latch.await();
logWindow.waitForLoaded();
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
Platform.runLater(() -> logWindow.logLine(log, level));
}
if (!lwjgl && log.contains("LWJGL Version: ") || !detectWindow) {
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
lwjgl = true;
switch (visibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
});
break;
case CLOSE:
// Never come to here.
break;
case KEEP:
// No operations here
break;
case HIDE:
Platform.runLater(() -> {
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
});
break;
}
finishLaunch();
}
}
@@ -374,29 +542,38 @@ public final class LauncherHelper {
if (exitType == ExitType.INTERRUPTED)
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (exitType != ExitType.NORMAL && logWindow == null)
Platform.runLater(() -> {
logWindow = new LogWindow();
switch (exitType) {
case JVM_ERROR:
logWindow.setTitle(Launcher.i18n("launch.failed.cannot_create_jvm"));
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
logWindow.setTitle(Launcher.i18n("launch.failed.exited_abnormally"));
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
break;
}
logWindow.show();
logWindow.onDone.register(() -> {
logWindow.logLine("Commands: " + StringUtils.makeCommand(process.getCommands()), Log4jLevel.INFO);
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
});
});
checkExit(visibility);
checkExit();
}
}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
}
}
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Launcher;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public enum LoadingState {
DEPENDENCIES("launch.state.dependencies"),
@@ -33,6 +33,6 @@ public enum LoadingState {
}
public String getLocalizedMessage() {
return Launcher.i18n(key);
return i18n(key);
}
}
@@ -25,9 +25,9 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.FinalizedCallback;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
@@ -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 JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
@@ -86,11 +86,14 @@ public final class ModpackHelper {
profile.getRepository().markVersionAsModpack(name);
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
profile.getRepository().refreshVersions();
VersionSetting vs = profile.specializeVersionSetting(name);
profile.getRepository().undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
if (isDependentsSucceeded) {
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
}
};
if (modpack.getManifest() instanceof CurseManifest)
@@ -102,7 +105,7 @@ public final class ModpackHelper {
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.finalized(finalizeTask)
.with(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
@@ -44,7 +44,7 @@ public final class MultiMCInstallVersionSettingTask extends Task {
@Override
public void execute() {
VersionSetting vs = Objects.requireNonNull(profile.specializeVersionSetting(version));
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
}
}
@@ -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
@@ -17,28 +17,40 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorBuildInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerResponse;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.task.Schedulers;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
@@ -46,60 +58,215 @@ import java.util.Map;
public final class Accounts {
private Accounts() {}
public static final String OFFLINE_ACCOUNT_KEY = "offline";
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), DownloadProviders::getDownloadProvider)::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
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))
);
private static final String TYPE_OFFLINE = "offline";
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
private static final Map<String, String> AUTHLIB_INJECTOR_SERVER_NAMES = new HashMap<>();
private static Map<String, AccountFactory<?>> type2factory = mapOf(
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
else return YGGDRASIL_ACCOUNT_KEY;
private static String accountType(Account account) {
if (account instanceof OfflineAccount)
return TYPE_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return TYPE_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return TYPE_YGGDRASIL_ACCOUNT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
static String getAccountId(Account account) {
return getAccountId(account.getUsername(), account.getCharacter());
public static AccountFactory<?> getAccountFactory(Account account) {
return type2factory.get(accountType(account));
}
static String getAccountId(String username, String character) {
return username + ":" + character;
static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
private static String downloadAuthlibInjector() throws Exception {
AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo();
File jar = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.jar");
File local = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.txt");
int buildNumber = 0;
try {
buildNumber = Integer.parseInt(FileUtils.readText(local));
} catch (IOException | NumberFormatException ignore) {
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
if (buildNumber < buildInfo.getBuildNumber()) {
new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run();
FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber()));
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
config().setSelectedAccount(selected == null ? "" : accountId(selected));
}
return jar.getAbsolutePath();
};
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
public static String getAuthlibInjectorServerName(String serverIp) throws Exception {
if (AUTHLIB_INJECTOR_SERVER_NAMES.containsKey(serverIp))
return AUTHLIB_INJECTOR_SERVER_NAMES.get(serverIp);
else {
AuthlibInjectorServerResponse response = Constants.GSON.fromJson(NetworkUtils.doGet(NetworkUtils.toURL(serverIp)), AuthlibInjectorServerResponse.class);
AUTHLIB_INJECTOR_SERVER_NAMES.put(serverIp, response.getMeta().getServerName());
return response.getMeta().getServerName();
}
static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
}
public static TaskResult<String> getAuthlibInjectorServerNameAsync(AuthlibInjectorAccount account) {
return Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(account.getServerBaseURL()));
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
}
/**
* Called when it's ready to load accounts from {@link ConfigHolder#config()}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
});
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
Account selected = accounts.stream()
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null);
selectedAccount.set(selected);
Schedulers.io().schedule(() -> {
if (selected != null)
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
}
});
}
public static ObservableList<Account> getAccounts() {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
public static void setSelectedAccount(Account selectedAccount) {
Accounts.selectedAccount.set(selectedAccount);
}
public static ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
// ==== authlib-injector ====
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);
}
config().getAuthlibInjectorServers().add(server);
return server;
});
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
*/
private static void removeDanglingAuthlibInjectorAccounts() {
accounts.stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.map(AuthlibInjectorAccount.class::cast)
.filter(it -> !config().getAuthlibInjectorServers().contains(it.getServer()))
.collect(toList())
.forEach(accounts::remove);
}
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> loginType2name = mapOf(
pair(Accounts.FACTORY_OFFLINE, i18n("account.methods.offline")),
pair(Accounts.FACTORY_YGGDRASIL, i18n("account.methods.yggdrasil")),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, i18n("account.methods.authlib_injector")));
public static String getAccountTypeName(AccountFactory<?> factory) {
return Optional.ofNullable(loginType2name.get(factory))
.orElseThrow(() -> new IllegalArgumentException("No corresponding login type name"));
}
public static String getAccountTypeName(Account account) {
return getAccountTypeName(getAccountFactory(account));
}
// ====
}
@@ -17,274 +17,453 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
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.util.JavaVersion;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.util.*;
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;
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(SupportedLocale.class, new SupportedLocale.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("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@SerializedName("commonpath")
private String commonDirectory = Launcher.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.HTTP);
@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 ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("downloadtype")
private int downloadType = 0;
@SerializedName("downloadType")
private StringProperty downloadType = new SimpleStringProperty("bmclapi");
@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>> accountStorages = 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("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@SerializedName("uiVersion")
private IntegerProperty uiVersion = new SimpleIntegerProperty(0);
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 EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}
public ObjectProperty<EnumCommonDirectory> commonDirTypeProperty() {
return commonDirType;
}
public void setCommonDirType(EnumCommonDirectory commonDirType) {
this.commonDirType.set(commonDirType);
}
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 SupportedLocale getLocalization() {
return localization.get();
}
public List<JavaVersion> getJava() {
return java;
public void setLocalization(SupportedLocale localization) {
this.localization.set(localization);
}
public void setJava(List<JavaVersion> java) {
this.java = java;
Settings.INSTANCE.save();
}
public String getLocalization() {
public ObjectProperty<SupportedLocale> localizationProperty() {
return localization;
}
public void setLocalization(String localization) {
this.localization = localization;
Settings.INSTANCE.save();
public String getDownloadType() {
return downloadType.get();
}
public int getDownloadType() {
public void setDownloadType(String downloadType) {
this.downloadType.set(downloadType);
}
public StringProperty downloadTypeProperty() {
return downloadType;
}
public void setDownloadType(int downloadType) {
this.downloadType = downloadType;
Settings.INSTANCE.save();
}
public Map<String, Profile> getConfigurations() {
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() {
return accounts;
}
public void setAccounts(List<Map<Object, Object>> accounts) {
this.accounts = accounts;
Settings.INSTANCE.save();
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
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 ObservableList<AuthlibInjectorServer> getAuthlibInjectorServers() {
return authlibInjectorServers;
}
public UpdateChannel getUpdateChannel() {
return updateChannel.get();
}
public ObjectProperty<UpdateChannel> updateChannelProperty() {
return updateChannel;
}
public void setUpdateChannel(UpdateChannel updateChannel) {
this.updateChannel.set(updateChannel);
}
public int getConfigVersion() {
return configVersion.get();
}
public IntegerProperty configVersionProperty() {
return configVersion;
}
public void setConfigVersion(int configVersion) {
this.configVersion.set(configVersion);
}
public int getUiVersion() {
return uiVersion.get();
}
public IntegerProperty uiVersionProperty() {
return uiVersion;
}
public void setUiVersion(int uiVersion) {
this.uiVersion.set(uiVersion);
}
}
@@ -0,0 +1,150 @@
/*
* 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 com.google.gson.Gson;
import com.google.gson.JsonParseException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.logging.Level;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class ConfigHolder {
private ConfigHolder() {}
public static final String CONFIG_FILENAME = "hmcl.json";
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
private static Path configLocation;
private static Config configInstance;
private static boolean newlyCreated;
public static Config config() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return configInstance;
}
public static boolean isNewlyCreated() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return newlyCreated;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configLocation = locateConfig();
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
Settings.init();
if (newlyCreated) {
saveConfigSync();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
}
}
}
if (!Files.isWritable(configLocation)) {
// the config cannot be saved
// throw up the error now to prevent further data loss
throw new IOException("Config at " + configLocation + " is not writable");
}
}
private static Path locateConfig() {
Path config = Paths.get(CONFIG_FILENAME);
if (Files.isRegularFile(config))
return config;
Path dotConfig = Paths.get(CONFIG_FILENAME_LINUX);
if (Files.isRegularFile(dotConfig))
return dotConfig;
// create new
return OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? config : dotConfig;
}
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
String content = new String(Files.readAllBytes(configLocation), UTF_8);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
ConfigUpgrader.upgradeConfig(deserialized, raw);
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
}
}
LOG.info("Creating an empty config");
newlyCreated = true;
return new Config();
}
private static InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
}
});
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (configLocation) {
Files.write(configLocation, content.getBytes(UTF_8));
}
}
static void markConfigDirty() {
configWriter.accept(configInstance.toJson());
}
private static void saveConfigSync() throws IOException {
writeToConfig(configInstance.toJson());
}
}
@@ -0,0 +1,116 @@
/*
* 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 org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
final class ConfigUpgrader {
private static final int VERSION = 0;
private ConfigUpgrader() {
}
/**
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
* @return true if config version is upgraded
*/
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
boolean upgraded;
if (deserialized.getConfigVersion() < VERSION) {
deserialized.setConfigVersion(VERSION);
// TODO: Add upgrade code here.
upgraded = true;
} else {
upgraded = false;
}
upgradeV2(deserialized, rawJson);
upgradeV3(deserialized, rawJson);
return upgraded;
}
/**
* Upgrade configuration of HMCL 2.x
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
List<OfflineAccount> accounts = new LinkedList<>();
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
accounts.add(account);
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
});
});
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
if (!rawJson.containsKey("selectedAccount"))
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
deserialized.setSelectedAccount(Accounts.accountId(account)));
});
});
});
}
/**
* Upgrade configuration of HMCL earlier than 3.1.70
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
if (!rawJson.containsKey("downloadType")) {
tryCast(rawJson.get("downloadtype"), Number.class)
.map(Number::intValue)
.ifPresent(id -> {
if (id == 0) {
deserialized.setDownloadType("mojang");
} else if (id == 1) {
deserialized.setDownloadType("bmclapi");
}
});
}
}
}
@@ -17,20 +17,44 @@
*/
package org.jackhuang.hmcl.setting;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import java.util.Map;
import java.util.Optional;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.CurseCDNDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import org.jackhuang.hmcl.util.Lang;
import java.util.List;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
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 Map<String, DownloadProvider> providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider()));
public static DownloadProvider getDownloadProvider(int index) {
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(DOWNLOAD_PROVIDERS.get(0));
public static final String DEFAULT_PROVIDER_ID = "bmclapi";
private static ObjectBinding<DownloadProvider> downloadProviderProperty;
static void init() {
downloadProviderProperty = Bindings.createObjectBinding(
() -> Optional.ofNullable(providersById.get(config().getDownloadType()))
.orElse(providersById.get(DEFAULT_PROVIDER_ID)),
config().downloadTypeProperty());
}
public static DownloadProvider getDownloadProvider() {
return downloadProviderProperty.get();
}
public static ObservableObjectValue<DownloadProvider> downloadProviderProperty() {
return downloadProviderProperty;
}
}
@@ -19,12 +19,5 @@ package org.jackhuang.hmcl.setting;
public enum EnumBackgroundImage {
DEFAULT,
CUSTOM;
public static EnumBackgroundImage indexOf(int index) {
if (index >= values().length || index < 0)
return DEFAULT;
else
return values()[index];
}
CUSTOM
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
* 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
@@ -15,10 +15,15 @@
* 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;
package org.jackhuang.hmcl.setting;
/**
* throws if wrong password.
*/
public class InvalidPasswordException extends AuthenticationException {
public enum EnumCommonDirectory {
/**
* %appdata%/.minecraft or ~/.minecraft
*/
DEFAULT,
/**
* user customized common directory.
*/
CUSTOM
}
@@ -18,73 +18,104 @@
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.game.HMCLDependencyManager;
import javafx.beans.Observable;
import javafx.beans.property.*;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
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 org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateStringProperty;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.io.File;
import java.lang.reflect.Type;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
/**
*
* @author huangyuhui
*/
public final class Profile {
public final class Profile implements Observable {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final HMCLGameRepository repository;
private final ModManager modManager;
private final ImmediateObjectProperty<File> gameDirProperty;
private final StringProperty selectedVersion = new SimpleStringProperty();
public ImmediateObjectProperty<File> gameDirProperty() {
return gameDirProperty;
public StringProperty selectedVersionProperty() {
return selectedVersion;
}
public String getSelectedVersion() {
return selectedVersion.get();
}
public void setSelectedVersion(String selectedVersion) {
this.selectedVersion.set(selectedVersion);
}
private final ObjectProperty<File> gameDir;
public ObjectProperty<File> gameDirProperty() {
return gameDir;
}
public File getGameDir() {
return gameDirProperty.get();
return gameDir.get();
}
public void setGameDir(File gameDir) {
gameDirProperty.set(gameDir);
this.gameDir.set(gameDir);
}
private final ImmediateObjectProperty<VersionSetting> globalProperty = new ImmediateObjectProperty<>(this, "global", new VersionSetting());
private final ReadOnlyObjectWrapper<VersionSetting> global = new ReadOnlyObjectWrapper<>(this, "global");
public ImmediateObjectProperty<VersionSetting> globalProperty() {
return globalProperty;
public ReadOnlyObjectProperty<VersionSetting> globalProperty() {
return global.getReadOnlyProperty();
}
public VersionSetting getGlobal() {
return globalProperty.get();
return global.get();
}
public void setGlobal(VersionSetting global) {
if (global == null)
global = new VersionSetting();
globalProperty.set(global);
}
private final ImmediateStringProperty nameProperty;
private final ImmediateStringProperty name;
public ImmediateStringProperty nameProperty() {
return nameProperty;
return name;
}
public String getName() {
return nameProperty.get();
return name.get();
}
public void setName(String name) {
nameProperty.set(name);
this.name.set(name);
}
public Profile() {
this("Default");
private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
public BooleanProperty useRelativePathProperty() {
return useRelativePath;
}
public boolean isUseRelativePath() {
return useRelativePath.get();
}
public void setUseRelativePath(boolean useRelativePath) {
this.useRelativePath.set(useRelativePath);
}
public Profile(String name) {
@@ -92,12 +123,41 @@ public final class Profile {
}
public Profile(String name, File initialGameDir) {
nameProperty = new ImmediateStringProperty(this, "name", name);
gameDirProperty = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
this(name, initialGameDir, new VersionSetting());
}
public Profile(String name, File initialGameDir, VersionSetting global) {
this(name, initialGameDir, global, null, false);
}
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
this.name = new ImmediateStringProperty(this, "name", name);
gameDir = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
modManager = new ModManager(repository);
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
this.useRelativePath.set(useRelativePath);
gameDirProperty.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
this.selectedVersion.addListener(o -> checkSelectedVersion());
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
private void checkSelectedVersion() {
JFXUtilities.runInFX(() -> {
if (!repository.isLoaded()) return;
String newValue = selectedVersion.get();
if (!repository.hasVersion(newValue)) {
Optional<String> version = repository.getVersions().stream().findFirst().map(Version::getId);
if (version.isPresent())
selectedVersion.setValue(version.get());
else if (newValue != null)
selectedVersion.setValue(null);
}
});
}
public HMCLGameRepository getRepository() {
@@ -108,57 +168,52 @@ public final class Profile {
return modManager;
}
public HMCLDependencyManager getDependency() {
return new HMCLDependencyManager(this, Settings.INSTANCE.getDownloadProvider(), Settings.INSTANCE.getProxy());
public DefaultDependencyManager getDependency() {
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
}
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;
}
public boolean isVersionGlobal(String id) {
VersionSetting vs = repository.getVersionSetting(id);
return vs == null || vs.isUsesGlobal();
}
/**
* Make version use self version settings instead of the global one.
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id);
if (vs == null)
vs = repository.createVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
return vs;
}
public void globalizeVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id);
if (vs != null)
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);
private void addPropertyChangedListener(InvalidationListener listener) {
name.addListener(listener);
global.addListener(listener);
gameDir.addListener(listener);
useRelativePath.addListener(listener);
global.get().addPropertyChangedListener(listener);
selectedVersion.addListener(listener);
}
private ObservableHelper observableHelper = new ObservableHelper(this);
@Override
public void addListener(InvalidationListener listener) {
observableHelper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
observableHelper.removeListener(listener);
}
private void invalidate() {
Platform.runLater(observableHelper::invalidate);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -175,6 +230,8 @@ 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());
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
return jsonObject;
}
@@ -185,9 +242,11 @@ public final class Profile {
JsonObject obj = (JsonObject) json;
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
Profile profile = new Profile("Default", new File(gameDir));
profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class));
return profile;
return new Profile("Default",
new File(gameDir),
context.deserialize(obj.get("global"), VersionSetting.class),
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
}
}
@@ -17,20 +17,199 @@
*/
package org.jackhuang.hmcl.setting;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Profiles {
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
private Profiles() {
}
public static String getProfileDisplayName(Profile profile) {
switch (profile.getName()) {
case Settings.DEFAULT_PROFILE:
return Launcher.i18n("profile.default");
case Settings.HOME_PROFILE:
return Launcher.i18n("profile.home");
case Profiles.DEFAULT_PROFILE:
return i18n("profile.default");
case Profiles.HOME_PROFILE:
return i18n("profile.home");
default:
return profile.getName();
}
}
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
profiles.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
if (!initialized)
return;
Profile profile = get();
if (profiles.isEmpty()) {
if (profile != null) {
set(null);
return;
}
} else {
if (!profiles.contains(profile)) {
set(profiles.get(0));
return;
}
}
config().setSelectedProfile(profile == null ? "" : profile.getName());
if (profile != null) {
if (profile.getRepository().isLoaded())
selectedVersion.bind(profile.selectedVersionProperty());
else {
selectedVersion.unbind();
selectedVersion.set(null);
// bind when repository was reloaded.
profile.getRepository().refreshVersionsAsync().start();
}
} else {
selectedVersion.unbind();
selectedVersion.set(null);
}
}
};
private static void checkProfiles() {
if (profiles.isEmpty()) {
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY);
Platform.runLater(() -> profiles.addAll(current, home));
}
}
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
profiles.addListener(onInvalidating(Profiles::updateProfileStorages));
profiles.addListener(onInvalidating(Profiles::checkProfiles));
selectedProfile.addListener((a, b, newValue) -> {
if (newValue != null)
newValue.getRepository().refreshVersionsAsync().start();
});
}
private static void updateProfileStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getConfigurations().clear();
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
}
/**
* Called when it's ready to load profiles from {@link ConfigHolder#config()}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
HashSet<String> names = new HashSet<>();
config().getConfigurations().forEach((name, profile) -> {
if (!names.add(name)) return;
profiles.add(profile);
profile.setName(name);
});
checkProfiles();
// Platform.runLater is necessary or profiles will be empty
// since checkProfiles adds 2 base profile later.
Platform.runLater(() -> {
initialized = true;
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
selectedVersion.bind(profile.selectedVersionProperty());
for (Consumer<Profile> listener : versionsListeners)
listener.accept(profile);
}
});
});
}
public static ObservableList<Profile> getProfiles() {
return profiles;
}
public static ReadOnlyListProperty<Profile> profilesProperty() {
return profilesWrapper.getReadOnlyProperty();
}
public static Profile getSelectedProfile() {
return selectedProfile.get();
}
public static void setSelectedProfile(Profile profile) {
selectedProfile.set(profile);
}
public static ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
private static final ReadOnlyStringWrapper selectedVersion = new ReadOnlyStringWrapper();
public static ReadOnlyStringProperty selectedVersionProperty() {
return selectedVersion.getReadOnlyProperty();
}
// Guaranteed that the repository is loaded.
public static String getSelectedVersion() {
return selectedVersion.get();
}
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
listener.accept(profile);
versionsListeners.add(listener);
}
}
@@ -1,36 +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.setting;
import org.jackhuang.hmcl.util.Lang;
import java.net.Proxy;
import java.util.List;
/**
* @author huangyuhui
*/
public final class Proxies {
private Proxies() {}
public static final List<Proxy.Type> PROXIES = Lang.immutableListOf(Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS);
public static Proxy.Type getProxyType(int index) {
return Lang.get(PROXIES, index).orElse(null);
}
}
@@ -0,0 +1,112 @@
/*
* 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 javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class ProxyManager {
private ProxyManager() {
}
private static ObjectBinding<Proxy> proxyProperty;
public static Proxy getProxy() {
return proxyProperty.get();
}
public static ObservableObjectValue<Proxy> proxyProperty() {
return proxyProperty;
}
static void init() {
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());
proxyProperty.addListener(any -> 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,542 +17,64 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.text.Font;
import javafx.beans.binding.Bindings;
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.download.DownloadProvider;
import org.jackhuang.hmcl.event.AccountLoadingEvent;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
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.logging.Level;
import java.util.stream.Collectors;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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";
private static Settings instance;
public static final File SETTINGS_FILE = new File("hmcl.json").getAbsoluteFile();
public static final Settings INSTANCE = new Settings();
private Settings() {}
private final Config SETTINGS = initSettings();
private final Map<String, Account> accounts = new HashMap<>();
{
loadProxy();
for (Iterator<Map<Object, Object>> iterator = SETTINGS.getAccounts().iterator(); iterator.hasNext(); ) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class).orElse(""));
if (factory == null) {
// unrecognized account type, so remove it.
iterator.remove();
continue;
}
Account account;
try {
account = factory.fromStorage(settings, getProxy());
} catch (Exception e) {
// storage is malformed, delete.
iterator.remove();
continue;
}
accounts.put(Accounts.getAccountId(account), account);
public static Settings instance() {
if (instance == null) {
throw new IllegalStateException("Settings hasn't been initialized");
}
checkAuthlibInjectorServerURLs();
checkAuthlibInjectorAccounts();
checkProfileMap();
save();
for (Map.Entry<String, Profile> entry2 : getProfileMap().entrySet()) {
entry2.getValue().setName(entry2.getKey());
entry2.getValue().nameProperty().setChangedListener(this::profileNameChanged);
entry2.getValue().addPropertyChangedListener(e -> save());
}
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::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;
}
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);
}
}
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());
public Locales.SupportedLocale getLocale() {
return locale;
}
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());
}
});
}
}
}
public Font getFont() {
return Font.font(SETTINGS.getFontFamily(), SETTINGS.getFontSize());
}
public void setFont(Font font) {
SETTINGS.setFontFamily(font.getFamily());
SETTINGS.setFontSize(font.getSize());
}
public int getLogLines() {
return Math.max(SETTINGS.getLogLines(), 100);
}
public void setLogLines(int logLines) {
SETTINGS.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);
}
}
}
/****************************************
* DOWNLOAD PROVIDERS *
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(SETTINGS.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);
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.getSelectedAccount())) {
@Override
public Account get() {
Account a = super.get();
if (a == null || !accounts.containsKey(Accounts.getAccountId(a))) {
Account acc = accounts.values().stream().findAny().orElse(null);
set(acc);
return acc;
} else return a;
}
@Override
public void set(Account newValue) {
if (newValue == null || accounts.containsKey(Accounts.getAccountId(newValue))) {
super.set(newValue);
}
}
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
}
};
public Account getSelectedAccount() {
return selectedAccount.get();
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public void setSelectedAccount(Account selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
onAccountLoading();
}
public Account getAccount(String name, String character) {
return accounts.get(Accounts.getAccountId(name, character));
}
public Collection<Account> getAccounts() {
return Collections.unmodifiableCollection(accounts.values());
}
public void deleteAccount(String name, String character) {
accounts.remove(Accounts.getAccountId(name, character));
onAccountLoading();
selectedAccount.get();
}
public void deleteAccount(Account account) {
accounts.remove(Accounts.getAccountId(account));
onAccountLoading();
selectedAccount.get();
}
/****************************************
* 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 *
****************************************/
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(SETTINGS.getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(SETTINGS::setSelectedProfile);
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(SETTINGS.getSelectedProfile());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(selectedProfile.getName());
Schedulers.computation().schedule(this::onProfileChanged);
}
}
public Profile getProfile(String name) {
checkProfileMap();
Optional<Profile> p = name == null ? getProfileMap().values().stream().findFirst() : Optional.ofNullable(getProfileMap().get(name));
return p.orElse(null);
}
public boolean hasProfile(String name) {
return getProfileMap().containsKey(name);
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.getConfigurations();
}
public Collection<Profile> getProfiles() {
return getProfileMap().values().stream().filter(t -> StringUtils.isNotBlank(t.getName())).collect(Collectors.toList());
}
public void putProfile(Profile ver) {
if (StringUtils.isBlank(ver.getName()))
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) {
deleteProfile(profile.getName());
}
public void deleteProfile(String profileName) {
getProfileMap().remove(profileName);
checkProfileMap();
Schedulers.computation().schedule(this::onProfileLoading);
}
private void checkProfileMap() {
if (getProfileMap().isEmpty()) {
getProfileMap().put(DEFAULT_PROFILE, new Profile(DEFAULT_PROFILE));
getProfileMap().put(HOME_PROFILE, new Profile(HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY));
}
}
private void onProfileChanged() {
EventBus.EVENT_BUS.fireEvent(new ProfileChangedEvent(this, getSelectedProfile()));
getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
private void profileNameChanged(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {
getProfileMap().put(newValue, getProfileMap().remove(oldValue));
return instance;
}
/**
* Start profiles loading process.
* Invoked by loading GUI phase.
* Should be called from {@link ConfigHolder#init()}.
*/
public void onProfileLoading() {
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
onProfileChanged();
static void init() {
instance = new Settings();
}
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
private Settings() {
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
Profiles.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
String str = getCommonDirectory();
try {
Paths.get(str);
return str;
} catch (InvalidPathException e) {
return getDefaultCommonDirectory();
}
}, config().commonDirectoryProperty(), config().commonDirTypeProperty()));
}
public static String getDefaultCommonDirectory() {
return Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
}
public String getCommonDirectory() {
switch (config().getCommonDirType()) {
case DEFAULT:
return getDefaultCommonDirectory();
case CUSTOM:
return config().getCommonDirectory();
default:
return null;
}
}
}
@@ -17,27 +17,34 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
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 org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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("#B71C1C") // red
};
private final String color;
private final String name;
@@ -68,8 +75,7 @@ public class Theme {
}
public String[] getStylesheets() {
String name = isCustom() ? BLUE.getName() : this.name;
String css = Theme.class.getResource("/assets/css/" + name + ".css").toExternalForm();
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
@@ -77,12 +83,11 @@ public class Theme {
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet", e);
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = Theme.class.getResource("/assets/css/blue.css").toExternalForm();
}
return new String[]{
Theme.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
Theme.class.getResource("/css/jfoenix-design.css").toExternalForm(),
css,
Theme.class.getResource("/assets/css/root.css").toExternalForm()
};
@@ -97,10 +102,18 @@ public class Theme {
public static Optional<Theme> getTheme(String name) {
if (name == null)
return Optional.empty();
for (Theme theme : VALUES)
if (theme.name.equalsIgnoreCase(name))
return Optional.of(theme);
else if (name.equalsIgnoreCase("blue"))
return Optional.of(custom("#5C6BC0"));
else if (name.equalsIgnoreCase("darker_blue"))
return Optional.of(custom("#283593"));
else if (name.equalsIgnoreCase("green"))
return Optional.of(custom("#43A047"));
else if (name.equalsIgnoreCase("orange"))
return Optional.of(custom("#E67E22"));
else if (name.equalsIgnoreCase("purple"))
return Optional.of(custom("#9C27B0"));
else if (name.equalsIgnoreCase("red"))
return Optional.of(custom("#F44336"));
if (name.startsWith("#"))
try {
@@ -117,7 +130,7 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> Settings.INSTANCE.getTheme().getForegroundColor(), Settings.INSTANCE.themeProperty());
return Bindings.createObjectBinding(() -> config().getTheme().getForegroundColor(), config().themeProperty());
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -127,4 +140,16 @@ public class Theme {
public static ObjectBinding<Color> whiteFillBinding() {
return Bindings.createObjectBinding(() -> Color.WHITE);
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
@Override
public void write(JsonWriter out, Theme value) throws IOException {
out.value(value.getName().toLowerCase());
}
@Override
public Theme read(JsonReader in) throws IOException {
return getTheme(in.nextString()).orElse(Theme.BLUE);
}
}
}
@@ -19,14 +19,25 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateBooleanProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateIntegerProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
import org.jackhuang.hmcl.util.javafx.ImmediateStringProperty;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
*
@@ -77,7 +88,7 @@ public final class VersionSetting {
}
/**
* Java version or null if user customizes java directory.
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
@@ -87,6 +98,29 @@ public final class VersionSetting {
javaProperty.set(java);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setDefaultJavaPath(null);
}
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() {
@@ -260,23 +294,6 @@ public final class VersionSetting {
notCheckGameProperty.set(notCheckGame);
}
private final ImmediateBooleanProperty noCommonProperty = new ImmediateBooleanProperty(this, "noCommon", false);
public ImmediateBooleanProperty noCommonProperty() {
return noCommonProperty;
}
/**
* True if HMCL does not find/download libraries in/to common path.
*/
public boolean isNoCommon() {
return noCommonProperty.get();
}
public void setNoCommon(boolean noCommon) {
noCommonProperty.set(noCommon);
}
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
public ImmediateBooleanProperty showLogsProperty() {
@@ -437,22 +454,33 @@ public final class VersionSetting {
if (StringUtils.isBlank(getJava()))
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if ("Custom".equals(getJava())) {
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(new File(getJavaDir()));
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
} catch (IOException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
JavaVersion c = JavaVersion.getJREs().get(getJava());
if (c == null) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().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();
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
@@ -466,7 +494,6 @@ public final class VersionSetting {
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
noCommonProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
@@ -475,6 +502,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 +510,8 @@ public final class VersionSetting {
return new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Launcher.TITLE)
.setProfileName(Launcher.TITLE)
.setVersionName(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
@@ -494,10 +522,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();
@@ -530,11 +558,11 @@ public final class VersionSetting {
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("noCommon", src.isNoCommon());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
return obj;
}
@@ -567,10 +595,10 @@ public final class VersionSetting {
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoCommon(Optional.ofNullable(obj.get("noCommon")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
return vs;
}
@@ -580,7 +608,9 @@ public final class VersionSetting {
}
private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) {
if (primitive.isNumber())
if (primitive == null)
return defaultValue;
else if (primitive.isNumber())
return primitive.getAsInt();
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
@@ -1,119 +0,0 @@
/*
* 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 javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
public class AccountPage extends StackPane implements DecoratorPage {
private final StringProperty title;
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;
public AccountPage(Account account) {
this.account = account;
title = new SimpleStringProperty(this, "title", Launcher.i18n("account") + " - " + account.getCharacter());
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
FXUtils.setLimitWidth(this, 300);
if (account instanceof AuthlibInjectorAccount) {
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
.subscribe(Schedulers.javafx(), variables -> lblServer.setText(variables.get("serverName")));
FXUtils.setLimitHeight(this, 182);
} else {
componentList.removeChildren(paneServer);
if (account instanceof OfflineAccount) {
componentList.removeChildren(paneEmail);
FXUtils.setLimitHeight(this, 110);
} else
FXUtils.setLimitHeight(this, 145);
}
btnDelete.setGraphic(SVG.delete(Theme.blackFillBinding(), 15, 15));
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
lblCharacter.setText(account.getCharacter());
lblType.setText(AddAccountPane.accountType(account));
lblEmail.setText(account.getUsername());
btnRefresh.setVisible(account instanceof YggdrasilAccount);
}
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Controllers.navigate(null);
}
@FXML
private void onRefresh() {
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account).start();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}
@@ -1,260 +0,0 @@
/*
* 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.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Logging;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private JFXProgressBar progressBar;
@FXML private Label lblAddInjectorServer;
@FXML private Hyperlink linkAddInjectorServer;
@FXML private JFXDialogLayout layout;
private final Runnable finalization;
public AddAccountPane(Runnable finalization) {
this.finalization = finalization;
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
loadServers();
cboType.getItems().setAll(Launcher.i18n("account.methods.offline"), Launcher.i18n("account.methods.yggdrasil"), Launcher.i18n("account.methods.authlib_injector"));
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
txtPassword.setVisible(newValue.intValue() != 0);
lblPassword.setVisible(newValue.intValue() != 0);
cboServers.setVisible(newValue.intValue() == 2);
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(Launcher.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
}
private void loadServers() {
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.flatMap(serverURL -> {
try {
return Stream.of(new TwoLineListItem(Accounts.getAuthlibInjectorServerName(serverURL), serverURL));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Authlib-injector server root " + serverURL + " cannot be recognized.", e);
return Stream.empty();
}
})
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
cboServers.getItems().setAll(variables.<Collection<TwoLineListItem>>get("list"));
if (!cboServers.getItems().isEmpty())
cboServers.getSelectionModel().select(0);
}));
}
@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"));
progressBar.setVisible(false);
finalization.run();
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
finalization.run();
} else {
lblCreationWarning.setText(accountException(exception));
}
progressBar.setVisible(false);
}).start();
}
@FXML
private void onCreationCancel() {
finalization.run();
}
@FXML
private void onAddInjecterServer() {
finalization.run();
Controllers.navigate(Controllers.getServersPage());
}
private void showSelector(Node node) {
getChildren().setAll(node);
}
private void closeSelector() {
getChildren().setAll(layout);
}
private class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
{
setStyle("-fx-padding: 8px;");
cancel.setText(Launcher.i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(Launcher.i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount))
return CharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
for (GameProfile profile : names) {
Image image;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
image = FXUtils.DEFAULT_ICON;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == FXUtils.DEFAULT_ICON)
portraitView.setImage(FXUtils.DEFAULT_ICON);
else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
JFXUtilities.runInFX(() -> showSelector(this));
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException(account);
JFXUtilities.runInFX(AddAccountPane.this::closeSelector);
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account);
}
}
}
public static String accountException(Exception exception) {
if (exception instanceof InvalidCredentialsException) {
return Launcher.i18n("account.failed.invalid_credentials");
} else if (exception instanceof NoCharacterException) {
return Launcher.i18n("account.failed.no_charactor");
} else if (exception instanceof ServerDisconnectException) {
return Launcher.i18n("account.failed.connect_authentication_server");
} else if (exception instanceof InvalidTokenException) {
return Launcher.i18n("account.failed.invalid_token");
} else if (exception instanceof InvalidPasswordException) {
return Launcher.i18n("account.failed.invalid_password");
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return Launcher.i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return Launcher.i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return Launcher.i18n("account.methods.yggdrasil");
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
}
}
@@ -1,163 +0,0 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Collection;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("account.injector.server"));
@FXML private ScrollPane scrollPane;
@FXML private StackPane addServerContainer;
@FXML private Label lblServerIp;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@FXML private Label lblServerWarning;
@FXML private VBox listPane;
@FXML private JFXTextField txtServerIp;
@FXML private JFXDialogLayout addServerPane;
@FXML private JFXDialogLayout confirmServerPane;
@FXML private JFXDialog dialog;
@FXML private StackPane contentPane;
@FXML private JFXSpinner spinner;
@FXML private JFXProgressBar progressBar;
@FXML private JFXButton btnAddNext;
private final TransitionHandler transitionHandler;
{
FXUtils.loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
FXUtils.smoothScrolling(scrollPane);
transitionHandler = new TransitionHandler(addServerContainer);
getChildren().remove(dialog);
dialog.setDialogContainer(this);
txtServerIp.textProperty().addListener((a, b, newValue) ->
btnAddNext.setDisable(!txtServerIp.validate()));
loading();
}
private void removeServer(AuthlibInjectorServerItem item) {
Settings.INSTANCE.removeAuthlibInjectorServerURL(item.getInfo().getServerIp());
loading();
}
private void loading() {
getChildren().remove(contentPane);
spinner.setVisible(true);
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.flatMap(serverURL -> {
try {
return Stream.of(new AuthlibInjectorServerItem(new AuthlibInjectorServerInfo(serverURL, Accounts.getAuthlibInjectorServerName(serverURL)), this::removeServer));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Authlib-injector server root " + serverURL + " cannot be recognized.", e);
return Stream.empty();
}
})
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
listPane.getChildren().setAll(variables.<Collection<? extends Node>>get("list"));
loadingCompleted();
}));
}
private void loadingCompleted() {
getChildren().add(contentPane);
spinner.setVisible(false);
if (Settings.INSTANCE.getAuthlibInjectorServerURLs().isEmpty())
onAdd();
}
@FXML
private void onAdd() {
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
txtServerIp.setText("");
addServerPane.setDisable(false);
progressBar.setVisible(false);
dialog.show();
}
@FXML
private void onAddCancel() {
dialog.close();
}
@FXML
private void onAddNext() {
String serverIp = txtServerIp.getText();
progressBar.setVisible(true);
addServerPane.setDisable(true);
Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(serverIp))
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
addServerPane.setDisable(false);
if (isDependentsSucceeded) {
lblServerName.setText(variables.get("serverName"));
lblServerIp.setText(txtServerIp.getText());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverIp).getProtocol()));
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else
lblCreationWarning.setText(variables.<Exception>get("lastException").getLocalizedMessage());
}).start();
}
@FXML
private void onAddPrev() {
transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
}
@FXML
private void onAddFinish() {
String ip = txtServerIp.getText();
if (!ip.endsWith("/"))
ip += "/";
Settings.INSTANCE.addAuthlibInjectorServerURL(ip);
loading();
dialog.close();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}
@@ -17,34 +17,61 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.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.JavaVersion;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
public static final int UI_VERSION = 1;
private static Scene scene;
private static Stage stage;
private static MainPage mainPage = null;
private static SettingsPage settingsPage = null;
private static VersionPage versionPage = null;
private static GameList gameListPage = null;
private static AccountList accountListPage = null;
private static ProfileList profileListPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static LeftPaneController leftPaneController;
private static Decorator decorator;
private static DecoratorController decorator;
public static Scene getScene() {
return scene;
@@ -61,6 +88,33 @@ public final class Controllers {
return settingsPage;
}
// FXThread
public static GameList getGameListPage() {
if (gameListPage == null)
gameListPage = new GameList();
return gameListPage;
}
// FXThread
public static AccountList getAccountListPage() {
if (accountListPage == null) {
accountListPage = new AccountList();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
}
return accountListPage;
}
// FXThread
public static ProfileList getProfileListPage() {
if (profileListPage == null) {
profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
}
return profileListPage;
}
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
@@ -76,13 +130,65 @@ public final class Controllers {
}
// FXThread
public static Decorator getDecorator() {
public static DecoratorController getDecorator() {
return decorator;
}
public static MainPage getMainPage() {
if (mainPage == null)
if (mainPage == null) {
mainPage = new MainPage();
mainPage.setOnDragOver(event -> {
if (event.getGestureSource() != mainPage && event.getDragboard().hasFiles()) {
if (event.getDragboard().getFiles().stream().anyMatch(it -> "zip".equals(FileUtils.getExtension(it))))
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
event.consume();
});
mainPage.setOnDragDropped(event -> {
List<File> files = event.getDragboard().getFiles();
if (files != null) {
List<File> modpacks = files.stream()
.filter(it -> "zip".equals(FileUtils.getExtension(it)))
.collect(Collectors.toList());
if (!modpacks.isEmpty()) {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(modpack), i18n("install.modpack"));
event.setDropCompleted(true);
}
}
event.consume();
});
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
mainPage.setCurrentGame(version);
} else {
mainPage.setCurrentGame(i18n("version.empty"));
}
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
MultiStepBinding.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing(Version::getReleaseTime).thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
return node;
})
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
});
}
return mainPage;
}
@@ -91,38 +197,28 @@ public final class Controllers {
}
public static void initialize(Stage stage) {
Logging.LOG.info("Start initializing application");
Controllers.stage = stage;
stage.setOnCloseRequest(e -> Launcher.stopApplication());
decorator = new Decorator(stage, getMainPage(), Launcher.TITLE, false, true);
decorator.showPage(null);
leftPaneController = new LeftPaneController(decorator.getLeftPane());
decorator = new DecoratorController(stage, getMainPage());
leftPaneController = new LeftPaneController();
decorator.getDecorator().drawerProperty().setAll(leftPaneController);
Settings.INSTANCE.onProfileLoading();
Task.of(JavaVersion::initialize).start();
decorator.setCustomMaximize(false);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().setAll(Settings.INSTANCE.getTheme().getStylesheets());
stage.setMinWidth(804);
stage.setMaxWidth(804);
stage.setMinHeight(521);
stage.setMaxHeight(521);
scene = new Scene(decorator.getDecorator(), 800, 519);
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.setTitle(Launcher.TITLE);
stage.setTitle(Metadata.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) {
@@ -138,39 +234,38 @@ 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);
}
public static void closeDialog() {
decorator.getDialog().close();
return pane;
}
public static void navigate(Node node) {
if (decorator.getNowPage() == node)
decorator.showPage(null);
else
decorator.showPage(node);
decorator.getNavigator().navigate(node);
}
public static void showUpdate() {
getLeftPaneController().showUpdate();
public static boolean isStopped() {
return decorator == null;
}
public static void shutdown() {
@@ -181,5 +276,8 @@ public final class Controllers {
decorator = null;
stage = null;
scene = null;
gameListPage = null;
accountListPage = null;
profileListPage = null;
}
}
@@ -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.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
@@ -36,10 +39,10 @@ public class CrashWindow extends Stage {
public CrashWindow(String text) {
Label lblCrash = new Label();
if (Launcher.UPDATE_CHECKER.isOutOfDate())
lblCrash.setText(Launcher.i18n("launcher.crash_out_dated"));
if (UpdateChecker.isOutdated())
lblCrash.setText(i18n("launcher.crash_out_dated"));
else
lblCrash.setText(Launcher.i18n("launcher.crash"));
lblCrash.setText(i18n("launcher.crash"));
lblCrash.setWrapText(true);
TextArea textArea = new TextArea();
@@ -47,8 +50,8 @@ public class CrashWindow extends Stage {
textArea.setEditable(false);
Button btnContact = new Button();
btnContact.setText(Launcher.i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT));
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
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(Launcher.i18n("message.error"));
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
}
@@ -1,634 +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.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.svg.SVGGlyph;
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.fxml.FXML;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.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.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
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),
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
private static final SVGGlyph resizeMax = Lang.apply(new SVGGlyph(0, "RESIZE_MAX", "M726 810v-596h-428v596h428zM726 44q34 0 59 25t25 59v768q0 34-25 60t-59 26h-428q-34 0-59-26t-25-60v-768q0-34 25-60t59-26z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
private static final SVGGlyph resizeMin = Lang.apply(new SVGGlyph(0, "RESIZE_MIN", "M80.842 943.158v-377.264h565.894v377.264h-565.894zM0 404.21v619.79h727.578v-619.79h-727.578zM377.264 161.684h565.894v377.264h-134.736v80.842h215.578v-619.79h-727.578v323.37h80.842v-161.686z", Color.WHITE),
glyph -> { glyph.setPrefSize(12, 12); glyph.setSize(12, 12); });
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;
private final BooleanProperty customMaximize = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private final Node mainPage;
private final boolean max, min;
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging, dialogShown, maximized;
private BoundingBox originalBox, maximizedBox;
private final TransitionHandler animationHandler;
@FXML
private StackPane contentPlaceHolder;
@FXML
private StackPane drawerWrapper;
@FXML
private BorderPane titleContainer;
@FXML
private BorderPane leftRootPane;
@FXML
private HBox buttonsContainer;
@FXML
private JFXButton backNavButton;
@FXML
private JFXButton refreshNavButton;
@FXML
private JFXButton closeNavButton;
@FXML
private JFXButton refreshMenuButton;
@FXML
private Label titleLabel;
@FXML
private Label lblTitle;
@FXML
private AdvancedListBox leftPane;
@FXML
private JFXDrawer drawer;
@FXML
private StackPane titleBurgerContainer;
@FXML
private JFXHamburger titleBurger;
@FXML
private JFXDialog dialog;
@FXML
private JFXButton btnMin;
@FXML
private JFXButton btnMax;
@FXML
private JFXButton btnClose;
@FXML
private HBox navLeft;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
}
public Decorator(Stage primaryStage, Node mainPage, String title, boolean max, boolean min) {
this.primaryStage = primaryStage;
this.mainPage = mainPage;
this.max = max;
this.min = min;
FXUtils.loadFXML(this, "/assets/fxml/decorator.fxml");
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)));
titleContainer.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getClickCount() == 2)
btnMax.fire();
});
dialog.setDialogContainer(drawerWrapper);
dialog.setOnDialogClosed(e -> dialogShown = false);
dialog.setOnDialogOpened(e -> dialogShown = true);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
//JFXDepthManager.setDepth(titleContainer, 1);
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
if (!isDragging) allowMove = false;
});
Rectangle rectangle = new Rectangle(0, 0, 0, 0);
rectangle.widthProperty().bind(titleContainer.widthProperty());
rectangle.heightProperty().bind(Bindings.createDoubleBinding(() -> titleContainer.getHeight() + 100, titleContainer.heightProperty()));
titleContainer.setClip(rectangle);
animationHandler = new TransitionHandler(contentPlaceHolder);
loadBackground();
}
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))));
}
private static Image searchBackgroundImage(Image def, String customPath) {
Random random = new Random();
boolean loaded = false;
Image background = def;
// custom path
if (StringUtils.isNotBlank(customPath)) {
try {
background = new Image("file:" + customPath);
loaded = true;
} catch (IllegalArgumentException ignore) {
}
}
// 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;
}
@FXML
private void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!primaryStage.isResizable())
updateInitMouseValues(mouseEvent);
else {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = getBoundsInParent();
if (getBorder() != null && getBorder().getStrokes().size() > 0) {
double borderWidth = this.contentPlaceHolder.snappedLeftInset();
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
setCursor(Cursor.NE_RESIZE);
} else if (y > this.getHeight() - borderWidth) {
setCursor(Cursor.SE_RESIZE);
} else {
setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < borderWidth) {
setCursor(Cursor.NW_RESIZE);
} else if (y > this.getHeight() - borderWidth) {
setCursor(Cursor.SW_RESIZE);
} else {
setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
setCursor(Cursor.N_RESIZE);
} else if (this.isBottomEdge(x, y, boundsInParent)) {
setCursor(Cursor.S_RESIZE);
} else {
setCursor(Cursor.DEFAULT);
}
this.updateInitMouseValues(mouseEvent);
}
}
} else {
setCursor(Cursor.DEFAULT);
}
}
@FXML
private void onMouseReleased() {
isDragging = false;
}
@FXML
private void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltaX = this.newX - this.initX;
double deltaY = this.newY - this.initY;
Cursor cursor = this.getCursor();
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
mouseEvent.consume();
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
mouseEvent.consume();
} else if (this.allowMove) {
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
mouseEvent.consume();
}
}
}
}
@FXML
private void onMin() {
primaryStage.setIconified(true);
}
@FXML
private void onMax() {
if (!max) return;
if (!this.isCustomMaximize()) {
this.primaryStage.setMaximized(!this.primaryStage.isMaximized());
this.maximized = this.primaryStage.isMaximized();
if (this.primaryStage.isMaximized()) {
this.btnMax.setGraphic(resizeMin);
this.btnMax.setTooltip(new Tooltip("Restore Down"));
} else {
this.btnMax.setGraphic(resizeMax);
this.btnMax.setTooltip(new Tooltip("Maximize"));
}
} else {
if (!this.maximized) {
this.originalBox = new BoundingBox(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight());
Screen screen = Screen.getScreensForRectangle(primaryStage.getX(), primaryStage.getY(), primaryStage.getWidth(), primaryStage.getHeight()).get(0);
Rectangle2D bounds = screen.getVisualBounds();
this.maximizedBox = new BoundingBox(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
primaryStage.setX(this.maximizedBox.getMinX());
primaryStage.setY(this.maximizedBox.getMinY());
primaryStage.setWidth(this.maximizedBox.getWidth());
primaryStage.setHeight(this.maximizedBox.getHeight());
this.btnMax.setGraphic(resizeMin);
this.btnMax.setTooltip(new Tooltip("Restore Down"));
} else {
primaryStage.setX(this.originalBox.getMinX());
primaryStage.setY(this.originalBox.getMinY());
primaryStage.setWidth(this.originalBox.getWidth());
primaryStage.setHeight(this.originalBox.getHeight());
this.originalBox = null;
this.btnMax.setGraphic(resizeMax);
this.btnMax.setTooltip(new Tooltip("Maximize"));
}
this.maximized = !this.maximized;
}
}
@FXML
private void onClose() {
onCloseButtonAction.get().run();
}
private void updateInitMouseValues(MouseEvent mouseEvent) {
initX = mouseEvent.getScreenX();
initY = mouseEvent.getScreenY();
xOffset = mouseEvent.getSceneX();
yOffset = mouseEvent.getSceneY();
}
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
return x < getWidth() && x > getWidth() - contentPlaceHolder.snappedLeftInset();
}
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
return y >= 0 && y < contentPlaceHolder.snappedLeftInset();
}
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
return y < getHeight() && y > getHeight() - contentPlaceHolder.snappedLeftInset();
}
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
return x >= 0 && x < contentPlaceHolder.snappedLeftInset();
}
private boolean setStageWidth(double width) {
if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) {
primaryStage.setWidth(width);
initX = newX;
return true;
} else {
if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth())
primaryStage.setWidth(titleContainer.getMinWidth());
return false;
}
}
private boolean setStageHeight(double height) {
if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) {
primaryStage.setHeight(height);
initY = newY;
return true;
} else {
if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight())
primaryStage.setHeight(titleContainer.getHeight());
return false;
}
}
public void setMaximized(boolean maximized) {
if (this.maximized != maximized) {
Platform.runLater(btnMax::fire);
}
}
private void showCloseNavButton() {
navLeft.getChildren().add(closeNavButton);
}
private void hideCloseNavButton() {
navLeft.getChildren().remove(closeNavButton);
}
private void setContent(Node content, AnimationProducer animation) {
isWizardPageNow = false;
animationHandler.setContent(content, animation);
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content, true);
}
refreshNavButton.setVisible(content instanceof Refreshable);
backNavButton.setVisible(content != mainPage);
String prefix = category == null ? "" : category + " - ";
titleLabel.textProperty().unbind();
if (content instanceof WizardPage)
titleLabel.setText(prefix + ((WizardPage) content).getTitle());
if (content instanceof DecoratorPage)
titleLabel.textProperty().bind(((DecoratorPage) content).titleProperty());
}
private String category;
private Node nowPage;
private boolean isWizardPageNow;
public Node getNowPage() {
return nowPage;
}
public void showPage(Node content) {
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
if (content != null)
contentPlaceHolder.getStyleClass().add("gray-background");
Node c = content == null ? mainPage : content;
onEnd();
if (nowPage instanceof DecoratorPage)
((DecoratorPage) nowPage).onClose();
nowPage = content;
setContent(c, ContainerAnimations.FADE.getAnimationProducer());
if (c instanceof Region) {
// Let root pane fix window size.
StackPane parent = (StackPane) c.getParent();
((Region) c).prefWidthProperty().bind(parent.widthProperty());
((Region) c).prefHeightProperty().bind(parent.heightProperty());
}
}
public JFXDialog showDialog(Region content) {
dialog.setContent(content);
if (!dialogShown)
dialog.show();
return dialog;
}
public void startWizard(WizardProvider wizardProvider) {
startWizard(wizardProvider, null);
}
public void startWizard(WizardProvider wizardProvider, String category) {
this.category = category;
wizardController.setProvider(wizardProvider);
wizardController.onStart();
}
@Override
public void onStart() {
backNavButton.setVisible(true);
backNavButton.setDisable(false);
showCloseNavButton();
refreshNavButton.setVisible(false);
}
@Override
public void onEnd() {
backNavButton.setVisible(false);
hideCloseNavButton();
refreshNavButton.setVisible(false);
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
contentPlaceHolder.getStyleClass().add("white-background");
setContent(page, nav.getAnimation().getAnimationProducer());
isWizardPageNow = true;
}
@FXML
private void onRefresh() {
((Refreshable) contentPlaceHolder.getChildren().get(0)).refresh();
}
@FXML
private void onCloseNav() {
wizardController.onCancel();
showPage(null);
}
@FXML
private void onBack() {
if (isWizardPageNow && wizardController.canPrev())
wizardController.onPrev(true);
else
onCloseNav();
}
@Override
public Queue<Object> getCancelQueue() {
return cancelQueue;
}
public Runnable getOnCloseButtonAction() {
return onCloseButtonAction.get();
}
public ObjectProperty<Runnable> onCloseButtonActionProperty() {
return onCloseButtonAction;
}
public void setOnCloseButtonAction(Runnable onCloseButtonAction) {
this.onCloseButtonAction.set(onCloseButtonAction);
}
public boolean isCustomMaximize() {
return customMaximize.get();
}
public BooleanProperty customMaximizeProperty() {
return customMaximize;
}
public void setCustomMaximize(boolean customMaximize) {
this.customMaximize.set(customMaximize);
}
@Override
public WizardController getWizardController() {
return wizardController;
}
public JFXDialog getDialog() {
return dialog;
}
public AdvancedListBox getLeftPane() {
return leftPane;
}
}
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.task.SilentException;
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
@@ -37,12 +38,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,47 +17,70 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.concurrency.JFXUtilities;
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;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
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.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
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.thread;
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));
}
@@ -67,13 +90,18 @@ public final class FXUtils {
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onChange(value, consumer);
consumer.accept(value.getValue());
onChange(value, consumer);
}
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
onWeakChange(value, consumer);
consumer.accept(value.getValue());
onWeakChange(value, consumer);
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();
}
public static void limitSize(ImageView imageView, double maxWidth, double maxHeight) {
@@ -114,7 +142,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);
@@ -179,21 +207,27 @@ public final class FXUtils {
return region.getMaxHeight();
}
public static Node limitingSize(Node node, double width, double height) {
StackPane pane = new StackPane(node);
pane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(pane, width);
FXUtils.setLimitHeight(pane, height);
return pane;
}
public static void smoothScrolling(ScrollPane scrollPane) {
JFXScrollPane.smoothScrolling(scrollPane);
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), Launcher.RESOURCE_BUNDLE);
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
}
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
pane.getChildren().setAll(children);
try {
loader.load();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static void installTooltip(Node node, String tooltip) {
@@ -201,22 +235,29 @@ 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) {
JFXUtilities.runInFX(() -> {
try {
// Java 9
call(tooltip, "setShowDelay", new Duration(openDelay));
call(tooltip, "setShowDuration", new Duration(visibleDelay));
call(tooltip, "setHideDelay", new Duration(closeDelay));
} catch (Throwable e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
// Java 8
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
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", e);
}
Tooltip.install(node, tooltip);
}
Tooltip.install(node, tooltip);
}
});
}
public static void openFolder(File file) {
@@ -236,11 +277,15 @@ public final class FXUtils {
}
break;
default:
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
}
});
}
}
@@ -252,17 +297,21 @@ public final class FXUtils {
public static void openLink(String link) {
if (link == null)
return;
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
Runtime.getRuntime().exec(new String[] { "/usr/bin/open", link });
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
}
});
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
@@ -303,8 +352,10 @@ public final class FXUtils {
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
unbindEnum(comboBox);
ChangeListener<Number> listener = (a, b, newValue) ->
@@ -321,8 +372,9 @@ public final class FXUtils {
* @see #bindEnum(JFXComboBox, Property)
*/
@SuppressWarnings("unchecked")
@Deprecated
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 +420,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
@@ -381,4 +483,11 @@ public final class FXUtils {
return "Interpolator.SINE";
}
};
public static Runnable withJFXPopupClosing(Runnable runnable, JFXPopup popup) {
return () -> {
runnable.run();
popup.hide();
};
}
}
@@ -1,96 +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 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;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
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 java.util.LinkedList;
import java.util.Optional;
import java.util.function.Consumer;
public class InstallerController {
private Profile profile;
private String versionId;
private Version version;
@FXML
private ScrollPane scrollPane;
@FXML private VBox contentPane;
private String forge;
private String liteLoader;
private String optiFine;
@FXML
private void initialize() {
FXUtils.smoothScrolling(scrollPane);
}
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
this.version = profile.getRepository().getResolvedVersion(versionId);
contentPane.getChildren().clear();
forge = liteLoader = optiFine = null;
for (Library library : version.getLibraries()) {
Consumer<InstallerItem> removeAction = x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction));
forge = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction));
liteLoader = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction));
optiFine = library.getVersion();
}
}
}
@FXML
private void onAdd() {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
Controllers.dialog("version.cannot_read");
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
}
}
@@ -21,10 +21,11 @@ import com.jfoenix.effects.JFXDepthManager;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.Launcher;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
*/
@@ -44,7 +45,7 @@ public class InstallerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblInstallerArtifact.setText(artifact);
lblInstallerVersion.setText(Launcher.i18n("archive.version") + ": " + version);
lblInstallerVersion.setText(i18n("archive.version") + ": " + version);
}
@FXML
@@ -18,184 +18,103 @@
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.input.MouseButton;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.game.AccountHelper;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
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.*;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.Versions;
import java.io.File;
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 VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(Launcher.i18n("account.missing"), Launcher.i18n("message.unknown"));
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
public final class LeftPaneController extends AdvancedListBox {
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), Launcher.i18n("settings.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
public LeftPaneController() {
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
}
});
leftPane
.add(new ClassTitle(Launcher.i18n("account").toUpperCase(), Lang.apply(new JFXButton(), button -> {
button.setGraphic(SVG.plus(Theme.blackFillBinding(), 10, 10));
button.getStyleClass().add("toggle-icon-tiny");
button.setOnMouseClicked(e -> addNewAccount());
})))
.add(accountPane)
.startCategory(Launcher.i18n("launcher").toUpperCase())
.add(launcherSettingsItem)
.add(new ClassTitle(Launcher.i18n("profile.title").toUpperCase(), Lang.apply(new JFXButton(), button -> {
button.setGraphic(SVG.plus(Theme.blackFillBinding(), 10, 10));
button.getStyleClass().add("toggle-icon-tiny");
button.setOnMouseClicked(e ->
Controllers.getDecorator().showPage(new ProfilePage(null)));
})))
.add(profilePane);
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage()));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
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);
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(new Image("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
onAccountsLoading();
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(new Image("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
this
.startCategory(i18n("account").toUpperCase())
.add(accountListItem)
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
}
// ==== Accounts ====
public void checkAccount() {
if (Accounts.getAccounts().isEmpty())
Platform.runLater(this::addNewAccount);
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane(Controllers::closeDialog));
}
private void onSelectedAccountChanged(Account newAccount) {
Platform.runLater(() -> {
for (Node node : accountPane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) {
boolean current = Objects.equals(node.getProperties().get("account"), newAccount);
((RipplerContainer) node).setSelected(current);
}
}
});
}
private void onProfileChanged(ProfileChangedEvent event) {
Profile profile = event.getProfile();
Platform.runLater(() -> {
for (Node node : profilePane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof String) {
boolean current = Objects.equals(node.getProperties().get("profile"), profile.getName());
((RipplerContainer) node).setSelected(current);
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? Launcher.i18n("profile.selected") : "");
}
}
});
}
private void onProfilesLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
for (Profile profile : Settings.INSTANCE.getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
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());
list.add(ripplerContainer);
}
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return Launcher.i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account);
JFXPopup popup = new JFXPopup(accountPage);
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, 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount)
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
.subscribe(Schedulers.javafx(), variables -> FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(variables.get("serverName"))));
if (selectedAccount == account)
ripplerContainer.setSelected(true);
list.add(ripplerContainer);
}
if (Settings.INSTANCE.getAccounts().isEmpty()) {
RipplerContainer container = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
container.setOnMouseClicked(e -> addNewAccount());
list.add(container);
}
Platform.runLater(() -> accountPane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(Launcher.i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
Controllers.dialog(new AddAccountPane());
}
// ====
private boolean checkedModpack = false;
private static boolean showNewAccount = true;
private void onRefreshedVersions(RefreshedVersionsEvent event) {
private void onRefreshedVersions(HMCLGameRepository repository) {
JFXUtilities.runInFX(() -> {
boolean flag = true;
HMCLGameRepository repository = (HMCLGameRepository) event.getSource();
if (!checkedModpack) {
checkedModpack = true;
@@ -203,27 +122,26 @@ 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(),
Launcher.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();
}
});
}
public void checkAccount() {
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
}
@@ -0,0 +1,64 @@
/*
* 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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
public abstract class ListPage<T extends Node> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
public abstract void add();
@Override
protected Skin<?> createDefaultSkin() {
return new ListPageSkin(this);
}
public ObservableList<T> getItems() {
return items.get();
}
public void setItems(ObservableList<T> items) {
this.items.set(items);
}
public ListProperty<T> itemsProperty() {
return items;
}
public boolean isLoading() {
return loading.get();
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public BooleanProperty loadingProperty() {
return loading;
}
}
@@ -0,0 +1,90 @@
/*
* 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.JFXScrollPane;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
public class ListPageSkin extends SkinBase<ListPage> {
public ListPageSkin(ListPage<?> skinnable) {
super(skinnable);
StackPane rootPane = new StackPane();
JFXSpinner spinner = new JFXSpinner();
spinner.setRadius(16);
spinner.getStyleClass().setAll("materialDesign-purple", "first-spinner");
StackPane contentPane = new StackPane();
{
ScrollPane scrollPane = new ScrollPane();
{
scrollPane.setFitToWidth(true);
VBox list = new VBox();
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
list.setPadding(new Insets(10));
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(list);
JFXScrollPane.smoothScrolling(scrollPane);
}
VBox vBox = new VBox();
{
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
vBox.setPadding(new Insets(15));
vBox.setSpacing(15);
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().setAll("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
vBox.getChildren().setAll(btnAdd);
}
contentPane.getChildren().setAll(scrollPane, vBox);
}
rootPane.getChildren().setAll(contentPane);
skinnable.loadingProperty().addListener((a, b, newValue) -> {
if (newValue) rootPane.getChildren().setAll(spinner);
else rootPane.getChildren().setAll(contentPane);
});
getChildren().setAll(rootPane);
}
}
@@ -30,21 +30,22 @@ import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.concurrent.CountDownLatch;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
*
* @author huangyuhui
@@ -62,8 +63,8 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Settings.INSTANCE.getTheme().getStylesheets());
setTitle(Launcher.i18n("logwindow.title"));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -174,12 +175,12 @@ public final class LogWindow extends Stage {
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", Settings.INSTANCE.getFont().getSize() + "px \"" + Settings.INSTANCE.getFont().getFamily() + "\""));
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
body = document.getElementsByTagName("body").item(0);
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
engine.executeScript("limitedLogs=" + config().getLogLines());
latch.countDown();
onDone.fireEvent(new Event(LogWindow.this));
}
@@ -187,14 +188,14 @@ public final class LogWindow extends Stage {
boolean flag = false;
for (String i : cboLines.getItems())
if (Integer.toString(Settings.INSTANCE.getLogLines()).equals(i)) {
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
Settings.INSTANCE.setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + config().getLogLines());
});
if (!flag)
@@ -17,213 +17,192 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Profile;
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.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 org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("main_page"));
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("main_page"));
private final PopupMenu menu = new PopupMenu();
private final JFXPopup popup = new JFXPopup(menu);
private Profile profile;
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion");
private final ObservableList<Node> versions = FXCollections.observableArrayList();
@FXML
private JFXButton btnRefresh;
@FXML
private StackPane contentPane;
@FXML
private JFXButton btnAdd;
@FXML
private JFXSpinner spinner;
@FXML
private JFXMasonryPane masonryPane;
private StackPane updatePane;
private JFXButton menuButton;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
setPadding(new Insets(25));
loadingVersions();
updatePane = new StackPane();
updatePane.setVisible(false);
updatePane.getStyleClass().add("bubble");
FXUtils.setLimitWidth(updatePane, 230);
FXUtils.setLimitHeight(updatePane, 55);
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
updatePane.setOnMouseClicked(e -> onUpgrade());
FXUtils.onChange(showUpdateProperty(), this::doAnimation);
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())
JFXUtilities.runInFXAndWait(this::loadingVersions);
});
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(event -> {
this.profile = event.getProfile();
});
{
HBox hBox = new HBox();
hBox.setSpacing(12);
hBox.setAlignment(Pos.CENTER_LEFT);
StackPane.setAlignment(hBox, Pos.CENTER_LEFT);
StackPane.setMargin(hBox, new Insets(9, 12, 9, 16));
{
Label lblIcon = new Label();
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), Launcher.i18n("install")));
FXUtils.installTooltip(btnAdd, Launcher.i18n("install"));
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersionsAsync().start());
FXUtils.installTooltip(btnRefresh, Launcher.i18n("button.refresh"));
}
TwoLineListItem prompt = new TwoLineListItem();
prompt.setTitleFill(Color.WHITE);
prompt.setSubtitleFill(Color.WHITE);
prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false);
prompt.setStyle("-jfx-title-font-weight: BOLD;");
prompt.titleProperty().bind(latestVersionProperty());
private Node buildNode(HMCLGameRepository repository, Version version, String game) {
Profile profile = repository.getProfile();
String id = version.getId();
VersionItem item = new VersionItem();
item.setUpdate(repository.isModpack(id));
item.setGameVersion(game);
item.setVersionName(id);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Launcher.i18n("install.installer.forge")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)forge", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(Launcher.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(Launcher.i18n("install.installer.optifine")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)optifine", "").replace(game, "").trim(), "-"), "-")).append("\n");
hBox.getChildren().setAll(lblIcon, prompt);
}
JFXButton closeUpdateButton = new JFXButton();
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT);
closeUpdateButton.getStyleClass().add("toggle-icon-tiny");
StackPane.setMargin(closeUpdateButton, new Insets(5));
closeUpdateButton.setOnMouseClicked(e -> closeUpdateBubble());
updatePane.getChildren().setAll(hBox, closeUpdateButton);
}
item.setLibraries(libraries.toString());
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Launcher.i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
chooser.setInitialDirectory(repository.getRunDirectory(id));
chooser.setTitle(Launcher.i18n("version.launch_script.save"));
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(Launcher.i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(Launcher.i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
}
});
item.setOnSettingsButtonClicked(e -> {
Controllers.getDecorator().showPage(Controllers.getVersionPage());
Controllers.getVersionPage().load(id, profile);
});
item.setOnUpdateButtonClicked(event -> {
FileChooser chooser = new FileChooser();
chooser.setTitle(Launcher.i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(null);
try {
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id)))
.then(Task.of(Schedulers.javafx(), Controllers::closeDialog)).executor();
pane.setExecutor(executor);
pane.setTitle(Launcher.i18n("modpack.update"));
executor.start();
Controllers.dialog(pane);
} catch (UnsupportedModpackException e) {
Controllers.dialog(Launcher.i18n("modpack.unsupported"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(Launcher.i18n("modpack.mismatched_type"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
Controllers.dialog(Launcher.i18n("modpack.invalid"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
}
});
item.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.SECONDARY) {
JFXListView<String> versionList = new JFXListView<>();
JFXPopup versionPopup = new JFXPopup(versionList);
versionList.getStyleClass().add("option-list-view");
FXUtils.setLimitWidth(versionList, 150);
versionList.getItems().setAll(Lang.immutableListOf(
Launcher.i18n("version.manage.rename"),
Launcher.i18n("version.manage.remove"),
Launcher.i18n("modpack.export"),
Launcher.i18n("folder.game")
));
versionList.setOnMouseClicked(e ->{
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
VersionPage.renameVersion(profile, id);
break;
case 1:
VersionPage.deleteVersion(profile, id);
break;
case 2:
VersionPage.exportVersion(profile, id);
break;
case 3:
FXUtils.openFolder(repository.getRunDirectory(id));
break;
default:
throw new Error();
}});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.dialog(Launcher.i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
}
});
File iconFile = repository.getVersionIcon(id);
if (iconFile.exists())
item.setImage(new Image("file:" + iconFile.getAbsolutePath()));
return item;
}
StackPane launchPane = new StackPane();
launchPane.setMaxWidth(230);
launchPane.setMaxHeight(55);
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
{
JFXButton launchButton = new JFXButton();
launchButton.setPrefWidth(230);
launchButton.setPrefHeight(55);
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.getStyleClass().add("jfx-button-raised");
launchButton.setOnMouseClicked(e -> launch());
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
{
VBox graphic = new VBox();
graphic.setAlignment(Pos.CENTER);
graphic.setTranslateX(-7);
graphic.setMaxWidth(200);
Label launchLabel = new Label(i18n("version.launch"));
launchLabel.setStyle("-fx-font-size: 16px;");
Label currentLabel = new Label();
currentLabel.setStyle("-fx-font-size: 12px;");
currentLabel.textProperty().bind(currentGameProperty());
graphic.getChildren().setAll(launchLabel, currentLabel);
private void loadingVersions() {
getChildren().setAll(spinner);
FXUtils.resetChildren(masonryPane, Collections.emptyList());
}
launchButton.setGraphic(graphic);
}
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = new LinkedList<>();
for (Version version : repository.getVersions()) {
children.add(buildNode(repository, version, GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")));
Rectangle separator = new Rectangle();
separator.getStyleClass().add("darker-fill");
separator.setWidth(1);
separator.setHeight(57);
separator.setTranslateX(95);
separator.setMouseTransparent(true);
menuButton = new JFXButton();
menuButton.setPrefHeight(55);
menuButton.setPrefWidth(230);
menuButton.setButtonType(JFXButton.ButtonType.RAISED);
menuButton.getStyleClass().add("jfx-button-raised");
menuButton.setStyle("-fx-font-size: 15px;");
menuButton.setOnMouseClicked(e -> onMenu());
menuButton.setClip(new Rectangle(211, -100, 100, 200));
StackPane graphic = new StackPane();
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
graphic.getChildren().setAll(svg);
graphic.setTranslateX(12);
menuButton.setGraphic(graphic);
launchPane.getChildren().setAll(launchButton, separator, menuButton);
}
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile()) {
getChildren().setAll(contentPane);
FXUtils.resetChildren(masonryPane, children);
}
});
getChildren().setAll(updatePane, launchPane);
menu.setMaxHeight(365);
menu.setMaxWidth(545);
menu.setAlwaysShowingVBar(true);
menu.setOnMouseClicked(e -> popup.hide());
Bindings.bindContent(menu.getContent(), versions);
}
private void doAnimation(boolean show) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)),
new KeyFrame(duration,
new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE)));
if (show) nowAnimation.getKeyFrames().add(
new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true)));
else nowAnimation.getKeyFrames().add(
new KeyFrame(duration, e -> updatePane.setVisible(false)));
nowAnimation.play();
}
private void launch() {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion());
}
private void onMenu() {
popup.show(menuButton, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -menuButton.getHeight());
}
private void onUpgrade() {
RemoteVersion target = UpdateChecker.getLatestVersion();
if (target == null) {
return;
}
UpdateHandler.updateFrom(target);
}
private void closeUpdateBubble() {
showUpdate.unbind();
showUpdate.set(false);
}
public String getTitle() {
@@ -231,11 +210,51 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
@Override
public StringProperty titleProperty() {
return title;
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public void setTitle(String title) {
this.title.set(title);
}
public String getCurrentGame() {
return currentGame.get();
}
public StringProperty currentGameProperty() {
return currentGame;
}
public void setCurrentGame(String currentGame) {
this.currentGame.set(currentGame);
}
public boolean isShowUpdate() {
return showUpdate.get();
}
public BooleanProperty showUpdateProperty() {
return showUpdate;
}
public void setShowUpdate(boolean showUpdate) {
this.showUpdate.set(showUpdate);
}
public String getLatestVersion() {
return latestVersion.get();
}
public StringProperty latestVersionProperty() {
return latestVersion;
}
public void setLatestVersion(String latestVersion) {
this.latestVersion.set(latestVersion);
}
public ObservableList<Node> getVersions() {
return versions;
}
}
@@ -18,8 +18,10 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
@@ -33,6 +35,12 @@ public final class SVG {
path.setContent(d);
path.fillProperty().bind(fill);
if (width < 0 || height < 0) {
StackPane pane = new StackPane(path);
pane.setAlignment(Pos.CENTER);
return pane;
}
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
svg.setScaleX(scale);
@@ -83,6 +91,10 @@ public final class SVG {
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 launch2(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 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);
}
@@ -127,4 +139,27 @@ public final class SVG {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height);
}
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z", fill, width, height);
}
public static Node openInNew(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.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1,21H23L12,2", fill, width, height);
}
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
}
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
}
}
@@ -17,209 +17,165 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXColorPicker;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.*;
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.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.*;
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.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.net.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.Optional;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("settings.launcher"));
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelectedPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
@FXML
private JFXTextField txtProxyHost;
@FXML
private JFXTextField txtProxyPort;
@FXML
private JFXTextField txtProxyUsername;
@FXML
private JFXPasswordField txtProxyPassword;
@FXML
private JFXTextField txtFontSize;
@FXML
private JFXComboBox<Label> cboLanguage;
@FXML
private JFXComboBox<?> cboDownloadSource;
@FXML
private FontComboBox cboFont;
@FXML
private FileItem fileCommonLocation;
@FXML
private Label lblDisplay;
@FXML
private Label lblUpdate;
@FXML
private Label lblUpdateSub;
@FXML
private JFXButton btnUpdate;
@FXML
private ScrollPane scroll;
@FXML
private MultiFileItem backgroundItem;
@FXML
private StackPane themeColorPickerContainer;
@FXML
private JFXRadioButton chkNoProxy;
@FXML
private JFXRadioButton chkManualProxy;
@FXML
private JFXRadioButton chkProxyHttp;
@FXML
private JFXRadioButton chkProxySocks;
@FXML
private JFXCheckBox chkProxyAuthentication;
@FXML
private GridPane authPane;
@FXML
private Pane proxyPane;
public final class SettingsPage extends SettingsView implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("settings.launcher"));
{
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
private InvalidationListener updateListener;
public SettingsPage() {
FXUtils.smoothScrolling(scroll);
txtProxyHost.setText(Settings.INSTANCE.getProxyHost());
txtProxyHost.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyHost(newValue));
// ==== Download sources ====
cboDownloadSource.getItems().setAll(DownloadProviders.providersById.keySet());
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
// ====
txtProxyPort.setText(Settings.INSTANCE.getProxyPort());
txtProxyPort.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPort(newValue));
// ==== Font ====
cboFont.valueProperty().bindBidirectional(config().fontFamilyProperty());
txtProxyUsername.setText(Settings.INSTANCE.getProxyUser());
txtProxyUsername.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyUser(newValue));
txtFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
SafeStringConverter.fromFiniteDouble()
.restrict(it -> it > 0)
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtFontSize)));
txtProxyPassword.setText(Settings.INSTANCE.getProxyPass());
txtProxyPassword.textProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setProxyPass(newValue));
lblDisplay.fontProperty().bind(Bindings.createObjectBinding(
() -> Font.font(config().getFontFamily(), config().getFontSize()),
config().fontFamilyProperty(), config().fontSizeProperty()));
// ====
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
// ====
cboFont.getSelectionModel().select(Settings.INSTANCE.getFont().getFamily());
cboFont.valueProperty().addListener((a, b, newValue) -> {
Font font = Font.font(newValue, Settings.INSTANCE.getFont().getSize());
Settings.INSTANCE.setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
});
// ==== Proxy ====
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty());
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
txtFontSize.setText(Double.toString(Settings.INSTANCE.getFont().getSize()));
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
txtFontSize.textProperty().addListener((a, b, newValue) -> {
if (txtFontSize.validate()) {
Font font = Font.font(Settings.INSTANCE.getFont().getFamily(), Double.parseDouble(newValue));
Settings.INSTANCE.setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
}
});
lblDisplay.setStyle("-fx-font: " + Settings.INSTANCE.getFont().getSize() + " \"" + Settings.INSTANCE.getFont().getFamily() + "\";");
ObservableList<Label> list = FXCollections.observableArrayList();
for (Locales.SupportedLocale locale : Locales.LOCALES)
list.add(new Label(locale.getName(Settings.INSTANCE.getLocale().getResourceBundle())));
cboLanguage.setItems(list);
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(Settings.INSTANCE.getLocale()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setLocale(Locales.getLocale(newValue.intValue())));
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
reservedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyHttp.setUserData(Proxy.Type.HTTP);
chkProxyHttp.setToggleGroup(proxyConfigurationGroup);
chkProxySocks.setUserData(Proxy.Type.SOCKS);
chkProxySocks.setToggleGroup(proxyConfigurationGroup);
selectedItemPropertyFor(proxyConfigurationGroup, Proxy.Type.class).bindBidirectional(config().proxyTypeProperty());
// ====
for (Toggle toggle : proxyConfigurationGroup.getToggles())
if (toggle.getUserData() == Settings.INSTANCE.getProxyType())
toggle.setSelected(true);
fileCommonLocation.loadChildren(Arrays.asList(
fileCommonLocation.createChildren(i18n("launcher.common_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(config().commonDirectoryProperty());
fileCommonLocation.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.common_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
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());
// ==== Update ====
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
hasProxyGroup.selectedToggleProperty().addListener((a, b, newValue) ->
Settings.INSTANCE.setHasProxy(newValue != chkNoProxy));
if (UpdateChecker.isOutdated()) {
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
proxyConfigurationGroup.selectedToggleProperty().addListener((a, b, newValue) ->
Settings.INSTANCE.setProxyType((Proxy.Type) newValue.getUserData()));
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
chkProxyAuthentication.setSelected(Settings.INSTANCE.hasProxyAuth());
chkProxyAuthentication.selectedProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setHasProxyAuth(newValue));
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty());
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
FXUtils.installTooltip(btnUpdate, Launcher.i18n("update.tooltip"));
checkUpdate();
lblUpdateNote.setWrappingWidth(470);
// background
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class).bindBidirectional(config().updateChannelProperty());
// ====
// ==== Background ====
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(Launcher.i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
));
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
), EnumBackgroundImage.CUSTOM);
backgroundItem.customTextProperty().bindBidirectional(config().backgroundImageProperty());
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(config().backgroundImageProperty()));
// ====
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));
Settings.INSTANCE.backgroundImageProperty().setChangedListener(it -> initBackgroundItemSubtitle());
Settings.INSTANCE.backgroundImageTypeProperty().setChangedListener(it -> initBackgroundItemSubtitle());
initBackgroundItemSubtitle();
backgroundItem.setToggleSelectedListener(newValue ->
Settings.INSTANCE.setBackgroundImageType((EnumBackgroundImage) newValue.getUserData()));
// theme
JFXColorPicker picker = new JFXColorPicker(Color.web(Settings.INSTANCE.getTheme().getColor()), null);
picker.setCustomColorText(Launcher.i18n("color.custom"));
picker.setRecentColorsText(Launcher.i18n("color.recent"));
picker.getCustomColors().setAll(Arrays.stream(Theme.VALUES).map(Theme::getColor).map(Color::web).collect(Collectors.toList()));
// ==== Theme ====
JFXColorPicker picker = new JFXColorPicker(Color.web(config().getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
Settings.INSTANCE.setTheme(theme);
config().setTheme(theme);
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
});
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
}
private void initBackgroundItemSubtitle() {
switch (Settings.INSTANCE.getBackgroundImageType()) {
case DEFAULT:
backgroundItem.setSubtitle(Launcher.i18n("launcher.background.default"));
break;
case CUSTOM:
backgroundItem.setSubtitle(Settings.INSTANCE.getBackgroundImage());
break;
}
// ====
}
public String getTitle() {
@@ -227,34 +183,25 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}
@Override
public StringProperty titleProperty() {
return title;
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public void setTitle(String title) {
this.title.set(title);
}
public void checkUpdate() {
btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate());
if (Launcher.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(Launcher.i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(Launcher.i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else {
lblUpdateSub.setText(Launcher.i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(Launcher.i18n("update"));
lblUpdate.getStyleClass().setAll();
@Override
protected void onUpdate() {
RemoteVersion target = UpdateChecker.getLatestVersion();
if (target == null) {
return;
}
UpdateHandler.updateFrom(target);
}
@FXML
private void onUpdate() {
Launcher.UPDATE_CHECKER.checkOutdate();
@Override
protected void onHelp() {
FXUtils.openLink(Metadata.HELP_URL);
}
}
@@ -0,0 +1,480 @@
/*
* 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.*;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
public abstract class SettingsView extends StackPane {
protected final JFXTextField txtProxyHost;
protected final JFXTextField txtProxyPort;
protected final JFXTextField txtProxyUsername;
protected final JFXPasswordField txtProxyPassword;
protected final JFXTextField txtFontSize;
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final JFXComboBox<String> cboDownloadSource;
protected final FontComboBox cboFont;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final Label lblDisplay;
protected final Label lblUpdate;
protected final Label lblUpdateSub;
protected final Text lblUpdateNote;
protected final JFXRadioButton chkUpdateStable;
protected final JFXRadioButton chkUpdateDev;
protected final JFXButton btnUpdate;
protected final ScrollPane scroll;
protected final MultiFileItem<EnumBackgroundImage> backgroundItem;
protected final StackPane themeColorPickerContainer;
protected final JFXCheckBox chkDisableProxy;
protected final JFXRadioButton chkProxyHttp;
protected final JFXRadioButton chkProxySocks;
protected final JFXCheckBox chkProxyAuthentication;
protected final GridPane authPane;
protected final Pane proxyPane;
public SettingsView() {
scroll = new ScrollPane();
getChildren().setAll(scroll);
scroll.setStyle("-fx-font-size: 14;");
scroll.setFitToWidth(true);
{
VBox rootPane = new VBox();
rootPane.setStyle("-fx-padding: 18;");
{
ComponentList settingsPane = new ComponentList();
{
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
{
VBox headerLeft = new VBox();
lblUpdate = new Label(i18n("update"));
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().setAll("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
{
btnUpdate = new JFXButton();
btnUpdate.setOnMouseClicked(e -> onUpdate());
btnUpdate.getStyleClass().setAll("toggle-icon4");
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 20, 20));
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox();
content.setSpacing(8);
chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
VBox noteWrapper = new VBox();
noteWrapper.setStyle("-fx-padding: 10 0 0 0;");
lblUpdateNote = new Text(i18n("update.note"));
noteWrapper.getChildren().setAll(lblUpdateNote);
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
{
BorderPane updatePane = new BorderPane();
{
VBox headerLeft = new VBox();
Label help = new Label(i18n("help"));
Label helpSubtitle = new Label(i18n("help.detail"));
helpSubtitle.getStyleClass().setAll("subtitle-label");
headerLeft.getChildren().setAll(help, helpSubtitle);
updatePane.setLeft(headerLeft);
}
{
JFXButton btnExternal = new JFXButton();
btnExternal.setOnMouseClicked(e -> onHelp());
btnExternal.getStyleClass().setAll("toggle-icon4");
btnExternal.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
updatePane.setRight(btnExternal);
}
settingsPane.getContent().add(updatePane);
}
{
fileCommonLocation = new MultiFileItem<>(true);
fileCommonLocation.setTitle(i18n("launcher.common_directory"));
fileCommonLocation.setDirectory(true);
fileCommonLocation.setChooserTitle(i18n("launcher.common_directory.choose"));
fileCommonLocation.setHasSubtitle(true);
fileCommonLocation.setCustomText("settings.custom");
settingsPane.getContent().add(fileCommonLocation);
}
{
backgroundItem = new MultiFileItem<>(true);
backgroundItem.setTitle(i18n("launcher.background"));
backgroundItem.setChooserTitle(i18n("launcher.background.choose"));
backgroundItem.setHasSubtitle(true);
backgroundItem.setCustomText(i18n("settings.custom"));
settingsPane.getContent().add(backgroundItem);
}
{
BorderPane downloadSourcePane = new BorderPane();
{
Label label = new Label(i18n("settings.launcher.download_source"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
downloadSourcePane.setLeft(label);
}
{
cboDownloadSource = new JFXComboBox<>();
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourcePane.setRight(cboDownloadSource);
}
settingsPane.getContent().add(downloadSourcePane);
}
{
BorderPane languagePane = new BorderPane();
Label left = new Label(i18n("settings.launcher.language"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
languagePane.setLeft(left);
cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(locale -> locale.getName(config().getLocalization().getResourceBundle())));
FXUtils.setLimitWidth(cboLanguage, 400);
languagePane.setRight(cboLanguage);
settingsPane.getContent().add(languagePane);
}
{
ComponentList proxyList = new ComponentList();
proxyList.setTitle(i18n("settings.launcher.proxy"));
VBox proxyWrapper = new VBox();
proxyWrapper.setSpacing(10);
{
chkDisableProxy = new JFXCheckBox(i18n("settings.launcher.proxy.disable"));
proxyWrapper.getChildren().add(chkDisableProxy);
}
{
proxyPane = new VBox();
proxyPane.setStyle("-fx-padding: 0 0 0 30;");
ColumnConstraints colHgrow = new ColumnConstraints();
colHgrow.setHgrow(Priority.ALWAYS);
{
HBox hBox = new HBox();
chkProxyHttp = new JFXRadioButton(i18n("settings.launcher.proxy.http"));
chkProxySocks = new JFXRadioButton(i18n("settings.launcher.proxy.socks"));
hBox.getChildren().setAll(chkProxyHttp, chkProxySocks);
proxyPane.getChildren().add(hBox);
}
{
GridPane gridPane = new GridPane();
gridPane.setHgap(20);
gridPane.setVgap(10);
gridPane.setStyle("-fx-padding: 0 0 0 15;");
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), colHgrow);
gridPane.getRowConstraints().setAll(new RowConstraints(), new RowConstraints());
{
Label host = new Label(i18n("settings.launcher.proxy.host"));
GridPane.setRowIndex(host, 1);
GridPane.setColumnIndex(host, 0);
GridPane.setHalignment(host, HPos.RIGHT);
gridPane.getChildren().add(host);
}
{
txtProxyHost = new JFXTextField();
GridPane.setRowIndex(txtProxyHost, 1);
GridPane.setColumnIndex(txtProxyHost, 1);
gridPane.getChildren().add(txtProxyHost);
}
{
Label port = new Label(i18n("settings.launcher.proxy.port"));
GridPane.setRowIndex(port, 2);
GridPane.setColumnIndex(port, 0);
GridPane.setHalignment(port, HPos.RIGHT);
gridPane.getChildren().add(port);
}
{
txtProxyPort = new JFXTextField();
GridPane.setRowIndex(txtProxyPort, 2);
GridPane.setColumnIndex(txtProxyPort, 1);
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
txtProxyHost.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
gridPane.getChildren().add(txtProxyPort);
}
proxyPane.getChildren().add(gridPane);
}
{
VBox vBox = new VBox();
vBox.setStyle("-fx-padding: 20 0 20 5;");
chkProxyAuthentication = new JFXCheckBox(i18n("settings.launcher.proxy.authentication"));
vBox.getChildren().setAll(chkProxyAuthentication);
proxyPane.getChildren().add(vBox);
}
{
authPane = new GridPane();
authPane.setHgap(20);
authPane.setVgap(10);
authPane.setStyle("-fx-padding: 0 0 0 15;");
authPane.getColumnConstraints().setAll(new ColumnConstraints(), colHgrow);
authPane.getRowConstraints().setAll(new RowConstraints(), new RowConstraints());
{
Label username = new Label(i18n("settings.launcher.proxy.username"));
GridPane.setRowIndex(username, 0);
GridPane.setColumnIndex(username, 0);
authPane.getChildren().add(username);
}
{
txtProxyUsername = new JFXTextField();
GridPane.setRowIndex(txtProxyUsername, 0);
GridPane.setColumnIndex(txtProxyUsername, 1);
authPane.getChildren().add(txtProxyUsername);
}
{
Label password = new Label(i18n("settings.launcher.proxy.password"));
GridPane.setRowIndex(password, 1);
GridPane.setColumnIndex(password, 0);
authPane.getChildren().add(password);
}
{
txtProxyPassword = new JFXPasswordField();
GridPane.setRowIndex(txtProxyPassword, 1);
GridPane.setColumnIndex(txtProxyPassword, 1);
authPane.getChildren().add(txtProxyPassword);
}
proxyPane.getChildren().add(authPane);
}
proxyWrapper.getChildren().add(proxyPane);
}
proxyList.getContent().add(proxyWrapper);
settingsPane.getContent().add(proxyList);
}
{
BorderPane themePane = new BorderPane();
Label left = new Label(i18n("settings.launcher.theme"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
themePane.setLeft(left);
themeColorPickerContainer = new StackPane();
themeColorPickerContainer.setMinHeight(30);
themePane.setRight(themeColorPickerContainer);
settingsPane.getContent().add(themePane);
}
{
VBox fontPane = new VBox();
fontPane.setSpacing(5);
settingsPane.getContent().add(fontPane);
{
BorderPane borderPane = new BorderPane();
fontPane.getChildren().add(borderPane);
{
Label left = new Label(i18n("settings.launcher.log_font"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
borderPane.setLeft(left);
}
{
HBox hBox = new HBox();
hBox.setSpacing(3);
cboFont = new FontComboBox(12, false);
txtFontSize = new JFXTextField();
FXUtils.setLimitWidth(txtFontSize, 50);
hBox.getChildren().setAll(cboFont, txtFontSize);
borderPane.setRight(hBox);
}
}
lblDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
fontPane.getChildren().add(lblDisplay);
}
{
StackPane aboutPane = new StackPane();
GridPane gridPane = new GridPane();
gridPane.setHgap(20);
gridPane.setVgap(10);
ColumnConstraints col1 = new ColumnConstraints();
col1.setHgrow(Priority.SOMETIMES);
col1.setMaxWidth(Double.NEGATIVE_INFINITY);
col1.setMinWidth(Double.NEGATIVE_INFINITY);
ColumnConstraints col2 = new ColumnConstraints();
col2.setHgrow(Priority.SOMETIMES);
col2.setMinWidth(20);
col2.setMaxWidth(Double.POSITIVE_INFINITY);
gridPane.getColumnConstraints().setAll(col1, col2);
RowConstraints row = new RowConstraints();
row.setMinHeight(Double.NEGATIVE_INFINITY);
row.setValignment(VPos.TOP);
row.setVgrow(Priority.SOMETIMES);
gridPane.getRowConstraints().setAll(row, row, row, row, row, row);
{
Label label = new Label(i18n("about.copyright"));
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.copyright.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.author"));
GridPane.setRowIndex(label, 1);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.author.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 1);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.thanks_to"));
GridPane.setRowIndex(label, 2);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.thanks_to.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 2);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.dependency"));
GridPane.setRowIndex(label, 3);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.dependency.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 3);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.claim"));
GridPane.setRowIndex(label, 4);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.claim.statement"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 4);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.open_source"));
GridPane.setRowIndex(label, 5);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
{
Label label = new Label(i18n("about.open_source.statement"));
label.setWrapText(true);
GridPane.setRowIndex(label, 5);
GridPane.setColumnIndex(label, 1);
gridPane.getChildren().add(label);
}
aboutPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(aboutPane);
}
rootPane.getChildren().add(settingsPane);
}
scroll.setContent(rootPane);
}
}
protected abstract void onUpdate();
protected abstract void onHelp();
}
@@ -1,127 +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 javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import java.util.Optional;
public final class VersionItem extends StackPane {
@FXML
private Pane icon;
@FXML
private VBox content;
@FXML
private StackPane header;
@FXML
private StackPane body;
@FXML
private JFXButton btnSettings;
@FXML
private JFXButton btnUpdate;
@FXML
private JFXButton btnLaunch;
@FXML
private JFXButton btnScript;
@FXML
private Label lblVersionName;
@FXML
private Label lblGameVersion;
@FXML
private Label lblLibraries;
@FXML
private ImageView iconView;
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(Theme.blackFillBinding(), 15, 15));
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 15, 15));
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 15, 15));
btnScript.setGraphic(SVG.script(Theme.blackFillBinding(), 15, 15));
FXUtils.installTooltip(btnSettings, Launcher.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Launcher.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Launcher.i18n("version.launch_script"));
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
FXUtils.limitSize(iconView, 32, 32);
setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
if (e.getClickCount() == 2)
Optional.ofNullable(launchClickedHandler).ifPresent(h -> h.handle(e));
});
}
public void setUpdate(boolean update) {
btnUpdate.setVisible(update);
}
public void setVersionName(String versionName) {
lblVersionName.setText(versionName);
}
public void setGameVersion(String gameVersion) {
lblGameVersion.setText(gameVersion);
}
public void setLibraries(String libraries) {
lblLibraries.setText(libraries);
}
public void setImage(Image image) {
iconView.setImage(image);
}
public void setOnSettingsButtonClicked(EventHandler<? super MouseEvent> handler) {
btnSettings.setOnMouseClicked(handler);
}
public void setOnScriptButtonClicked(EventHandler<? super MouseEvent> handler) {
btnScript.setOnMouseClicked(handler);
}
public void setOnLaunchButtonClicked(EventHandler<? super MouseEvent> handler) {
launchClickedHandler = handler;
btnLaunch.setOnMouseClicked(handler);
}
public void setOnUpdateButtonClicked(EventHandler<? super MouseEvent> handler) {
btnUpdate.setOnMouseClicked(handler);
}
}
@@ -1,70 +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 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 {
@FXML
private StackPane imageViewContainer;
@FXML
private Label lblVersionName;
@FXML
private Label lblGameVersion;
@FXML
private ImageView imageView;
@FXML private JFXButton btnSettings;
public VersionListItem(String versionName) {
this(versionName, "");
}
public VersionListItem(String versionName, String gameVersion) {
FXUtils.loadFXML(this, "/assets/fxml/version-list-item.fxml");
lblVersionName.setText(versionName);
lblGameVersion.setText(gameVersion);
FXUtils.limitSize(imageView, 32, 32);
}
public void setOnSettingsButtonClicked(EventHandler<? super MouseEvent> handler) {
btnSettings.setOnMouseClicked(handler);
}
public void setVersionName(String versionName) {
lblVersionName.setText(versionName);
}
public void setGameVersion(String gameVersion) {
lblGameVersion.setText(gameVersion);
}
public void setImage(Image image, Rectangle2D viewport) {
imageView.setImage(image);
imageView.setViewport(viewport);
}
}
@@ -1,208 +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.JFXListView;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXTabPane;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import java.io.File;
public final class VersionPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", null);
@FXML
private VersionSettingsController versionSettingsController;
@FXML
private Tab modTab;
@FXML
private ModController modController;
@FXML
private InstallerController installerController;
@FXML
private JFXListView<?> browseList;
@FXML
private JFXListView<?> managementList;
@FXML
private JFXButton btnBrowseMenu;
@FXML
private JFXButton btnDelete;
@FXML
private JFXButton btnManagementMenu;
@FXML
private JFXButton btnExport;
@FXML
private StackPane rootPane;
@FXML
private StackPane contentPane;
@FXML
private JFXTabPane tabPane;
private final JFXPopup browsePopup;
private final JFXPopup managementPopup;
private Profile profile;
private String version;
{
FXUtils.loadFXML(this, "/assets/fxml/version/version.fxml");
getChildren().removeAll(browseList, managementList);
browsePopup = new JFXPopup(browseList);
managementPopup = new JFXPopup(managementList);
FXUtils.installTooltip(btnDelete, Launcher.i18n("version.manage.remove"));
FXUtils.installTooltip(btnBrowseMenu, Launcher.i18n("settings.game.exploration"));
FXUtils.installTooltip(btnManagementMenu, Launcher.i18n("settings.game.management"));
FXUtils.installTooltip(btnExport, Launcher.i18n("modpack.export"));
}
public void load(String id, Profile profile) {
this.version = id;
this.profile = profile;
title.set(Launcher.i18n("settings.game") + " - " + id);
versionSettingsController.loadVersionSetting(profile, id);
modController.setParentTab(tabPane);
modTab.setUserData(modController);
modController.loadMods(profile.getModManager(), id);
installerController.loadVersion(profile, id);
}
@FXML
private void onBrowseMenu() {
browseList.getSelectionModel().select(-1);
browsePopup.show(btnBrowseMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15);
}
@FXML
private void onManagementMenu() {
managementList.getSelectionModel().select(-1);
managementPopup.show(btnManagementMenu, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -12, 15);
}
@FXML
private void onDelete() {
deleteVersion(profile, version);
}
@FXML
private void onExport() {
exportVersion(profile, version);
}
@FXML
private void onBrowse() {
String sub;
switch (browseList.getSelectionModel().getSelectedIndex()) {
case 0:
sub = "";
break;
case 1:
sub = "mods";
break;
case 2:
sub = "coremods";
break;
case 3:
sub = "config";
break;
case 4:
sub = "resourcepacks";
break;
case 5:
sub = "screenshots";
break;
case 6:
sub = "saves";
break;
default:
throw new RuntimeException();
}
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
}
@FXML
private void onManagement() {
switch (managementList.getSelectionModel().getSelectedIndex()) {
case 0: // rename a version
renameVersion(profile, version);
break;
case 1: // remove a version
deleteVersion(profile, version);
break;
case 2: // redownload asset index
new GameAssetIndexDownloadTask(profile.getDependency(), profile.getRepository().getResolvedVersion(version)).start();
break;
case 3: // delete libraries
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
break;
case 4:
throw new Error();
}
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public static void deleteVersion(Profile profile, String version) {
Controllers.confirmDialog(Launcher.i18n("version.manage.remove.confirm", version), Launcher.i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
}
}, null);
}
public static void renameVersion(Profile profile, String version) {
Controllers.inputDialog(Launcher.i18n("version.manage.rename.message"), res -> {
if (profile.getRepository().renameVersion(version, res)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
}
});
}
public static void exportVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), Launcher.i18n("modpack.wizard"));
}
}
@@ -0,0 +1,58 @@
/*
* 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 javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import java.util.LinkedList;
import java.util.List;
public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>();
public WeakListenerHolder() {
}
public WeakInvalidationListener weak(InvalidationListener listener) {
refs.add(listener);
return new WeakInvalidationListener(listener);
}
public <T> WeakChangeListener<T> weak(ChangeListener<T> listener) {
refs.add(listener);
return new WeakChangeListener<>(listener);
}
public <T> WeakListChangeListener<T> weak(ListChangeListener<T> listener) {
refs.add(listener);
return new WeakListChangeListener<>(listener);
}
public void add(Object obj) {
refs.add(obj);
}
public boolean remove(Object obj) {
return refs.remove(obj);
}
}
@@ -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"));
}
@@ -0,0 +1,78 @@
/*
* 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.account;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@Override
protected void invalidated() {
Account account = get();
if (account == null) {
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
setImage(new Image("/assets/img/craft_table.png"));
} else {
setTitle(account.getCharacter());
setSubtitle(accountSubtitle(account));
final int scaleRatio = 4;
Image defaultSkin = AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
setImage(AccountHelper.getHead(defaultSkin, scaleRatio));
if (account instanceof YggdrasilAccount) {
AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
setImage(AccountHelper.getHead(image, scaleRatio));
});
}
}
}
};
public AccountAdvancedListItem() {
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
public ObjectProperty<Account> accountProperty() {
return account;
}
private static String accountSubtitle(Account account) {
if (account instanceof OfflineAccount)
return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount)
return account.getUsername();
else
return "";
}
}
@@ -0,0 +1,57 @@
/*
* 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.account;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class AccountList extends ListPage<AccountListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage"));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
public AccountList() {
setItems(MappedObservableList.create(accounts, AccountListItem::new));
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public ListProperty<Account> accountsProperty() {
return accounts;
}
@Override
public void add() {
Controllers.dialog(new AddAccountPane());
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
}
@@ -0,0 +1,130 @@
/*
* 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.account;
import javafx.beans.property.*;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItem extends RadioButton {
private final Account account;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
public AccountListItem(Account account) {
this.account = account;
getStyleClass().clear();
setUserData(account);
StringBuilder subtitleString = new StringBuilder(Accounts.getAccountTypeName(account));
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
subtitleString.append(", ").append(i18n("account.injector.server")).append(": ").append(server.getName());
}
if (account instanceof OfflineAccount)
title.set(account.getCharacter());
else
title.set(account.getUsername() + " - " + account.getCharacter());
subtitle.set(subtitleString.toString());
final int scaleRatio = 4;
Image image = account instanceof YggdrasilAccount ?
AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio) :
AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
this.image.set(AccountHelper.getHead(image, scaleRatio));
}
@Override
protected Skin<?> createDefaultSkin() {
return new AccountListItemSkin(this);
}
public void refresh() {
if (account instanceof YggdrasilAccount) {
// progressBar.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
// progressBar.setVisible(false);
if (isDependentsSucceeded) {
final int scaleRatio = 4;
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
this.image.set(AccountHelper.getHead(image, scaleRatio));
}
}).start();
}
}
public void remove() {
Accounts.getAccounts().remove(account);
}
public Account getAccount() {
return account;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public StringProperty titleProperty() {
return title;
}
public String getSubtitle() {
return subtitle.get();
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public StringProperty subtitleProperty() {
return subtitle;
}
public Image getImage() {
return image.get();
}
public void setImage(Image image) {
this.image.set(image);
}
public ObjectProperty<Image> imageProperty() {
return image;
}
}
@@ -0,0 +1,91 @@
/*
* 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.account;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
TwoLineListItem item = new TwoLineListItem();
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
root.setCenter(center);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnRefresh = new JFXButton();
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRefresh, i18n("button.refresh")));
right.getChildren().add(btnRefresh);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRemove, i18n("button.delete")));
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
getChildren().setAll(root);
}
}
@@ -15,9 +15,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
@@ -28,6 +27,8 @@ 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.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import java.util.function.Consumer;
@@ -41,7 +42,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,9 +69,9 @@ 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(AddAccountPane.accountException((Exception) account));
}
@@ -83,10 +83,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,297 @@
/*
* 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.account;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
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.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
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.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Logging;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept;
@FXML private JFXButton btnAddServer;
@FXML private JFXButton btnManageServer;
@FXML private SpinnerPane acceptPane;
public AddAccountPane() {
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(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getAccountTypeName));
cboType.getSelectionModel().select(0);
btnAddServer.visibleProperty().bind(cboServers.visibleProperty());
btnManageServer.visibleProperty().bind(cboServers.visibleProperty());
cboServers.getItems().addListener(onInvalidating(this::checkIfNoServer));
checkIfNoServer();
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !( // consider the opposite situation: input is valid
txtUsername.validate() &&
// invisible means the field is not needed, neither should it be validated
(!txtPassword.isVisible() || txtPassword.validate()) &&
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
),
txtUsername.textProperty(),
txtPassword.textProperty(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
}
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
}
}
private void checkIfNoServer() {
if (cboServers.getItems().isEmpty())
cboServers.getStyleClass().setAll("jfx-combo-box-warning");
else
cboServers.getStyleClass().setAll("jfx-combo-box");
}
/**
* Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}.
*/
private Object getAuthAdditionalData() {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
if (factory == Accounts.FACTORY_AUTHLIB_INJECTOR) {
return requireNonNull(cboServers.getSelectionModel().getSelectedItem(), "selected server cannot be null");
}
return null;
}
@FXML
private void onCreationAccept() {
if (btnAccept.isDisabled())
return;
acceptPane.showSpinner();
lblCreationWarning.setText("");
setDisable(true);
String username = txtUsername.getText();
String password = txtPassword.getText();
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData))
.finalized(Schedulers.javafx(), variables -> {
Account account = variables.get("create_account");
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
// adding an already-added account
// instead of discarding the new account, we first remove the existing one then add the new one
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
// select the new account
Accounts.setSelectedAccount(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());
}
@FXML
private void onAddInjecterServer() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
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;
final int scaleRatio = 4;
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, scaleRatio);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = AccountHelper.getDefaultSkin(profile.getId(), scaleRatio);
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
portraitView.setImage(AccountHelper.getHead(image, scaleRatio));
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 ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} 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 if (exception instanceof AuthlibInjectorDownloadException) {
return i18n("account.failed.injector_download_failure");
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
}
@@ -0,0 +1,138 @@
/*
* 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.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
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.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
@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(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
}
@Override
public void onDialogShown() {
txtServerUrl.requestFocus();
}
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 = txtServerUrl.getText();
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.locateServer(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());
}
}
@@ -15,7 +15,7 @@
* 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;
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
@@ -23,19 +23,20 @@ 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 org.jackhuang.hmcl.ui.SVG;
import java.util.function.Consumer;
public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServerInfo info;
private final AuthlibInjectorServer server;
private final Label lblServerName = new Label();
private final Label lblServerIp = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServerInfo info, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.info = info;
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.server = server;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;");
@@ -54,11 +55,11 @@ public final class AuthlibInjectorServerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(info.getServerName());
lblServerIp.setText(info.getServerIp());
lblServerName.setText(server.getName());
lblServerIp.setText(server.getUrl());
}
public AuthlibInjectorServerInfo getInfo() {
return info;
public AuthlibInjectorServer getServer() {
return server;
}
}
@@ -0,0 +1,77 @@
/*
* 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.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title"));
@FXML private ScrollPane scrollPane;
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
return new AuthlibInjectorServerItem(server,
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
public String getTitle() {
return title.get();
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public void setTitle(String title) {
this.title.set(title);
}
}
@@ -21,8 +21,8 @@ import javafx.animation.KeyFrame;
import java.util.List;
@FunctionalInterface
public interface AnimationProducer {
void init(AnimationHandler handler);
List<KeyFrame> animate(AnimationHandler handler);
}
@@ -24,27 +24,60 @@ import javafx.util.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}, 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)),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().opacityProperty(), 0.0D, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1.0D, Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)))),
/**
* A zoom effect
*/
ZOOM_IN(c ->
ZOOM_IN(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 ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().scaleXProperty(), 4, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 4, Interpolator.EASE_BOTH),
@@ -52,11 +85,19 @@ public enum ContainerAnimations {
/**
* A zoom effect
*/
ZOOM_OUT(c ->
ZOOM_OUT(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 ->
(Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 0, Interpolator.EASE_BOTH),
@@ -64,7 +105,16 @@ 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.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, 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 +125,16 @@ 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.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth());
}, 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 +144,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();
}
@@ -42,7 +42,7 @@ public class AdvancedListBox extends ScrollPane {
}
public AdvancedListBox add(Node child) {
if (child instanceof Pane)
if (child instanceof Pane || child instanceof AdvancedListItem)
container.getChildren().add(child);
else {
StackPane pane = new StackPane();
@@ -0,0 +1,123 @@
/*
* 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.beans.property.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public AdvancedListItem() {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void setImage(Image image) {
this.image.set(image);
}
public Node getRightGraphic() {
return rightGraphic.get();
}
public ObjectProperty<Node> rightGraphicProperty() {
return rightGraphic;
}
public void setRightGraphic(Node rightGraphic) {
this.rightGraphic.set(rightGraphic);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public boolean isActionButtonVisible() {
return actionButtonVisible.get();
}
public BooleanProperty actionButtonVisibleProperty() {
return actionButtonVisible;
}
public void setActionButtonVisible(boolean actionButtonVisible) {
this.actionButtonVisible.set(actionButtonVisible);
}
public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
public final void setOnAction(EventHandler<ActionEvent> value) {
onActionProperty().set(value);
}
public final EventHandler<ActionEvent> getOnAction() {
return onActionProperty().get();
}
private ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") {
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
@Override
protected Skin<?> createDefaultSkin() {
return new AdvancedListItemSkin(this);
}
}
@@ -0,0 +1,108 @@
/*
* 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.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
public AdvancedListItemSkin(AdvancedListItem skinnable) {
super(skinnable);
StackPane stackPane = new StackPane();
RipplerContainer container = new RipplerContainer(stackPane);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
HBox left = new HBox();
left.setAlignment(Pos.CENTER);
left.setMouseTransparent(true);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.imageProperty().bind(skinnable.imageProperty());
imageViewContainer.getChildren().setAll(imageView);
VBox vbox = new VBox();
vbox.setAlignment(Pos.CENTER_LEFT);
vbox.setPadding(new Insets(0, 0, 0, 10));
Label title = new Label();
title.textProperty().bind(skinnable.titleProperty());
title.setMaxWidth(90);
title.setStyle("-fx-font-size: 15;");
title.setTextAlignment(TextAlignment.JUSTIFY);
vbox.getChildren().add(title);
Label subtitle = new Label();
subtitle.textProperty().bind(skinnable.subtitleProperty());
subtitle.setMaxWidth(90);
subtitle.setStyle("-fx-font-size: 10;");
subtitle.setTextAlignment(TextAlignment.JUSTIFY);
vbox.getChildren().add(subtitle);
FXUtils.onChangeAndOperate(skinnable.subtitleProperty(), subtitleString -> {
if (subtitleString == null) vbox.getChildren().setAll(title);
else vbox.getChildren().setAll(title, subtitle);
});
left.getChildren().setAll(imageViewContainer, vbox);
root.setLeft(left);
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.setMouseTransparent(true);
right.getStyleClass().setAll("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
right.getChildren().clear();
} else {
right.getChildren().setAll(newGraphic);
}
});
root.setRight(right);
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().setAll("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
getChildren().setAll(container);
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -17,21 +17,25 @@
*/
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.VBox;
@DefaultProperty("content")
public class ComponentList extends StackPane {
private final VBox vbox = new VBox();
public class ComponentList extends Control {
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle", "");
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
@@ -39,35 +43,9 @@ public class ComponentList extends StackPane {
public final ObservableList<Node> content = FXCollections.observableArrayList();
public ComponentList() {
getChildren().setAll(vbox);
content.addListener((ListChangeListener<? super Node>) change -> {
while (change.next()) {
for (int i = change.getFrom(); i < change.getTo(); ++i)
addChildren(change.getList().get(i));
}
});
getStyleClass().add("options-list");
}
public void addChildren(Node node) {
if (node instanceof ComponentList) {
node.getProperties().put("title", ((ComponentList) node).getTitle());
node.getProperties().put("subtitle", ((ComponentList) node).getSubtitle());
}
StackPane child = new StackPane();
child.getChildren().add(new ComponentListCell(node));
if (vbox.getChildren().isEmpty())
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();
}
@@ -115,4 +93,40 @@ public class ComponentList extends StackPane {
public ObservableList<Node> getContent() {
return content;
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new Skin(this);
}
protected static class Skin extends SkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private final ObservableList<Node> list;
private final ObjectBinding<Node> firstItem;
protected Skin(ComponentList control) {
super(control);
list = MappedObservableList.create(control.getContent(), node -> {
ComponentListCell cell = new ComponentListCell(node);
cell.getStyleClass().add("options-list-item");
return cell;
});
firstItem = Bindings.valueAt(list, 0);
firstItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_FIRST, false);
});
if (!list.isEmpty())
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
VBox vbox = new VBox();
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
}
}
}
@@ -24,9 +24,12 @@ import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
@@ -38,13 +41,13 @@ import org.jackhuang.hmcl.ui.SVG;
/**
* @author huangyuhui
*/
public class ComponentListCell extends StackPane {
class ComponentListCell extends StackPane {
private final Node content;
private Animation expandAnimation;
private Rectangle clipRect;
private final BooleanProperty expanded = new SimpleBooleanProperty(this, "expanded", false);
public ComponentListCell(Node content) {
ComponentListCell(Node content) {
this.content = content;
updateLayout();
@@ -75,44 +78,51 @@ public class ComponentListCell extends StackPane {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
StackPane groupNode = new StackPane();
groupNode.getStyleClass().add("options-list-item-header");
BorderPane groupNode = new BorderPane();
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
StackPane.setAlignment(expandButton, Pos.CENTER_RIGHT);
VBox labelVBox = new VBox();
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
labelVBox.setAlignment(Pos.CENTER_LEFT);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
if (list instanceof ComponentSublist) {
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
if (leftNode != null)
labelVBox.getChildren().setAll(leftNode);
} else {
Label label = new Label();
label.textProperty().bind(list.titleProperty());
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
}
}
StackPane.setAlignment(labelVBox, Pos.CENTER_LEFT);
groupNode.getChildren().setAll(labelVBox, expandButton);
groupNode.setLeft(labelVBox);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
if (list instanceof ComponentSublist) {
Node rightNode = ((ComponentSublist) list).getHeaderRight();
if (rightNode != null)
right.getChildren().add(rightNode);
}
right.getChildren().add(expandButton);
groupNode.setRight(right);
VBox container = new VBox();
container.setStyle("-fx-padding: 8 0 0 0;");
container.setPadding(new Insets(8, 0, 0, 0));
FXUtils.setLimitHeight(container, 0);
Rectangle clipRect = new Rectangle();
clipRect.widthProperty().bind(container.widthProperty());
clipRect.heightProperty().bind(container.heightProperty());
container.setClip(clipRect);
FXUtils.setOverflowHidden(container, true);
container.getChildren().setAll(content);
VBox holder = new VBox();
holder.getChildren().setAll(groupNode, container);
holder.getStyleClass().add("options-list-item-container");
groupNode.setBottom(container);
expandButton.setOnMouseClicked(e -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
@@ -144,7 +154,7 @@ public class ComponentListCell extends StackPane {
expandedProperty().addListener((a, b, newValue) ->
expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(holder);
getChildren().setAll(groupNode);
} else
getChildren().setAll(content);
}
@@ -0,0 +1,41 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
@DefaultProperty("content")
public final class ComponentSublist extends ComponentList {
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
public ComponentSublist() {
super();
}
public Node getHeaderLeft() {
return headerLeft.get();
}
public ObjectProperty<Node> headerLeftProperty() {
return headerLeft;
}
public void setHeaderLeft(Node headerLeft) {
this.headerLeft.set(headerLeft);
}
public Node getHeaderRight() {
return headerRight.get();
}
public ObjectProperty<Node> headerRightProperty() {
return headerRight;
}
public void setHeaderRight(Node headerRight) {
this.headerRight.set(headerRight);
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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
@@ -15,11 +15,17 @@
* 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;
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.ui.Controllers;
/**
* throws if wrong password or logging fails for too many times.
* @author huangyuhui
* @author yushijinhun
* @see Controllers#dialog(javafx.scene.layout.Region)
*/
public final class InvalidCredentialsException extends AuthenticationException {
public interface DialogAware {
default void onDialogShown() {
}
}
@@ -15,26 +15,30 @@
* 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.game;
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.download.DefaultGameBuilder;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.ui.Controllers;
/**
* @author huangyuhui
* Indicates a close operation on the dialog.
*
* @author yushijinhun
* @see Controllers#dialog(Region)
*/
public class HMCLGameBuilder extends DefaultGameBuilder {
private final Profile profile;
public class DialogCloseEvent extends Event {
public HMCLGameBuilder(Profile profile) {
super(profile.getDependency());
public static final EventType<DialogCloseEvent> CLOSE = new EventType<>("DIALOG_CLOSE");
this.profile = profile;
public DialogCloseEvent() {
super(CLOSE);
}
@Override
protected Task downloadGameAsync(String gameVersion, Version version) {
return new HMCLGameDownloadTask(profile, gameVersion, version);
public DialogCloseEvent(Object source, EventTarget target) {
super(source, target, CLOSE);
}
}
@@ -18,7 +18,8 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.Property;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
@@ -26,46 +27,69 @@ import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FileItem extends BorderPane {
private Property<String> property;
private final Label x = new Label();
private final Label lblPath = new Label();
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
public FileItem() {
VBox left = new VBox();
Label name = new Label();
name.textProperty().bind(nameProperty());
x.getStyleClass().addAll("subtitle-label");
left.getChildren().addAll(name, x);
lblPath.getStyleClass().addAll("subtitle-label");
lblPath.textProperty().bind(path);
left.getChildren().addAll(name, lblPath);
setLeft(left);
JFXButton right = new JFXButton();
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
FXUtils.installTooltip(right, Launcher.i18n("button.edit"));
FXUtils.installTooltip(right, i18n("button.edit"));
setRight(right);
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
}
/**
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
*/
private String processPath(String path) {
Path given = Paths.get(path).toAbsolutePath();
if (isConvertToRelativePath()) {
try {
return Paths.get(".").normalize().toAbsolutePath().relativize(given).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return given.normalize().toString();
}
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
if (property.getValue() != null) {
File file = new File(property.getValue());
if (path.get() != null) {
File file = new File(path.get());
if (file.exists()) {
if (file.isFile())
file = file.getAbsoluteFile().getParentFile();
@@ -76,16 +100,12 @@ public class FileItem extends BorderPane {
}
chooser.titleProperty().bind(titleProperty());
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
property.setValue(selectedDir.getAbsolutePath());
if (selectedDir != null) {
path.set(processPath(selectedDir.toString()));
}
chooser.titleProperty().unbind();
}
public void setProperty(Property<String> property) {
this.property = property;
x.textProperty().bind(property);
}
public String getName() {
return name.get();
}
@@ -121,4 +141,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);
}
}
@@ -17,22 +17,26 @@
*/
package org.jackhuang.hmcl.ui.construct;
import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import com.jfoenix.controls.JFXComboBox;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ListCell;
import javafx.scene.text.Font;
public class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false;
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize,
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
super(FXCollections.observableArrayList(Font.getFamilies()));
valueProperty().addListener((a, b, newValue) -> {
if (enableStyle)
setStyle("-fx-font-family: \"" + newValue + "\";");
});
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new ListCell<String>() {
@Override
@@ -44,5 +48,16 @@ public class FontComboBox extends JFXComboBox<String> {
}
}
});
itemsProperty().bind(MultiStepBinding.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
if (loaded)
return;
itemsProperty().unbind();
setItems(observableList(Font.getFamilies()));
loaded = true;
});
}
}
@@ -21,32 +21,39 @@ 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 Label label;
public IconedItem(Node icon, String text) {
super(createHBox(icon, text));
this(icon);
label.setText(text);
}
private static HBox createHBox(Node icon, String text) {
public IconedItem(Node icon) {
super(createHBox(icon));
label = ((Label) lookup("#label"));
getStyleClass().setAll("iconed-item");
}
private static HBox createHBox(Node icon) {
HBox hBox = new HBox();
icon.setMouseTransparent(true);
Label textLabel = new Label(text);
if (icon != null) {
icon.setMouseTransparent(true);
hBox.getChildren().add(icon);
}
hBox.getStyleClass().setAll("iconed-item-container");
Label textLabel = new Label();
textLabel.setId("label");
textLabel.setAlignment(Pos.CENTER);
textLabel.setMouseTransparent(true);
hBox.getChildren().addAll(icon, textLabel);
hBox.setStyle("-fx-padding: 10 16 10 16; -fx-spacing: 10; -fx-font-size: 14;");
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(textLabel);
return hBox;
}
public void setText(String text) {
((Label) lookup("#label")).setText(text);
}
public void setTextFill(Paint paint) {
((Label) lookup("#label")).setTextFill(paint);
public Label getLabel() {
return label;
}
}
@@ -0,0 +1,30 @@
/*
* 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;
public class IconedMenuItem extends IconedItem {
public IconedMenuItem(Node node, String text, Runnable action) {
super(node, text);
getStyleClass().setAll("iconed-menu-item");
setOnMouseClicked(e -> action.run());
}
}
@@ -15,20 +15,20 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@DefaultProperty("image")
public final class ImagePickerItem extends BorderPane {
private final ImageView imageView;
private final JFXButton selectButton;
private final Label label;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
private final ObjectProperty<EventHandler<? super MouseEvent>> onDeleteButtonClicked = new SimpleObjectProperty<>(this, "onDeleteButtonClicked");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
public ImagePickerItem() {
@@ -36,21 +36,26 @@ public final class ImagePickerItem extends BorderPane {
imageView.setSmooth(false);
imageView.setPreserveRatio(true);
selectButton = new JFXButton();
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, Launcher.i18n("button.edit"));
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, i18n("button.edit"));
HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton);
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setSpacing(8);
setRight(hBox);
VBox vBox = new VBox();
label = new Label();
Label label = new Label();
label.textProperty().bind(title);
vBox.getChildren().setAll(label);
vBox.setAlignment(Pos.CENTER_LEFT);
@@ -83,6 +88,18 @@ public final class ImagePickerItem extends BorderPane {
this.onSelectButtonClicked.set(onSelectButtonClicked);
}
public EventHandler<? super MouseEvent> getOnDeleteButtonClicked() {
return onDeleteButtonClicked.get();
}
public ObjectProperty<EventHandler<? super MouseEvent>> onDeleteButtonClickedProperty() {
return onDeleteButtonClicked;
}
public void setOnDeleteButtonClicked(EventHandler<? super MouseEvent> onDeleteButtonClicked) {
this.onDeleteButtonClicked.set(onDeleteButtonClicked);
}
public Image getImage() {
return image.get();
}
@@ -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);
}
}
@@ -0,0 +1,19 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class MenuSeparator extends StackPane {
public MenuSeparator() {
Rectangle rect = new Rectangle();
rect.widthProperty().bind(widthProperty().add(-14));
rect.setHeight(1);
rect.setFill(Color.GRAY);
maxHeightProperty().set(10);
setPadding(new Insets(3));
getChildren().setAll(rect);
}
}
@@ -20,16 +20,18 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import javax.swing.*;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageBox {
private MessageBox() {
}
private static final String TITLE = Launcher.i18n("message.info");
private static final String TITLE = i18n("message.info");
/**
* User Operation: Yes
@@ -123,30 +125,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,20 +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.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
private boolean closingDialog = true;
@FXML
private JFXButton acceptButton;
@@ -46,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)
@@ -54,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);
});
@@ -82,23 +80,18 @@ public final class MessageDialogPane extends StackPane {
}
}
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(Launcher.i18n("button.yes"));
cancelButton.setText(Launcher.i18n("button.no"));
acceptButton.setText(i18n("button.yes"));
cancelButton.setText(i18n("button.no"));
actions.getChildren().add(cancelButton);
}
public void disableClosingDialog() {
closingDialog = false;
}
}
@@ -13,16 +13,17 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiColorItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Launcher.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Launcher.i18n("selector.choose_file"));
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final ToggleGroup group = new ToggleGroup();
private final JFXColorPicker colorPicker = new JFXColorPicker();
@@ -56,7 +57,7 @@ public class MultiColorItem extends ComponentList {
if (hasCustom)
pane.getChildren().add(custom);
addChildren(pane);
getContent().add(pane);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
@@ -21,24 +21,20 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.NamedArg;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.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.Launcher;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -46,13 +42,17 @@ import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
public class MultiFileItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Launcher.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Launcher.i18n("selector.choose_file"));
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiFileItem<T> extends ComponentList {
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 ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
@@ -65,6 +65,7 @@ public class MultiFileItem extends ComponentList {
private Consumer<Toggle> toggleSelectedListener;
@SuppressWarnings("unchecked")
public MultiFileItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
this.hasCustom = hasCustom;
@@ -106,29 +107,39 @@ public class MultiFileItem extends ComponentList {
if (hasCustom)
pane.getChildren().add(custom);
addChildren(pane);
getContent().add(pane);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
});
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
selectedData.set((T) newValue.getUserData());
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == newValue)
.findFirst();
if (!selecting.isPresent()) {
selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == getFallbackData())
.findFirst();
}
selecting.ifPresent(toggle -> toggle.setSelected(true));
});
}
public Node createChildren(String title) {
return createChildren(title, null);
}
public Node createChildren(String title, Object userData) {
public Node createChildren(String title, T userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, Object userData) {
public Node createChildren(String title, String subtitle, T userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 20);
JFXRadioButton left = new JFXRadioButton(title);
@@ -151,12 +162,9 @@ public class MultiFileItem extends ComponentList {
pane.getChildren().add(custom);
}
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(Launcher.i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
public void loadChildren(Collection<Node> list, T customUserData) {
loadChildren(list);
setCustomUserData(customUserData);
}
public ToggleGroup getGroup() {
@@ -187,7 +195,7 @@ public class MultiFileItem extends ComponentList {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(Object userData) {
public void setCustomUserData(T userData) {
radioCustom.setUserData(userData);
}
@@ -231,15 +239,27 @@ public class MultiFileItem extends ComponentList {
return extensionFilters;
}
public String getTooltip() {
return tooltip.get();
public T getSelectedData() {
return selectedData.get();
}
public StringProperty tooltipProperty() {
return tooltip;
public ObjectProperty<T> selectedDataProperty() {
return selectedData;
}
public void setTooltip(String tooltip) {
this.tooltip.set(tooltip);
public void setSelectedData(T selectedData) {
this.selectedData.set(selectedData);
}
public T getFallbackData() {
return fallbackData.get();
}
public ObjectProperty<T> fallbackDataProperty() {
return fallbackData;
}
public void setFallbackData(T fallbackData) {
this.fallbackData.set(fallbackData);
}
}
@@ -0,0 +1,184 @@
/*
* 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.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
import java.util.Stack;
public class Navigator extends StackPane {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
private final Stack<Node> stack = new Stack<>();
private final TransitionHandler animationHandler = new TransitionHandler(this);
private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper();
public Navigator(Node init) {
stack.push(init);
getChildren().setAll(init);
Platform.runLater(() ->
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED)));
}
public void navigate(Node node) {
FXUtils.checkFxUserThread();
Node from = stack.peek();
if (from == node)
return;
Logging.LOG.info("Navigate to " + node);
stack.push(node);
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
node.fireEvent(navigated);
EventHandler<PageCloseEvent> handler = event -> close(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(PageCloseEvent.CLOSE, handler);
}
public void close() {
close(stack.peek());
}
public void clear() {
while (stack.size() > 1)
close(stack.peek());
}
@SuppressWarnings("unchecked")
public void close(Node from) {
FXUtils.checkFxUserThread();
Logging.LOG.info("Closed page " + from);
if (stack.peek() != from)
throw new IllegalStateException();
stack.pop();
Node node = stack.peek();
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
node.fireEvent(navigated);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> from.removeEventHandler(PageCloseEvent.CLOSE, (EventHandler<PageCloseEvent>) handler));
}
public Node getCurrentPage() {
return stack.peek();
}
public boolean canGoBack() {
return stack.size() > 1;
}
private void setContent(Node content) {
animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer());
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content, true);
}
}
public EventHandler<NavigationEvent> getOnNavigated() {
return onNavigated.get();
}
public ObjectProperty<EventHandler<NavigationEvent>> onNavigatedProperty() {
return onNavigated;
}
public void setOnNavigated(EventHandler<NavigationEvent> onNavigated) {
this.onNavigated.set(onNavigated);
}
private ObjectProperty<EventHandler<NavigationEvent>> onNavigated = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigated") {
@Override
protected void invalidated() {
setEventHandler(NavigationEvent.NAVIGATED, get());
}
};
public EventHandler<NavigationEvent> getOnNavigating() {
return onNavigating.get();
}
public ObjectProperty<EventHandler<NavigationEvent>> onNavigatingProperty() {
return onNavigating;
}
public void setOnNavigating(EventHandler<NavigationEvent> onNavigating) {
this.onNavigating.set(onNavigating);
}
private ObjectProperty<EventHandler<NavigationEvent>> onNavigating = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigating") {
@Override
protected void invalidated() {
setEventHandler(NavigationEvent.NAVIGATING, get());
}
};
public static class NavigationEvent extends Event {
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
private final Node node;
public NavigationEvent(Object source, Node target, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.node = target;
}
public Node getNode() {
return node;
}
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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
@@ -15,24 +15,27 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.Property;
import java.util.concurrent.atomic.AtomicReference;
import javafx.event.Event;
import javafx.event.EventTarget;
import javafx.event.EventType;
/**
* Indicates a close operation on the navigator page.
*
* @author huangyuhui
*/
public final class Properties {
public class PageCloseEvent extends Event {
private Properties() {
public static final EventType<PageCloseEvent> CLOSE = new EventType<>("PAGE_CLOSE");
public PageCloseEvent() {
super(CLOSE);
}
public static <T> void updateAsync(Property<? super T> property, T newValue, AtomicReference<T> update) {
if (update.getAndSet(newValue) == null)
Constants.UI_THREAD_SCHEDULER.accept(() ->
property.setValue(update.getAndSet(null)));
public PageCloseEvent(Object source, EventTarget target) {
super(source, target, CLOSE);
}
}
@@ -0,0 +1,94 @@
/*
* 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.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class PopupMenu extends Control {
private final ObservableList<Node> content = FXCollections.observableArrayList();
private final BooleanProperty alwaysShowingVBar = new SimpleBooleanProperty();
public PopupMenu() {
}
public ObservableList<Node> getContent() {
return content;
}
public boolean isAlwaysShowingVBar() {
return alwaysShowingVBar.get();
}
public BooleanProperty alwaysShowingVBarProperty() {
return alwaysShowingVBar;
}
public void setAlwaysShowingVBar(boolean alwaysShowingVBar) {
this.alwaysShowingVBar.set(alwaysShowingVBar);
}
@Override
protected Skin<?> createDefaultSkin() {
return new PopupMenuSkin();
}
public static Node wrapPopupMenuItem(Node node) {
StackPane pane = new StackPane();
pane.getChildren().setAll(node);
pane.getStyleClass().setAll("menu-container");
node.setMouseTransparent(true);
return new RipplerContainer(pane);
}
private class PopupMenuSkin extends SkinBase<PopupMenu> {
protected PopupMenuSkin() {
super(PopupMenu.this);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.vbarPolicyProperty().bind(new When(alwaysShowingVBar)
.then(ScrollPane.ScrollBarPolicy.ALWAYS)
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
VBox content = new VBox();
content.getStyleClass().add("menu");
Bindings.bindContent(content.getChildren(), PopupMenu.this.getContent());
scrollPane.setContent(content);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
}
}
@@ -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();
}
}
@@ -20,18 +20,21 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Bindings;
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;
@@ -40,19 +43,26 @@ public class TaskExecutorDialogPane extends StackPane {
@FXML
private Label lblSubtitle;
@FXML
private Label lblProgress;
@FXML
private JFXButton btnCancel;
@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);
});
lblProgress.textProperty().bind(Bindings.createStringBinding(
() -> taskListPane.finishedTasksProperty().get() + "/" + taskListPane.totTasksProperty().get(),
taskListPane.finishedTasksProperty(), taskListPane.totTasksProperty()
));
}
public void setExecutor(TaskExecutor executor) {
@@ -93,7 +103,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,14 +19,14 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameAssetRefreshTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
@@ -39,9 +39,13 @@ import org.jackhuang.hmcl.task.TaskListener;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private final AdvancedListBox listBox = new AdvancedListBox();
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
public TaskListPane() {
listBox.setSpacing(0);
@@ -49,11 +53,28 @@ public final class TaskListPane extends StackPane {
getChildren().setAll(listBox);
}
public ReadOnlyIntegerProperty finishedTasksProperty() {
return finishedTasks.getReadOnlyProperty();
}
public ReadOnlyIntegerProperty totTasksProperty() {
return totTasks.getReadOnlyProperty();
}
public void setExecutor(TaskExecutor executor) {
executor.addTaskListener(new TaskListener() {
@Override
public void onStart() {
Platform.runLater(listBox::clear);
Platform.runLater(() -> {
listBox.clear();
finishedTasks.set(0);
totTasks.set(0);
});
}
@Override
public void onReady(Task task) {
Platform.runLater(() -> totTasks.set(totTasks.getValue() + 1));
}
@Override
@@ -61,30 +82,28 @@ public final class TaskListPane extends StackPane {
if (!task.getSignificance().shouldShow())
return;
if (task instanceof GameAssetRefreshTask) {
task.setName(Launcher.i18n("assets.download"));
} else if (task instanceof GameAssetDownloadTask) {
task.setName(Launcher.i18n("assets.download_all"));
if (task instanceof GameAssetDownloadTask) {
task.setName(i18n("assets.download_all"));
} else if (task instanceof ForgeInstallTask) {
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.forge")));
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.liteloader")));
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
} else if (task instanceof OptiFineInstallTask) {
task.setName(Launcher.i18n("install.installer.install", Launcher.i18n("install.installer.optifine")));
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
} else if (task instanceof CurseCompletionTask) {
task.setName(Launcher.i18n("modpack.type.curse.completion"));
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
task.setName(Launcher.i18n("modpack.installing"));
task.setName(i18n("modpack.installing"));
} else if (task instanceof CurseInstallTask) {
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.curse")));
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.multimc")));
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(Launcher.i18n("modpack.install", Launcher.i18n("modpack.type.hmcl")));
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof HMCLModpackExportTask) {
task.setName(Launcher.i18n("modpack.export"));
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(Launcher.i18n("modpack.scan"));
task.setName(i18n("modpack.scan"));
}
ProgressListNode node = new ProgressListNode(task);
@@ -99,7 +118,10 @@ public final class TaskListPane extends StackPane {
if (node == null)
return;
node.unbind();
Platform.runLater(() -> listBox.remove(node));
Platform.runLater(() -> {
listBox.remove(node);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
@Override
@@ -107,7 +129,10 @@ public final class TaskListPane extends StackPane {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
Platform.runLater(() -> node.setThrowable(throwable));
Platform.runLater(() -> {
node.setThrowable(throwable);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
});
}
Loaded 100 of 433 files, more files were not shown because too many files have changed in this diff. Show more