Compare commits

..
Author SHA1 Message Date
huanghongxun 9706a799be feat: custom zipfilesystem. 2021-10-24 17:21:47 +08:00
huanghongxun ec37b32eaa feat(microsoft): copy device code instead of verification uri. 2021-10-23 23:28:56 +08:00
huanghongxun f893eeea1f fix: not closing launching progress pane when java downloading was cancelled. Closes #1134. 2021-10-23 23:28:55 +08:00
huanghongxun 8d559dba02 feat(multiplayer): verify checksum. Closes #1122. 2021-10-23 23:28:55 +08:00
Glavo d167c712a6 Improved Java path display when no suitable Java is found 2021-10-23 22:06:07 +08:00
Glavo 8b224cb240 Avoid throwing NPE when the Java version cannot be obtained 2021-10-23 22:06:07 +08:00
huanghongxun 651aedaa50 feat(microsoft): use device code to login. 2021-10-23 02:22:04 +08:00
huanghongxun a36b0ebed2 feat: WIP: use commons-compress 2021-10-23 01:47:24 +08:00
huanghongxun 5a9e8683e4 feat(microsoft): WIP: use device code grant flow instead. 2021-10-23 00:01:55 +08:00
huanghongxun 843b29eff0 feat(multiplayer): update cato to 1.1.1 2021-10-22 22:03:08 +08:00
huanghongxun 7bad0be2c4 fix: incorrect system architecture detection in macOS. 2021-10-22 22:02:28 +08:00
huanghongxun 5561011122 fix: cannot choose custom java path. 2021-10-22 22:02:27 +08:00
huanghongxun 192a0c987e fix: unable to rename game versions that do not have jar file in directory. 2021-10-22 22:02:27 +08:00
huanghongxun 85a9a21e57 fix: unable to duplicate game version. 2021-10-22 22:02:27 +08:00
huanghongxun 610864d674 feat(launch): compatible with TLauncher. 2021-10-22 22:02:27 +08:00
Glavo 06d0fa6e22 Pass Launcher.class to Application.launch 2021-10-22 14:16:25 +08:00
Glavo fb9a6fd314 Lazy initialization CrashReporter 2021-10-22 14:16:25 +08:00
Glavo 5cab8ad41e Lazy initialization JFX_DEPENDENCIES 2021-10-22 14:16:25 +08:00
Glavo d9f2b964c9 Close letsEncryptFile (#1127) 2021-10-21 23:59:01 +08:00
huanghongxun ac78b69ec2 fix: Update Lets Encrypt root certificate. 2021-10-21 22:43:03 +08:00
Glavo 516975688f 在发现文件系统实现出错时提示用户 (#1040)
* fix: The implementation of 'Files.isWritable' is incorrect

* Report Samba problems to users

* Rollback FileUtils
2021-10-21 22:05:46 +08:00
Glavo 7f07439388 Limit check updates 2021-10-21 21:37:33 +08:00
Glavo bf87c4ac52 Skip generate release note when not necessary 2021-10-21 21:37:33 +08:00
Glavo 272132af51 GitHub Action: Check Update 2021-10-21 21:37:33 +08:00
zhixuan 04dd179631 Update I18N_ja.properties 2021-10-21 21:37:16 +08:00
huanghongxun 57554c8d4b fix(ui): validate background image url. Mentioned in #1116. 2021-10-20 15:49:16 +08:00
huanghongxun 74a20bf11d feat(crash): find module name in stacktrace if available. 2021-10-20 02:17:58 +08:00
huanghongxun 16c96419d3 feat(crash): TwilightForest may be incompatible with OptiFine on Minecraft 1.16.5. 2021-10-20 02:09:50 +08:00
huanghongxun 0b444bc5a2 feat(ui): support translucent background.
Fix no background when network image url is wrong. Closes #1116.
2021-10-20 01:24:12 +08:00
huanghongxun 7846e9e196 fix: checkstyle. 2021-10-20 01:15:22 +08:00
huanghongxun a74b971b98 fix(auto-installing): verify fabric api file. 2021-10-20 01:15:21 +08:00
huanghongxun 6707ce1ed9 feat(crash): read crash report from raw log as fallback. 2021-10-20 01:15:21 +08:00
huanghongxun de421f93b5 feat(multiplayer): much simpler invitation code format. 2021-10-20 01:15:20 +08:00
huanghongxun 1762e78cb3 fix(java): java7 suggestion warning. 2021-10-19 23:17:27 +08:00
huanghongxun 2631f3a007 fix(java): test forge installation instead of main class. 2021-10-19 23:11:13 +08:00
huanghongxun f70ec8a090 fix(launch): cannot launch far old versions on macOS. 2021-10-19 23:07:00 +08:00
huanghongxun a4f22671c6 feat(java): download java 8/16 when auto selected java not found. 2021-10-18 23:38:57 +08:00
huanghongxun 71c23df971 feat: use ArrayUtils from commons-lang3 instead. 2021-10-18 15:33:44 +08:00
huanghongxun 1bb2593c5b feat(crash): more detailed message when cause of game crash cannot be found out. 2021-10-18 15:33:19 +08:00
huanghongxun 45d5ef4602 build: commons-lang3 & commons-compress. 2021-10-18 03:28:09 +08:00
huanghongxun 7b5b2596b9 feat(multiplayer): WIP: update cato 1.1.0 2021-10-18 03:03:21 +08:00
huanghongxun 23a805d576 build: proguard works without multi-release. 2021-10-18 03:02:57 +08:00
huanghongxun f8cf3c22a1 fix(lang): incorrect hyperlinks marks. 2021-10-18 03:02:21 +08:00
Glavo 8e5725946c Copy manifest into final jar 2021-10-18 01:41:00 +08:00
Glavo 99bb0d7a93 final jar 2021-10-18 01:41:00 +08:00
huanghongxun 9695ddf8de build: WIP: make proguard work. 2021-10-17 16:29:24 +08:00
huanghongxun 50cff0bcba feat(lang): ja_JP. 2021-10-17 16:29:24 +08:00
zhixuan 1832cf5924 . 2021-10-17 16:26:20 +08:00
zhixuan 10fba0a2b2 Add Google translate 2021-10-17 16:26:20 +08:00
zhixuan e2050fd47a . 2021-10-17 16:26:20 +08:00
zhixuan f72da5973f first commit 2021-10-17 16:26:20 +08:00
huanghongxun 0ebeffac32 feat: improve error message for AccessDeniedException. Mentioned in #1114. 2021-10-17 15:13:42 +08:00
huanghongxun dcfe724803 fix: incorrect position of title when drag in a modpack file. 2021-10-17 03:24:05 +08:00
huanghongxun 821020e7b6 fix: typo. 2021-10-17 00:54:27 +08:00
huanghongxun 33efc38f4c feat(crash): more detailed prompt for graphics card issue. Closes #1102. 2021-10-17 00:49:52 +08:00
huanghongxun f01c0408e1 fix: misused skin texture as cape texture. Closes #1106. 2021-10-17 00:32:48 +08:00
huanghongxun 365ae0ef8c fix: FlowPane does not accept max-width setting. Closes #1109. 2021-10-17 00:32:48 +08:00
huanghongxun a6e593e3a1 feat(multiplayer): timeout for cato main net connection. 2021-10-17 00:32:47 +08:00
huanghongxun 67d38d6333 fix: DST ROOT CA X3 has already expired. Closes #1100. 2021-10-17 00:32:46 +08:00
Glavo 513d101fc4 fix github action warnings 2021-10-17 00:22:14 +08:00
Glavo 5e1304f699 Add short SHA to the name of github action artifact 2021-10-16 17:48:18 +08:00
Glavo 955a06d689 Optimize invitation code generation 2021-10-16 17:47:43 +08:00
Glavo 59e1a470e3 Log classpath in log window 2021-10-16 00:01:17 +08:00
Glavo 16952f5695 Update script format 2021-10-16 00:01:17 +08:00
Glavo 67feca075b Allow users to export PowerShell scripts 2021-10-16 00:01:17 +08:00
Glavo 12368375cf Pass classpath by environment variable 2021-10-16 00:01:17 +08:00
Glavo 284ffb7aa0 Clearly specify the in all places 2021-10-14 23:09:06 +08:00
Glavo 1aefb26575 Extract duplicate code 2021-10-14 22:27:34 +08:00
Glavo 670d0f8462 Remove unnecessary code 2021-10-14 22:27:34 +08:00
Glavo 3bbe422421 When using Java with different architectures, display the Java architecture in the crash window 2021-10-14 22:27:34 +08:00
Glavo d68e988c15 Relax timeout restrictions 2021-10-14 22:27:34 +08:00
Glavo 74fd35047c Use 'PROCESSOR_IDENTIFIER' to determine the architecture on Windows 2021-10-14 22:27:34 +08:00
Glavo 21a9d19105 Allow use cato on Windows on ARM 2021-10-14 22:27:34 +08:00
Glavo f662e983a4 Improve 'Architecture' 2021-10-14 22:27:34 +08:00
Glavo 7db379492a Log architecture information 2021-10-14 22:27:34 +08:00
Glavo 4a82daccec fix: checkstyle 2021-10-14 22:27:34 +08:00
Glavo 21e44bbc90 Do not automatically select Java non for x86 and x86-64 2021-10-14 22:27:34 +08:00
Glavo 42a40a844a Minor improvements 2021-10-14 22:27:34 +08:00
Glavo cd7c515097 Accurately obtain the Windows version number; Report error when users try to run minecraft using java for other platforms 2021-10-14 22:27:34 +08:00
Glavo 9595626a27 Enhance JavaVersionConstraint 2021-10-14 22:27:34 +08:00
Glavo db105c90b5 Update the link of AdoptOpenJDK 2021-10-14 22:27:34 +08:00
Glavo 74a305c32a Download Java on MacOS aarch64 2021-10-14 22:27:34 +08:00
Glavo 3af115ce37 Use separate native folder for each platform 2021-10-14 22:27:34 +08:00
Glavo b4e7934396 Improve display Java version 2021-10-14 22:27:34 +08:00
Glavo 0a54037460 Improve 'Architecture' and 'JavaVersion' 2021-10-14 22:27:34 +08:00
Glavo dd8da57cfb New 'Platform' class 2021-10-14 22:27:34 +08:00
Glavo 9b6f2d00ca Rename 'Platform' to 'Bits' 2021-10-14 22:27:34 +08:00
huanghongxun 2852821d2c fix(mod): does not translate mod with id "examplemod" 2021-10-13 21:20:08 +08:00
huanghongxun 673bc75cd5 fix(javafx): should download javafx at beginning, not let user choose download source first. 2021-10-13 13:58:33 +08:00
Glavo 3fad76aae1 自动开始下载 JavaFX (#1063)
* Automatically start downloading JavaFX

* Reimplement ProgressFrame with JDialog
2021-10-13 13:54:24 +08:00
huanghongxun 2cb70a41fb fix(multiplayer): forget to respond handshake. 2021-10-13 13:48:47 +08:00
huanghongxun ef4f2ed791 feat(ui): translations for AccessDeniedException. 2021-10-13 13:47:59 +08:00
huanghongxun c9a73a366d fix: test. 2021-10-13 02:39:25 +08:00
huanghongxun 7973179ddf feat(crash): ModResolutionException (Could not resolve valid mod collection) 2021-10-13 02:32:56 +08:00
huanghongxun 6b656bdc48 chore(crash): some comment. 2021-10-12 23:56:34 +08:00
huanghongxun 428c1a1799 feat(java): WIP: show expected java version range when no valid java. 2021-10-12 23:49:06 +08:00
huanghongxun 8892e2b273 feat(crash): fabric 0.12 may be incompatible with mods. 2021-10-12 23:39:12 +08:00
huanghongxun 2e8e6dd39e fix: mess tag. Closes #1090. 2021-10-12 23:16:01 +08:00
huanghongxun 3ac16b15c6 fix(multiplayer): should not show unsupported platform when running on 32-bit Java, but 64-bit OS. 2021-10-12 23:06:13 +08:00
huanghongxun ec3dfb908c fix(auth): OAuth login dialog logging state. 2021-10-12 22:53:35 +08:00
huanghongxun b5484bb13b fix(system): we may get x86_64 when running 32-bit JVM in 64-bit Windows. 2021-10-12 22:53:09 +08:00
huanghongxun c5a43efafb fix: checkstyle. 2021-10-12 22:06:25 +08:00
huanghongxun 23c2c0689c feat(account): OAuth account relogin. 2021-10-12 22:00:41 +08:00
huanghongxun 948d64237e fix(multiplayer): show friendly message when cato file not found. 2021-10-12 21:14:14 +08:00
huanghongxun 3433e88bb8 feat(microsoft): WIP: handle credentials expiration. 2021-10-12 14:04:18 +08:00
huanghongxun 28278c091d feat(multiplayer): more logs. 2021-10-12 14:03:46 +08:00
huanghongxun 04a68d0b95 feat(ui): show world download button in worldlistpage. 2021-10-12 14:03:27 +08:00
huanghongxun 9b0c0d1870 feat(download): resourcepack game version selection. 2021-10-10 22:32:59 +08:00
huanghongxun 86734d2f5f fix(ui): incorrect padding. 2021-10-10 22:24:09 +08:00
huanghongxun 7d5a73828d feat(download): category 2021-10-10 22:20:32 +08:00
huanghongxun 0fd8a78972 fix(multiplayer): show connection timeout instead of kicked. 2021-10-10 21:51:36 +08:00
huanghongxun fca78cf419 fix(crash): JVM_32BIT pattern. 2021-10-10 21:18:19 +08:00
huanghongxun 5f98c8c8bb feat(crash): detect memory exceeding. 2021-10-10 20:13:21 +08:00
huanghongxun dc4a791a9e fix(multiplayer): show anti-virus software warning if cato file not found. 2021-10-10 19:02:37 +08:00
huanghongxun d89c8cc2d2 fix(crash): not displaying memory and java version. 2021-10-10 17:12:49 +08:00
huanghongxun 9b24c683c3 feat: show bilibili url in dev/nightly version. 2021-10-10 16:45:31 +08:00
huanghongxun 2acea40204 fix: compile. 2021-10-10 02:48:12 +08:00
huanghongxun f014f59cf2 feat(update): update channel is bound to build type. 2021-10-10 02:27:20 +08:00
huanghongxun 6221f4b616 feat(multiplayer): auto rejection. 2021-10-10 02:02:44 +08:00
huanghongxun 32d557ee07 fix(skin): CustomSkin API should have suffix slash. 2021-10-10 01:09:41 +08:00
huanghongxun ba6eb17a38 fix: checkstyle. 2021-10-10 00:48:06 +08:00
huanghongxun 935436ce49 fix(ui): crash when duplicating game version. 2021-10-10 00:41:39 +08:00
huanghongxun 877c5a827c feat(multiplayer): join request should be cancelled when user operation time out. 2021-10-10 00:37:12 +08:00
huanghongxun eedcff785e feat(multiplayer): an option for auto allowing all join requests. Closes #1093. 2021-10-10 00:37:11 +08:00
huanghongxun 60865163db chore(ui): title gray background. 2021-10-09 20:44:19 +08:00
huanghongxun c32a14a959 chore(multiplayer): test NAT by fast method. 2021-10-09 20:39:08 +08:00
huanghongxun 3ddab57ab5 chore(multiplayer): NAT full cone should be evaluated as good. 2021-10-09 20:34:54 +08:00
huanghongxun 20957106df feat(skin): custom model. 2021-10-09 17:49:17 +08:00
huanghongxun f528f3bf40 feat(crash): UnsatisfiedLinkError. 2021-10-09 17:24:07 +08:00
huanghongxun 14fd0d484e feat(ui): show warnings for workaround options. 2021-10-09 17:17:28 +08:00
huanghongxun 6fc60f1387 fix(feedback): incorrect action when clicking blank space. Closes #1087. 2021-10-09 15:59:22 +08:00
huanghongxun 5ad46279c2 feat(multiplayer): cato 1.0.c supports darwin-aarch64. 2021-10-09 15:45:09 +08:00
huanghongxun f4894ccb9d feat(about): minecraft-jfx-skin. 2021-10-09 15:42:48 +08:00
huanghongxun 2b04292424 fix(multiplayer): does not download cato if not found. 2021-10-07 19:00:27 +08:00
huanghongxun 775b58d0fd fix(crash): crash when game crashed. 2021-10-07 18:50:11 +08:00
huanghongxun 6d09d0984e fix(ui): refine local modpack install page ui. Closes #1089. 2021-10-07 15:25:25 +08:00
huanghongxun 6ce5106271 fix: update curse fabric mod. Closes #1091. 2021-10-07 15:22:03 +08:00
huanghongxun fdbe38496a fix: checkstyle. 2021-10-06 17:44:51 +08:00
huanghongxun abae32fd70 feat(mod): only show buttons for selection when some mods selected. 2021-10-06 17:28:21 +08:00
huanghongxun e4a1fa5bb4 fix(ui): crash. 2021-10-06 17:18:21 +08:00
huanghongxun 1b8cf6b1e5 feat(ui): decorator page animation.. 2021-10-06 17:05:51 +08:00
huanghongxun 135f15dd9f feat(ui): DecoratorAnimatedPage for future animation implementation. 2021-10-06 16:08:08 +08:00
huanghongxun 1cd4470187 feat(ui): title opacity setting. 2021-10-06 15:52:31 +08:00
huanghongxun 3a2f5a77ef fix(multiplayer): broadcast port wrong. 2021-10-06 03:22:14 +08:00
huanghongxun 4201ac9203 fix(ui): add download new game in gamelistpage again for tradition. 2021-10-06 03:22:14 +08:00
huanghongxun 6c3178a831 feat(mod): mod update. 2021-10-06 03:22:13 +08:00
huanghongxun d48591a4cd feat(mod): WIP: mod update. 2021-10-06 03:22:13 +08:00
huanghongxun 271885a85e fix(launch): On Linux x86_64, Minecraft 1.12.2- are not compatible with JDK 9+. 2021-10-06 03:22:12 +08:00
huanghongxun 57e6765bfd feat(crash): show forge/fabric version in game crash window. 2021-10-06 03:22:12 +08:00
huanghongxun 9a46596b6c fix(skin): NPE. 2021-10-06 03:22:11 +08:00
huanghongxun 27f6ff74fc fix(feedback): localized login error message. 2021-10-06 03:22:11 +08:00
huanghongxun 03df019638 fix: onWeakChange should return ChangeListener instead of WeakChangeListener. 2021-10-06 03:22:10 +08:00
huanghongxun 23750e1222 feat(mod): mod upgrade detection. 2021-10-06 03:22:09 +08:00
huanghongxun ba0a7ddfa8 refactor(task): fix stage progress. 2021-10-06 03:22:08 +08:00
huanghongxun 31327d685b refactor(mod): ModInfo -> LocalMod 2021-10-06 03:22:07 +08:00
huanghongxun bb6bd86e71 refactor(download): RemoteModRepository 2021-10-06 03:22:07 +08:00
catsout 4ba897660b Support XDG_Base_Directory environment variable (#1085) 2021-10-05 19:57:21 +08:00
catsout c3fc22c6a9 Check writable on jarfile for conifg (#1084) 2021-10-05 18:12:08 +08:00
huanghongxun 8748b8c277 feat(multiplayer): relay mode. 2021-10-04 00:38:05 +08:00
huanghongxun e934d6c983 feat(install): Optifine 1.17 compatible with Forge now. 2021-10-03 22:04:55 +08:00
huanghongxun dfb2d3f2bf feat(feedback): implement feedbacks. 2021-10-03 20:34:42 +08:00
huanghongxun c2a87e2474 feat(multiplayer): token should be hidden. 2021-10-03 20:34:41 +08:00
huanghongxun 9229727544 feat(multiplayer): move token storage to glboal to prevent from token leak. 2021-10-03 20:34:40 +08:00
huanghongxun cc8dd77d90 fix: cancellation exception. 2021-10-03 20:34:40 +08:00
Jason_Khew de9233fba5 fix(ui): AccountLoginPane not showing. (#1080) 2021-10-03 14:15:53 +08:00
huanghongxun 05cda699a5 fix(ui): AboutPage JSTUN. 2021-09-30 20:29:12 +08:00
huanghongxun 358a520305 feat(ui): adjust root page icons. 2021-09-30 20:22:46 +08:00
huanghongxun 57cb98f13a feat(build): try proguard again. 2021-09-30 19:38:51 +08:00
huanghongxun 33aeb24dbe fix(download): no longer download fabric api list from bmclapi. 2021-09-30 19:35:47 +08:00
huanghongxun 973510a3df fix(skin): cannot find image. 2021-09-30 19:31:39 +08:00
huanghongxun ae8f67d507 feat(multiplayer): join application. 2021-09-30 17:38:18 +08:00
huanghongxun a48336db4f feat(multiplayer): kick player. 2021-09-30 17:27:43 +08:00
huanghongxun e69ef0ce25 feat(multiplayer): cato EULA. 2021-09-30 15:01:56 +08:00
huanghongxun e01c31fb1d feat: show java version detection progress when launching game. 2021-09-29 18:52:56 +08:00
huanghongxun 36d1096976 fix: java version detection timeout. 2021-09-29 17:24:32 +08:00
huanghongxun 6901228a15 fix: ui crash. 2021-09-29 14:58:56 +08:00
huanghongxun 8100662eb5 feat(about): JSTUN 2021-09-28 21:42:40 +08:00
huanghongxun ee4c03a427 chore(about): show bilibili ID instead of personal website. 2021-09-28 20:42:56 +08:00
huanghongxun adb2ed0614 fix(download): only list required mod dependency 2021-09-28 20:30:36 +08:00
huanghongxun e78fd8ba94 feat: adjust announcement pane look. 2021-09-28 17:32:29 +08:00
huanghongxun 754ecadbb1 feat(crash): notify feedback when game crashed. 2021-09-28 17:23:07 +08:00
huanghongxun ee89815628 feat(build): build with version type. 2021-09-28 17:09:19 +08:00
huanghongxun 8a9527bf38 feat(crash): show fabric mod confliction and missing. 2021-09-28 17:08:43 +08:00
huanghongxun e476460ab4 feat: notification of dev/nightly version. 2021-09-28 12:54:54 +08:00
huanghongxun b556c51307 feat(multiplayer): see lobby members. 2021-09-28 02:13:33 +08:00
huanghongxun fac812f1dd fix(multiplayer): MultiplayerChannel works. 2021-09-28 02:05:22 +08:00
huanghongxun c66cea41bd fix: offline account crash. 2021-09-28 01:44:57 +08:00
huanghongxun 8a3cdfb14c fix(multiplayer): wrong port. 2021-09-27 22:18:48 +08:00
huanghongxun a58de31d4b feat: auto select java. Closes #1068. 2021-09-27 20:54:23 +08:00
huanghongxun 04e6897a0d feat: WIP: auto java. 2021-09-27 20:54:22 +08:00
huanghongxun 59df08cd93 fix(multiplayer): accidentally enabled relay. 2021-09-27 20:54:04 +08:00
huanghongxun c70898ced7 fix(skin): remove javafx.swing dep. 2021-09-27 20:53:33 +08:00
huanghongxun ecc38621df feat: print found java versions in log. 2021-09-27 13:05:46 +08:00
huanghongxun 56327eba54 feat(multiplayer): show cato version. 2021-09-27 13:05:28 +08:00
huanghongxun e776532947 feat(multiplayer): save token. 2021-09-27 12:48:42 +08:00
huanghongxun 35f2d89a23 fix(multiplayer): bind to 0.0.0.0 instead of 127.0.0.1. 2021-09-27 12:38:43 +08:00
huanghongxun 3515ce1034 fix: bidirectional binding cannot be chained. Closes #1067. 2021-09-26 03:24:32 +08:00
huanghongxun 6c1046c060 feat(multiplayer): notify user if connection lost. 2021-09-26 03:08:06 +08:00
huanghongxun 7c76c919bc feat(multiplayer): custom token. 2021-09-26 00:34:18 +08:00
huanghongxun 54954f276b feat(multiplayer): detect game port when creating multiplayer room. Update cato to 1.0.8. 2021-09-26 00:02:48 +08:00
huanghongxun 7fd8e0721f feat: import .minecraft modpack. Closes #973. 2021-09-25 20:24:50 +08:00
huanghongxun d075dd27bc fix: style. 2021-09-25 02:17:11 +08:00
huanghongxun ace8be3df4 fix(multiplayer): update cato. 2021-09-25 02:16:44 +08:00
huanghongxun c60df2c26b chore: cleanup. Closes #989. 2021-09-25 02:11:31 +08:00
huanghongxun cd030c1de0 feat: support change skin of offline accounts. 2021-09-25 02:04:25 +08:00
huanghongxun 01893b053d feat(skin): WIP: download skin from CSL server. 2021-09-23 23:41:45 +08:00
huanghongxun 79e4aa5aaf feat: show nav to special settings page in global settings page if needed. 2021-09-23 21:20:16 +08:00
huanghongxun 3f4dbb6978 feat: discord & kaiheila in AboutPage. 2021-09-23 16:17:26 +08:00
huanghongxun 1df5b0c629 fix: compilation. 2021-09-23 15:04:15 +08:00
Glavo 83dbeddb12 More OpenJDK Providers 2021-09-22 13:43:01 +08:00
Glavo a8522320ed Search for Java installed by the official launcher 2021-09-22 13:43:01 +08:00
Glavo bcaf3de164 fix #1054: Avoid incorrect UI layout caused by long strings 2021-09-22 13:39:59 +08:00
huanghongxun cbe9f7474e feat: skin viewer 3d 2021-09-22 13:38:35 +08:00
huanghongxun 3a4b4e129f fix: offline account skin 2021-09-22 13:38:26 +08:00
huanghongxun 4535b546f4 feat: EULA. 2021-09-21 21:55:58 +08:00
huanghongxun 7d0bbc6952 fix: feedback page navigation. 2021-09-21 19:57:21 +08:00
huanghongxun ea844e03f9 feat: clicking blanks area also toggles component list cell expansion. 2021-09-21 19:57:07 +08:00
huanghongxun c7a893b02d feat: clicking blanks area also controls the toggle button. 2021-09-21 19:38:34 +08:00
huanghongxun ca75ecbc93 feat(multiplayer): report. 2021-09-21 18:47:34 +08:00
huanghongxun 3f9d16ab8c feat: eula. 2021-09-21 18:41:50 +08:00
huanghongxun fb7ff7b891 feat(multiplayer): check cato version before establishing connection. 2021-09-21 16:39:15 +08:00
huanghongxun 3cf5b97201 feat(multiplayer): show state of cato. 2021-09-21 11:12:04 +08:00
huanghongxun 2f5a9a50af fix: checkstyle. 2021-09-21 10:19:07 +08:00
huanghongxun 8153d3bda9 feat: customize font. Closes #967. 2021-09-21 10:11:00 +08:00
huanghongxun e796e0f35e feat(offline-skin): WIP: implement yggdrasil server to serve skin/cape textures. 2021-09-21 09:35:14 +08:00
huanghongxun 4537fe0390 fix: refine UI. Closes #1052. 2021-09-21 06:59:10 +08:00
huanghongxun 87944a4934 feat: AggregatedObservableListTest 2021-09-21 06:50:31 +08:00
huanghongxun 33b9d37250 fix: cannot launch game at first click with 1.17.1+fabric 0.2 2021-09-21 06:44:43 +08:00
huanghongxun ebeec04f83 feat: crash report analyzer more rules. 2021-09-21 06:43:52 +08:00
huanghongxun 798327eabb feat: crash report analyzer test. 2021-09-21 01:19:22 +08:00
asdqp233 702dd21eec 更新英文翻译 (#1050)
* updata lang file

* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N.properties
2021-09-20 23:47:01 +08:00
huanghongxun e5754c70da feat: allow not checking modpack completion when option "Do not check game completion" is checked. Closes #1038. 2021-09-20 21:58:47 +08:00
huanghongxun b95119bb0e feat: show more friendly message if installing mod in InstallerListPage. Closes #996. 2021-09-20 21:55:07 +08:00
huanghongxun ae8600c882 feat: update HMCLauncher. Closes #1014. 2021-09-20 21:25:28 +08:00
huanghongxun 271fb4ce87 feat: show warning when logging in Microsoft account if app is released as snapshot version. 2021-09-20 19:36:11 +08:00
huanghongxun 37cee9cebd feat: show migration hint when 410. Closes #1045. 2021-09-20 18:07:51 +08:00
huanghongxun 0b2e9c3f62 feat: Mod dependencies & Mod downloads should be classified. 2021-09-20 17:36:02 +08:00
huanghongxun f2741f9725 fix: crash when PATH is malformed. Closes #1048. 2021-09-20 17:36:02 +08:00
Glavo e4a7ba0bb5 fix: build 2021-09-20 14:29:17 +08:00
Glavo 6ac8519a9f Automatically download OpenJFX when JavaFX is missing 2021-09-20 14:29:17 +08:00
huanghongxun 01158c2320 feat: thanks to noin.cn 2021-09-20 13:52:16 +08:00
huanghongxun bbb78ec749 fix: clicking gamelistitem should navigate to versionpage. 2021-09-20 13:35:48 +08:00
huanghongxun 90976cc7e0 fix: should not navigate to DownloadPage when clicking GameListItem 2021-09-20 13:31:35 +08:00
huanghongxun cfe801f280 feat: classic background. 2021-09-20 03:09:55 +08:00
huanghongxun ca3bc69a61 feat: multifileitem supports multiple custom type. 2021-09-20 01:54:08 +08:00
huanghongxun 43236049ac feat: game crash window. 2021-09-19 16:15:36 +08:00
huanghongxun 3c797252d8 feat(multiplayer): support cato --allow. 2021-09-19 12:15:45 +08:00
huanghongxun 543238be4b fix: crash when concurrency is negative. 2021-09-19 10:42:55 +08:00
huanghongxun 1b539ade9d feat: hint for custom commands. 2021-09-19 03:00:18 +08:00
huanghongxun c0f82b1ae7 feat: env vars for pre-launch command and post-exit command. 2021-09-19 02:44:51 +08:00
huanghongxun 5b02393c79 feat: smooth scrolling. 2021-09-19 01:35:27 +08:00
huanghongxun c54a3f2053 feat(multiplayer): broadcast lan server. 2021-09-19 01:31:00 +08:00
huanghongxun e90ec531e5 fix(task): test. 2021-09-19 00:43:22 +08:00
huanghongxun 6ac2274c29 fix(multiplayer): exchange "from" and "to". 2021-09-19 00:42:51 +08:00
huanghongxun ea9f547ada fix: FileDownloadTask does not fail if 404. 2021-09-18 23:26:37 +08:00
huanghongxun 28cc7c0700 fix: checkstyle. 2021-09-18 22:01:15 +08:00
huanghongxun 6761028afd feat(multiplayer): cato temp token 2021-09-18 21:57:12 +08:00
huanghongxun 27f87c3a25 feat: show no version hint in GameListPage 2021-09-18 20:10:28 +08:00
huanghongxun 223c45130f feat: add purchase link 2021-09-18 20:00:06 +08:00
huanghongxun 3513c1a431 chore: cleanup. 2021-09-18 15:49:07 +08:00
huanghongxun 5b4185840f fix: Cannot recognize MCBBS v2 modpack. Closes #1041. 2021-09-18 15:49:07 +08:00
huanghongxun 390f6e016d fix(task): wrong cancellation implementation. Closes #1035. 2021-09-18 15:49:07 +08:00
huanghongxun e10e1e0613 fix: Browse Mod Directory Button in ModListPage does not work. Closes #1039. 2021-09-18 15:49:07 +08:00
huanghongxun ba41e6bc5e feat(multiplayer): invitation code generation. 2021-09-18 14:23:00 +08:00
huanghongxun 05562b5c2a feat: unify look. 2021-09-18 02:57:03 +08:00
huanghongxun 7d9cce131b fix: icon color should be changed when active. 2021-09-18 01:51:38 +08:00
huanghongxun 112b096c56 feat: rippler container cursor hover animation 2021-09-18 01:41:44 +08:00
huanghongxun aecb100498 feat: refine modpack export page look. 2021-09-18 00:05:33 +08:00
huanghongxun d5b4aa5d44 chore: exchange title and tag of modlistpage item. 2021-09-17 23:32:03 +08:00
huanghongxun cbe5e3a9ff fix: horizontal scroll bar should not appear. 2021-09-17 23:29:09 +08:00
huanghongxun ff3fa31c25 chore: larger contrast color palette in game memory setting. 2021-09-17 23:29:08 +08:00
Glavo 7baf8eba6e Upgrade gradle wrapper 2021-09-17 23:26:54 +08:00
Glavo 53b0223257 Use Gradle 7.2 2021-09-17 23:26:54 +08:00
Glavo 91cc83c054 fix: action name 2021-09-16 14:29:35 +08:00
Haowei Wen b1031be748 fix: nanohttpd isn't started as daemon thread 2021-09-13 16:59:22 +08:00
Haowei Wen c6b76f5200 remove 'Download' category in DownloadPage 2021-09-13 16:42:45 +08:00
huanghongxun 08269b7ce9 build: checkstyle in another workflow. 2021-09-13 15:45:41 +08:00
Redlnn c65ef8107d fix: fixed Chinese grammatical errors & clean up 2021-09-13 14:25:54 +08:00
343 changed files with 27398 additions and 4442 deletions

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
name: Check Style
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
run: ./gradlew checkstyleTest
+67
View File
@@ -0,0 +1,67 @@
name: Check Update
on:
push:
branches:
- javafx
jobs:
check-update:
if: ${{ github.repository_owner == 'huanghongxun' }}
runs-on: ubuntu-latest
env:
HMCL_BASE_VERSION: '3.4'
steps:
- uses: actions/checkout@v2
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_BUILD_NUMBER}" != 3 ]; then exit 1; fi
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v1
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
+4 -6
View File
@@ -12,19 +12,17 @@ jobs:
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
- name: Build with Gradle
run: ./gradlew build
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: HMCL
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
run: ./gradlew checkstyleTest
+1
View File
@@ -10,6 +10,7 @@ hs_err_pid*
*.2
*.log
.mine*
/externalgames
NVIDIA
# gradle build
+97 -8
View File
@@ -6,6 +6,8 @@ buildscript {
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
classpath 'com.guardsquare:proguard-gradle:7.1.0' // The ProGuard Gradle plugin.
}
}
@@ -38,14 +40,16 @@ if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.4"
def microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
def versionType = System.getenv("VERSION_TYPE") ?: "nightly"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation project(":JSTUN")
implementation rootProject.files("lib/JFoenix.jar")
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
}
def digest(String algorithm, byte[] bytes) {
@@ -114,13 +118,8 @@ compileJava11Java {
targetCompatibility = 11
}
jar {
enabled = false
dependsOn shadowJar
}
shadowJar {
classifier = null
classifier = "shadow"
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
@@ -129,6 +128,7 @@ shadowJar {
'Implementation-Version': project.version,
'Microsoft-Auth-Id': microsoftAuthId,
'Microsoft-Auth-Secret': microsoftAuthSecret,
'Build-Channel': versionType,
'Class-Path': 'pack200.jar',
'Add-Opens': [
'java.base/java.lang',
@@ -152,6 +152,89 @@ shadowJar {
'javafx.base/com.sun.javafx.event'
].join(" ")
}
}
task proguard(type: proguard.gradle.ProGuardTask) {
dependsOn shadowJar
injars shadowJar
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
dontobfuscate
dontoptimize
keep 'class org.jackhuang.** { *; }'
keep 'class com.jfoenix.** { *; }'
keep 'class com.google.gson.** { *; }'
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
keep 'class * extends com.google.gson.TypeAdapter'
keep 'class * implements com.google.gson.TypeAdapterFactory'
keep 'class * implements com.google.gson.JsonSerializer'
keep 'class * implements com.google.gson.JsonDeserializer'
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
System.out.println(new File(".").absolutePath)
dontwarn 'com.nqzero.**'
dontwarn 'org.slf4j.**'
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
dontwarn 'com.sun.javafx.**'
dontwarn 'com.jfoenix.**'
dontwarn 'org.apache.**'
adaptclassstrings
// next block taken verbatim from Proguard's documentation examples:
libraryjars files(configurations.compileClasspath.collect())
keepattributes 'SourceFile,LineNumberTable'
keepattributes '*Annotation*'
keepattributes 'Signature'
var javaHome = System.getProperty('java.home')
// Automatically handle the Java version of this build.
if (System.getProperty('java.version').startsWith('1.')) {
// Before Java 9, the runtime classes were packaged in a single jar file.
libraryjars "${javaHome}/lib/rt.jar"
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
} else {
// As of Java 9, the runtime classes are packaged in modular jmod files.
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
}
}
}
task finalJar(type: Jar) {
dependsOn proguard
manifest = shadowJar.manifest
from { proguard.outJarFiles.collect { zipTree(it)} }
into('/') {
from { shadowJar.outputs.files.collect { zipTree(it) } }
include("META-INF/versions/**")
}
doLast {
repack(jar.archivePath) // see repack()
@@ -160,6 +243,11 @@ shadowJar {
}
}
jar {
enabled = false
dependsOn finalJar
}
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
@@ -175,7 +263,8 @@ processResources {
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
javaexec {
main = "com.sun.javafx.css.parser.Css2Bin"
classpath = sourceSets.main.compileClasspath
mainClass = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
@@ -0,0 +1,68 @@
package moe.mickey.minecraft.skin.fx;
import javafx.event.Event;
import javafx.event.EventHandler;
import java.util.Arrays;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public final class FunctionHelper {
private FunctionHelper() {
}
@SafeVarargs
public static <T> void always(Consumer<T> consumer, T... ts) {
Arrays.asList(ts).forEach(consumer);
}
@SafeVarargs
public static <A, B> void alwaysA(BiConsumer<A, B> consumer, A a, B... bs) {
Arrays.asList(bs).forEach(b -> consumer.accept(a, b));
}
@SafeVarargs
public static <A, B> void alwaysB(BiConsumer<A, B> consumer, B b, A... as) {
Arrays.asList(as).forEach(a -> consumer.accept(a, b));
}
public static <A, B> BiConsumer<B, A> exchange(BiConsumer<A, B> consumer) {
return (b, a) -> consumer.accept(a, b);
}
@SafeVarargs
public static <T> Consumer<T> link(Consumer<T>... consumers) {
return t -> {
for (Consumer<T> consumer : consumers)
consumer.accept(t);
};
}
@SafeVarargs
public static <T extends Event> EventHandler<T> link(EventHandler<T>... handlers) {
return t -> {
for (EventHandler<T> handler : handlers)
handler.handle(t);
};
}
public static <A, B> Consumer<A> link1(Function<A, B> function, Consumer<B> consumer) {
return a -> consumer.accept(function.apply(a));
}
public static <A, B, C> BiConsumer<A, C> link2(Function<A, B> function, BiConsumer<B, C> consumer) {
return (a, c) -> consumer.accept(function.apply(a), c);
}
public static <A, B> Consumer<B> link2(Supplier<A> supplier, BiConsumer<A, B> consumer) {
return b -> consumer.accept(supplier.get(), b);
}
public static <A, B> Supplier<B> link1(Supplier<A> supplier, Function<A, B> function) {
return () -> function.apply(supplier.get());
}
}
@@ -0,0 +1,58 @@
package moe.mickey.minecraft.skin.fx;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class SkinAnimation {
protected int weight, left;
protected List<SkinTransition> transitions;
@Deprecated
public SkinAnimation() {
this.transitions = new LinkedList<>();
}
public SkinAnimation(int weight, SkinTransition... transitions) {
this.weight = weight;
this.transitions = Arrays.asList(transitions);
init();
}
protected void init() {
transitions.forEach(t -> {
EventHandler<ActionEvent> oldHandler = t.getOnFinished();
EventHandler<ActionEvent> newHandler = e -> left--;
newHandler = oldHandler == null ? newHandler : FunctionHelper.link(oldHandler, newHandler);
t.setOnFinished(newHandler);
});
}
public int getWeight() {
return weight;
}
public boolean isPlaying() {
return left > 0;
}
public void play() {
transitions.forEach(SkinTransition::play);
left = transitions.size();
}
public void playFromStart() {
transitions.forEach(SkinTransition::playFromStart);
left = transitions.size();
}
public void stop() {
transitions.forEach(SkinTransition::stop);
left = 0;
}
}
@@ -0,0 +1,93 @@
package moe.mickey.minecraft.skin.fx;
import javafx.animation.AnimationTimer;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Random;
public class SkinAnimationPlayer {
protected final Random random = new Random();
protected LinkedList<SkinAnimation> animations = new LinkedList<>();
protected SkinAnimation playing;
protected boolean running;
protected int weightedSum = 0;
protected long lastPlayTime = -1L, interval = 10_000_000_000L;
protected AnimationTimer animationTimer = new AnimationTimer() {
@Override
public void handle(long now) {
if (playing == null || !playing.isPlaying() && now - lastPlayTime > interval) {
int nextAni = random.nextInt(weightedSum);
SkinAnimation tmp = null;
for (SkinAnimation animation : animations) {
nextAni -= animation.getWeight();
tmp = animation;
if (nextAni <= 0)
break;
}
playing = tmp;
if (playing == null && animations.size() > 0)
playing = animations.getLast();
if (playing != null) {
playing.playFromStart();
lastPlayTime = now;
}
}
}
};
public int getWeightedSum() {
return weightedSum;
}
public void setInterval(long interval) {
this.interval = interval;
if (interval < 1)
animationTimer.stop();
else
start();
}
public long getInterval() {
return interval;
}
public long getLastPlayTime() {
return lastPlayTime;
}
public boolean isRunning() {
return running;
}
public boolean isPlaying() {
return playing != null;
}
public SkinAnimation getPlaying() {
return playing;
}
public void addSkinAnimation(SkinAnimation... animations) {
this.animations.addAll(Arrays.asList(animations));
this.weightedSum = this.animations.stream().mapToInt(SkinAnimation::getWeight).sum();
start();
}
public void start() {
if (!running && weightedSum > 0 && interval > 0) {
animationTimer.start();
running = true;
}
}
public void stop() {
if (running)
animationTimer.stop();
if (playing != null)
playing.stop();
running = false;
}
}
@@ -0,0 +1,255 @@
package moe.mickey.minecraft.skin.fx;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Shape3D;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
public class SkinCanvas extends Group {
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/steve.png"));
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
public static final SkinCube STEVEN_LARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
public static final SkinCube STEVEN_RARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
protected Image srcSkin, skin;
protected boolean isSlim;
protected double preW, preH;
protected boolean msaa;
protected SubScene subScene;
protected Group root = new Group();
public final SkinMultipleCubes headOuter = new SkinMultipleCubes(8, 8, 8, 32F / 64F, 0F, 1.125, 0.2);
public final SkinMultipleCubes bodyOuter = new SkinMultipleCubes(8, 12, 4, 16F / 64F, 32F / 64F, 1, 0.2);
public final SkinMultipleCubes larmOuter = new SkinMultipleCubes(4, 12, 4, 48F / 64F, 48F / 64F, 1.0625, 0.2);
public final SkinMultipleCubes rarmOuter = new SkinMultipleCubes(4, 12, 4, 40F / 64F, 32F / 64F, 1.0625, 0.2);
public final SkinMultipleCubes llegOuter = new SkinMultipleCubes(4, 12, 4, 0F / 64F, 48F / 64F, 1.0625, 0.2);
public final SkinMultipleCubes rlegOuter = new SkinMultipleCubes(4, 12, 4, 0F / 64F, 32F / 64F, 1.0625, 0.2);
public final SkinCube headInside = new SkinCube(8, 8, 8, 32F / 64F, 16F / 64F, 0F, 0F, 0F, false);
public final SkinCube bodyInside = new SkinCube(8, 12, 4, 24F / 64F, 16F / 64F, 16F / 64F, 16F / 64F, 0.03F, false);
public final SkinCube larmInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
public final SkinCube rarmInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
public final SkinCube llegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 16F / 64F, 48F / 64F, 0F, false);
public final SkinCube rlegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 0F, 16F / 64F, 0F, false);
public final SkinGroup head = new SkinGroup(
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.Z_AXIS),
headOuter, headInside
);
public final SkinGroup body = new SkinGroup(
new Rotate(0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, Rotate.Z_AXIS),
bodyOuter, bodyInside
);
public final SkinGroup larm = new SkinGroup(
new Rotate(0, 0, -larmInside.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, +larmInside.getWidth() / 2, -larmInside.getHeight() / 2, 0, Rotate.Z_AXIS),
larmOuter, larmInside
);
public final SkinGroup rarm = new SkinGroup(
new Rotate(0, 0, -rarmInside.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, -rarmInside.getWidth() / 2, -rarmInside.getHeight() / 2, 0, Rotate.Z_AXIS),
rarmOuter, rarmInside
);
public final SkinGroup lleg = new SkinGroup(
new Rotate(0, 0, -llegInside.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, 0, -llegInside.getHeight() / 2, 0, Rotate.Z_AXIS),
llegOuter, llegInside
);
public final SkinGroup rleg = new SkinGroup(
new Rotate(0, 0, -rlegInside.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, 0, -rlegInside.getHeight() / 2, 0, Rotate.Z_AXIS),
rlegOuter, rlegInside
);
protected PerspectiveCamera camera = new PerspectiveCamera(true);
protected Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
protected Rotate yRotate = new Rotate(180, Rotate.Y_AXIS);
protected Rotate zRotate = new Rotate(0, Rotate.Z_AXIS);
protected Translate translate = new Translate(0, 0, -80);
protected Scale scale = new Scale(1, 1);
protected SkinAnimationPlayer animationplayer = new SkinAnimationPlayer();
public SkinAnimationPlayer getAnimationplayer() {
return animationplayer;
}
public Image getSrcSkin() {
return srcSkin;
}
public Image getSkin() {
return skin;
}
public void updateSkin(Image skin, boolean isSlim) {
if (SkinHelper.isNoRequest(skin) && SkinHelper.isSkin(skin)) {
this.srcSkin = skin;
this.skin = SkinHelper.x32Tox64(skin);
int multiple = Math.max((int) (1024 / skin.getWidth()), 1);
if (multiple > 1)
this.skin = SkinHelper.enlarge(this.skin, multiple);
if (this.isSlim != isSlim)
updateSkinModel(isSlim);
bindMaterial(root);
}
}
protected void updateSkinModel(boolean isSlim) {
this.isSlim = isSlim;
FunctionHelper.alwaysB(SkinMultipleCubes::setWidth, isSlim ? 3 : 4, larmOuter, rarmOuter);
FunctionHelper.alwaysB(SkinCube::setWidth, isSlim ? 3D : 4D, larmInside, rarmInside);
FunctionHelper.alwaysB(Node::setTranslateX, -(bodyInside.getWidth() + larmInside.getWidth()) / 2, larm);
FunctionHelper.alwaysB(Node::setTranslateX, +(bodyInside.getWidth() + rarmInside.getWidth()) / 2, rarm);
if (isSlim) {
larmInside.setModel(ALEX_LARM.getModel());
rarmInside.setModel(ALEX_RARM.getModel());
} else {
larmInside.setModel(STEVEN_LARM.getModel());
rarmInside.setModel(STEVEN_RARM.getModel());
}
larm.getZRotate().setPivotX(-larmInside.getWidth() / 2);
rarm.getZRotate().setPivotX(+rarmInside.getWidth() / 2);
}
public SkinCanvas(double preW, double preH) {
this(STEVE, preW, preH, true);
}
public SkinCanvas(Image skin, double preW, double preH, boolean msaa) {
this.skin = skin;
this.preW = preW;
this.preH = preH;
this.msaa = msaa;
init();
}
protected Material createMaterial() {
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(skin);
return material;
}
protected void bindMaterial(Group group) {
Material material = createMaterial();
for (Node node : group.getChildren())
if (node instanceof Shape3D)
((Shape3D) node).setMaterial(material);
else if (node instanceof SkinMultipleCubes)
((SkinMultipleCubes) node).updateSkin(skin);
else if (node instanceof Group)
bindMaterial((Group) node);
}
protected Group createPlayerModel() {
head.setTranslateY(-(bodyInside.getHeight() + headInside.getHeight()) / 2);
larm.setTranslateX(-(bodyInside.getWidth() + larmInside.getWidth()) / 2);
rarm.setTranslateX(+(bodyInside.getWidth() + rarmInside.getWidth()) / 2);
lleg.setTranslateX(-(bodyInside.getWidth() - llegInside.getWidth()) / 2);
rleg.setTranslateX(+(bodyInside.getWidth() - rlegInside.getWidth()) / 2);
lleg.setTranslateY(+(bodyInside.getHeight() + llegInside.getHeight()) / 2);
rleg.setTranslateY(+(bodyInside.getHeight() + rlegInside.getHeight()) / 2);
root.getTransforms().addAll(xRotate);
root.getChildren().addAll(
head,
body,
larm,
rarm,
lleg,
rleg
);
updateSkin(skin, false);
return root;
}
protected SubScene createSubScene() {
Group group = new Group();
group.getChildren().add(createPlayerModel());
group.getTransforms().add(zRotate);
camera.getTransforms().addAll(yRotate, translate, scale);
subScene = new SubScene(group, preW, preH, true,
msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED);
subScene.setCamera(camera);
return subScene;
}
protected void init() {
getChildren().add(createSubScene());
}
private double lastX, lastY;
public void enableRotation(double sensitivity) {
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
lastX = -1;
lastY = -1;
});
addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
if (!(lastX == -1 || lastY == -1)) {
if (e.isAltDown() || e.isControlDown() || e.isShiftDown()) {
if (e.isShiftDown())
zRotate.setAngle(zRotate.getAngle() - (e.getSceneY() - lastY) * sensitivity);
if (e.isAltDown())
yRotate.setAngle(yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity);
if (e.isControlDown())
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity);
} else {
double yaw = yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity;
yaw %= 360;
if (yaw < 0)
yaw += 360;
int flagX = yaw < 90 || yaw > 270 ? 1 : -1;
int flagZ = yaw < 180 ? -1 : 1;
double kx = Math.abs(90 - yaw % 180) / 90 * flagX, kz = Math.abs(90 - (yaw + 90) % 180) / 90 * flagZ;
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kx);
yRotate.setAngle(yaw);
zRotate.setAngle(zRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kz);
}
}
lastX = e.getSceneX();
lastY = e.getSceneY();
});
addEventHandler(ScrollEvent.SCROLL, e -> {
double delta = (e.getDeltaY() > 0 ? 1 : e.getDeltaY() == 0 ? 0 : -1) / 10D * sensitivity;
scale.setX(Math.min(Math.max(scale.getX() - delta, 0.1), 10));
scale.setY(Math.min(Math.max(scale.getY() - delta, 0.1), 10));
});
}
}
@@ -0,0 +1,142 @@
package moe.mickey.minecraft.skin.fx;
import javafx.scene.image.Image;
import javafx.scene.shape.Mesh;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import org.apache.commons.lang3.ArrayUtils;
public class SkinCube extends MeshView {
public static class Model extends TriangleMesh {
public Model(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, boolean isSlim) {
getPoints().addAll(createPoints(width, height, depth));
getTexCoords().addAll(createTexCoords(width, height, depth, scaleX, scaleY, startX, startY, isSlim));
getFaces().addAll(createFaces());
}
public static float[] createPoints(float width, float height, float depth) {
width /= 2F;
height /= 2F;
depth /= 2F;
return new float[]{
-width, -height, depth, // P0
width, -height, depth, // P1
-width, height, depth, // P2
width, height, depth, // P3
-width, -height, -depth, // P4
width, -height, -depth, // P5
-width, height, -depth, // P6
width, height, -depth // P7
};
}
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
float startX, float startY, boolean isSlim) {
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
return new float[]{
top_x, top_y, // T0 ---
top_x + half_width, top_y, // T1 |
top_x + half_width * 2, top_y, // T2 ---
bottom_x, middle_y, // T3 ---
bottom_x + half_depth, middle_y, // T4 |
bottom_x + half_depth + half_width, middle_y, // T5 |
bottom_x + scaleX - arm4, middle_y, // T6 |
bottom_x + scaleX, middle_y, // T7 ---
bottom_x, bottom_y, // T8 ---
bottom_x + half_depth, bottom_y, // T9 |
bottom_x + half_depth + half_width, bottom_y, // T10 |
bottom_x + scaleX - arm4, bottom_y, // T11 |
bottom_x + scaleX, bottom_y // T12 ---
};
}
public static int[] createFaces() {
int[] faces = new int[]{
// TOP
5, 0, 4, 1, 0, 5, //P5,T0, P4,T1, P0,T5
5, 0, 0, 5, 1, 4, //P5,T0, P0,T5, P1,T4
// RIGHT
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
// FRONT
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
// LEFT
5, 3, 1, 4, 3, 9, //P5,T6, P1,T5, P3,T10
5, 3, 3, 9, 7, 8, //P5,T6, P3,T10,P7,T11
// BACK
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
// BOTTOM
3, 5, 2, 6, 6, 2, //P3,T2, P2,T1, P6,T5
3, 5, 6, 2, 7, 1 //P3,T2, P6,T5, P7,T6
};
int[] copy = faces.clone();
ArrayUtils.reverse(copy);
for (int i = 0; i < copy.length; i += 2) {
int tmp = copy[i];
copy[i] = copy[i + 1];
copy[i + 1] = tmp;
}
return ArrayUtils.addAll(faces, copy);
}
}
private double width, height, depth;
private boolean isSlim;
private Image skin;
private Mesh model;
public SkinCube(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, float enlarge, boolean isSlim) {
this.width = width;
this.height = height;
this.depth = depth;
this.isSlim = isSlim;
setMesh(model = new Model(width + enlarge, height + enlarge, depth + enlarge, scaleX, scaleY, startX, startY, isSlim));
}
public void setWidth(double width) {
this.width = width;
}
public double getWidth() {
return width;
}
public void setHeight(double height) {
this.height = height;
}
public double getHeight() {
return height;
}
public void setDepth(double depth) {
this.depth = depth;
}
public double getDepth() {
return depth;
}
public boolean isSlim() {
return isSlim;
}
public Mesh getModel() {
return model;
}
public void setModel(Mesh model) {
this.model = model;
setMesh(model);
}
}
@@ -0,0 +1,45 @@
package moe.mickey.minecraft.skin.fx;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.transform.Rotate;
public class SkinGroup extends Group {
protected Rotate xRotate, yRotate, zRotate;
public SkinGroup(Rotate xRotate, Rotate yRotate, Rotate zRotate, Node... nodes) {
this.xRotate = xRotate;
this.yRotate = yRotate;
this.zRotate = zRotate;
Group group = new Group();
group.getChildren().addAll(nodes);
getChildren().add(addRotate(group, xRotate, yRotate, zRotate));
}
protected Group addRotate(Group group, Rotate... rotates) {
for (Rotate rotate : rotates)
group = addRotate(group, rotate);
return group;
}
protected Group addRotate(Group group, Rotate rotate) {
Group newGroup = new Group();
group.getTransforms().add(rotate);
newGroup.getChildren().add(group);
return newGroup;
}
public Rotate getXRotate() {
return xRotate;
}
public Rotate getYRotate() {
return yRotate;
}
public Rotate getZRotate() {
return zRotate;
}
}
@@ -0,0 +1,125 @@
package moe.mickey.minecraft.skin.fx;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
public final class SkinHelper {
private SkinHelper() {
}
public static class PixelCopyer {
protected Image srcImage;
protected WritableImage newImage;
public PixelCopyer(Image srcImage, WritableImage newImage) {
this.srcImage = srcImage;
this.newImage = newImage;
}
public void copy(int srcX, int srcY, int width, int height) {
copy(srcX, srcY, srcX, srcY, width, height);
}
public void copy(int srcX, int srcY, int toX, int toY, int width, int height) {
copy(srcX, srcY, toX, toY, width, height, false, false);
}
public void copy(int srcX, int srcY, int toX, int toY, int width, int height, boolean reversalX, boolean reversalY) {
PixelReader reader = srcImage.getPixelReader();
PixelWriter writer = newImage.getPixelWriter();
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
writer.setArgb(toX + x, toY + y,
reader.getArgb(srcX + (reversalX ? width - x - 1 : x), srcY + (reversalY ? height - y - 1 : y)));
}
public void copy(float srcX, float srcY, float toX, float toY, float width, float height) {
copy(srcX, srcY, toX, toY, width, height, false, false);
}
public void copy(float srcX, float srcY, float toX, float toY, float width, float height, boolean reversalX, boolean reversalY) {
PixelReader reader = srcImage.getPixelReader();
PixelWriter writer = newImage.getPixelWriter();
int srcScaleX = (int) srcImage.getWidth();
int srcScaleY = (int) srcImage.getHeight();
int newScaleX = (int) newImage.getWidth();
int newScaleY = (int) newImage.getHeight();
int srcWidth = (int) (width * srcScaleX);
int srcHeight = (int) (height * srcScaleY);
for (int x = 0; x < srcWidth; x++)
for (int y = 0; y < srcHeight; y++)
writer.setArgb((int) (toX * newScaleX + x), (int) (toY * newScaleY + y),
reader.getArgb((int) (srcX * srcScaleX + (reversalX ? srcWidth - x - 1 : x)),
(int) (srcY * srcScaleY + (reversalY ? srcHeight - y - 1 : y))));
}
}
public static boolean isNoRequest(Image image) {
return image.getRequestedWidth() == 0 && image.getRequestedHeight() == 0;
}
public static boolean isSkin(Image image) {
return image.getWidth() % 64 == 0 && image.getWidth() / 64 > 0 &&
(image.getHeight() == image.getWidth() / 2 || image.getHeight() == image.getWidth());
}
public static Image x32Tox64(Image srcSkin) {
if (srcSkin.getHeight() == 64)
return srcSkin;
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth(), (int) srcSkin.getHeight() * 2);
PixelCopyer copyer = new PixelCopyer(srcSkin, newSkin);
// HEAD & HAT
copyer.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
// LEFT-LEG
x32Tox64(copyer, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
// RIGHT-LEG
copyer.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
// BODY
copyer.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
// LEFT-ARM
x32Tox64(copyer, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
// RIGHT-ARM
copyer.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
return newSkin;
}
static void x32Tox64(PixelCopyer copyer, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
// TOP
copyer.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
// BOTTOM
copyer.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
// INS
copyer.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
// OUTS
copyer.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
// FRONT
copyer.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
// BACK
copyer.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
}
public static Image enlarge(Image srcSkin, int multiple) {
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multiple, (int) srcSkin.getHeight() * multiple);
PixelReader reader = srcSkin.getPixelReader();
PixelWriter writer = newSkin.getPixelWriter();
for (int x = 0, lenX = (int) srcSkin.getWidth(); x < lenX; x++)
for (int y = 0, lenY = (int) srcSkin.getHeight(); y < lenY; y++)
for (int mx = 0; mx < multiple; mx++)
for (int my = 0; my < multiple; my++) {
int argb = reader.getArgb(x, y);
writer.setArgb(x * multiple + mx, y * multiple + my, argb);
}
return newSkin;
}
}
@@ -0,0 +1,161 @@
package moe.mickey.minecraft.skin.fx;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class SkinMultipleCubes extends Group {
public static class Face extends Group {
public Face(Image image, int startX, int startY, int width, int height, int interval, boolean reverseX, boolean reverseY,
Supplier<Box> supplier, BiConsumer<Box, Point2D> consumer) {
PixelReader reader = image.getPixelReader();
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++) {
int argb;
if ((argb = reader.getArgb(startX + (reverseX ? width - x - 1 : x) * interval,
startY + (reverseY ? height - y - 1 : y) * interval)) != 0) {
Box pixel = supplier.get();
consumer.accept(pixel, new Point2D(x, y));
pixel.setMaterial(createMaterial(Color.rgb(
(argb >> 16) & 0xFF, (argb >> 8) & 0xFF, (argb >> 0) & 0xFF)));
getChildren().add(pixel);
}
}
}
protected Material createMaterial(Color color) {
return new PhongMaterial(color);
}
}
protected int width, height, depth;
protected float startX, startY;
protected double length, thick;
public SkinMultipleCubes(int width, int height, int depth, float startX, float startY, double length, double thick) {
this.width = width;
this.height = height;
this.depth = depth;
this.startX = startX;
this.startY = startY;
this.length = length;
this.thick = thick;
}
public void setWidth(int width) {
this.width = width;
}
public int getWidth() {
return width;
}
public void setHeight(int height) {
this.height = height;
}
public int getHeight() {
return height;
}
public void setDepth(int depth) {
this.depth = depth;
}
public int getDepth() {
return depth;
}
public void setStartX(float startX) {
this.startX = startX;
}
public float getStartX() {
return startX;
}
public void setStartY(float startY) {
this.startY = startY;
}
public float getStartY() {
return startY;
}
public void setLength(double length) {
this.length = length;
}
public double getLength() {
return length;
}
public void setThick(double thick) {
this.thick = thick;
}
public double getThick() {
return thick;
}
public void updateSkin(Image skin) {
getChildren().clear();
int start_x = (int) (startX * skin.getWidth()), start_y = (int) (startY * skin.getHeight()),
interval = (int) Math.max(skin.getWidth() / 64, 1),
width_interval = width * interval, height_interval = height * interval, depth_interval = depth * interval;
// FRONT
getChildren().add(new Face(skin, start_x + depth_interval, start_y + depth_interval, width, height, interval, false, false,
() -> new Box(length, length, thick), (b, p) -> {
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
b.setTranslateZ((depth * length + thick) / 2.0);
}));
// BACK
getChildren().add(new Face(skin, start_x + width_interval + depth_interval * 2, start_y + depth_interval, width, height, interval, true, false,
() -> new Box(length, length, thick), (b, p) -> {
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
b.setTranslateZ(-(depth * length + thick) / 2.0);
}));
// LEFT
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y + depth_interval, depth, height, interval, false, false,
() -> new Box(thick, length, length), (b, p) -> {
b.setTranslateX((width * length + thick) / 2.0);
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
}));
// RIGHT
getChildren().add(new Face(skin, start_x, start_y + depth_interval, depth, height, interval, true, false,
() -> new Box(thick, length, length), (b, p) -> {
b.setTranslateX(-(width * length + thick) / 2.0);
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
}));
// TOP
getChildren().add(new Face(skin, start_x + depth_interval, start_y, width, depth, interval, false, false,
() -> new Box(length, thick, length), (b, p) -> {
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
b.setTranslateY(-(height * length + thick) / 2.0);
b.setTranslateZ(-((depth - 1) / 2.0 - p.getY()) * b.getDepth());
}));
// BOTTOM
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y, width, depth, interval, false, false,
() -> new Box(length, thick, length), (b, p) -> {
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
b.setTranslateY((height * length + thick) / 2.0);
b.setTranslateZ(-((depth - 1) / 2.0 - p.getY()) * b.getDepth());
}));
}
}
@@ -0,0 +1,42 @@
package moe.mickey.minecraft.skin.fx;
import javafx.animation.Transition;
import javafx.beans.value.WritableValue;
import javafx.util.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
public class SkinTransition extends Transition {
protected Function<Double, Double> expression;
protected List<WritableValue<Number>> observables;
protected boolean fix;
protected int count;
public int getCount() {
return count;
}
public SkinTransition(Duration duration, Function<Double, Double> expression, WritableValue<Number>... observables) {
setCycleDuration(duration);
this.expression = expression;
this.observables = Arrays.asList(observables);
}
@Override
protected void interpolate(double frac) {
if (frac == 0 || frac == 1)
count++;
double val = expression.apply(frac);
observables.forEach(w -> w.setValue(val));
}
@Override
public void play() {
count = 0;
super.play();
}
}
@@ -0,0 +1,29 @@
package moe.mickey.minecraft.skin.fx.animation;
import javafx.util.Duration;
import moe.mickey.minecraft.skin.fx.FunctionHelper;
import moe.mickey.minecraft.skin.fx.SkinAnimation;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.SkinTransition;
public final class SkinAniRunning extends SkinAnimation {
private SkinTransition larmTransition, rarmTransition;
public SkinAniRunning(int weight, int time, double angle, SkinCanvas canvas) {
larmTransition = new SkinTransition(Duration.millis(time),
v -> v * (larmTransition.getCount() % 4 < 2 ? 1 : -1) * angle,
canvas.larm.getXRotate().angleProperty(), canvas.rleg.getXRotate().angleProperty());
rarmTransition = new SkinTransition(Duration.millis(time),
v -> v * (rarmTransition.getCount() % 4 < 2 ? 1 : -1) * -angle,
canvas.rarm.getXRotate().angleProperty(), canvas.lleg.getXRotate().angleProperty());
FunctionHelper.alwaysB(SkinTransition::setAutoReverse, true, larmTransition, rarmTransition);
FunctionHelper.alwaysB(SkinTransition::setCycleCount, 16, larmTransition, rarmTransition);
FunctionHelper.always(transitions::add, larmTransition, rarmTransition);
this.weight = weight;
init();
}
}
@@ -0,0 +1,25 @@
package moe.mickey.minecraft.skin.fx.animation;
import javafx.util.Duration;
import moe.mickey.minecraft.skin.fx.FunctionHelper;
import moe.mickey.minecraft.skin.fx.SkinAnimation;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.SkinTransition;
public final class SkinAniWavingArms extends SkinAnimation {
public SkinAniWavingArms(int weight, int time, double angle, SkinCanvas canvas) {
SkinTransition larmTransition = new SkinTransition(Duration.millis(time), v -> v * angle,
canvas.larm.getZRotate().angleProperty());
SkinTransition rarmTransition = new SkinTransition(Duration.millis(time), v -> v * -angle,
canvas.rarm.getZRotate().angleProperty());
FunctionHelper.alwaysB(SkinTransition::setAutoReverse, true, larmTransition, rarmTransition);
FunctionHelper.alwaysB(SkinTransition::setCycleCount, 2, larmTransition, rarmTransition);
FunctionHelper.always(transitions::add, larmTransition, rarmTransition);
this.weight = weight;
init();
}
}
@@ -21,6 +21,7 @@ import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.ui.AwtUtils;
@@ -31,6 +32,7 @@ import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
@@ -61,6 +63,8 @@ public final class Launcher extends Application {
try {
try {
ConfigHolder.init();
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", e);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
@@ -106,7 +110,9 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
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"));
@@ -116,7 +122,7 @@ public final class Launcher extends Application {
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
launch(args);
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
+11 -30
View File
@@ -20,7 +20,9 @@ package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import javax.net.ssl.*;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
@@ -33,8 +35,8 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -59,10 +61,7 @@ public final class Main {
checkDirectoryPath();
// This environment check will take ~300ms
thread(() -> {
fixLetsEncrypt();
checkDSTRootCAX3();
}, "CA Certificate Check", true);
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
@@ -99,31 +98,12 @@ public final class Main {
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
System.exit(0);
}
}
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.
*/
@@ -153,8 +133,9 @@ public final class Main {
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
}
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Path;
@@ -41,6 +42,9 @@ public final class Metadata {
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
public static final String BUILD_CHANNEL = JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Build-Channel")).orElse("nightly");
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
@@ -49,8 +53,24 @@ public final class Metadata {
String home = System.getProperty("user.home", ".");
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// to fulfill XDG standard.
String xdgCache = System.getenv("XDG_CACHE_HOME");
if (StringUtils.isNotBlank(xdgCache)) {
return Paths.get(xdgCache, "hmcl");
}
return Paths.get(home, ".cache", "hmcl");
}
return OperatingSystem.getWorkingDirectory("hmcl");
}
public static boolean isStable() {
return "stable".equals(BUILD_CHANNEL);
}
public static boolean isDev() {
return "dev".equals(BUILD_CHANNEL);
}
public static boolean isNightly() {
return !isStable() && !isDev();
}
}
@@ -32,21 +32,24 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -89,6 +92,13 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
public Stream<Version> getDisplayVersions() {
return getVersions().stream()
.filter(v -> !v.isHidden())
.sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime())
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
}
@Override
protected void refreshVersionsImpl() {
localVersionSettings.clear();
@@ -105,7 +115,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
// https://github.com/huanghongxun/HMCL/issues/938
@@ -128,15 +138,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
File srcDir = getVersionRoot(srcId);
File dstDir = getVersionRoot(dstId);
Path srcDir = getVersionRoot(srcId).toPath();
Path dstDir = getVersionRoot(dstId).toPath();
if (dstDir.exists()) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
Version fromVersion = getVersion(srcId);
if (Files.exists(dstDir)) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir, dstDir);
Path fromJson = dstDir.resolve(srcId + ".json");
Path fromJar = dstDir.resolve(srcId + ".jar");
Path toJson = dstDir.resolve(dstId + ".json");
Path toJar = dstDir.resolve(dstId + ".jar");
if (Files.exists(fromJar)) {
Files.move(fromJar, toJar);
}
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
Files.move(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
@@ -258,7 +279,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
return false;
}
}
@@ -284,14 +305,14 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
VersionSetting vs = getVersionSetting(version);
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionType(Metadata.TITLE)
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
@@ -307,7 +328,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
@@ -35,8 +35,6 @@ import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
@@ -107,13 +105,5 @@ public final class HMCLModpackInstallTask extends Task<Void> {
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
@Override
public List<String> getStages() {
return Stream.concat(
dependents.stream().flatMap(task -> task.getStages().stream()),
Stream.of("hmcl.modpack")
).collect(Collectors.toList());
}
public static final String MODPACK_TYPE = "HMCL";
}
@@ -18,14 +18,17 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
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.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.FileSystem;
/**
* @author huangyuhui
@@ -37,23 +40,31 @@ public final class HMCLModpackManager {
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param fs a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
String manifestJson = FileUtils.readText(fs.getPath("modpack.json"));
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = FileUtils.readText(fs.getPath("minecraft/pack.json"));
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
throw new JsonParseException("Cannot recognize the game version of modpack");
else
return manifest.setManifest(HMCLModpackManifest.INSTANCE);
manifest.setManifest(HMCLModpackManifest.INSTANCE);
else
return manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
return manifest;
}
private static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
}
@@ -17,23 +17,19 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
import org.jackhuang.hmcl.launch.ProcessCreationException;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
@@ -42,45 +38,34 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
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.FXUtils;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Queue;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.resolveException;
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;
@@ -105,28 +90,25 @@ public final class LauncherHelper {
this.launchingStepsPane.setTitle(i18n("version.launch"));
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {
});
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
showLogs = true;
}
public void setKeep() {
launcherVisibility = LauncherVisibility.KEEP;
}
public void launch() {
FXUtils.checkFxUserThread();
Logging.LOG.info("Launching game version: " + selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.defaultScheduler().execute(this::launch0);
});
} catch (InterruptedException | RejectedExecutionException ignore) {
}
});
Controllers.dialog(launchingStepsPane);
launch0();
}
public void makeLaunchScript(File scriptFile) {
@@ -143,7 +125,13 @@ public final class LauncherHelper {
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
CountDownLatch launchingLatch = new CountDownLatch(1);
TaskExecutor executor = dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck)
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version)
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
return dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck);
})
.thenComposeAsync(Task.allOf(
Task.composeAsync(() -> {
if (setting.isNotCheckGame())
@@ -151,18 +139,22 @@ public final class LauncherHelper {
else
return dependencyManager.checkGameCompletionAsync(version, integrityCheck);
}), Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
if (setting.isNotCheckGame()) {
return null;
} else {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
}
}))).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
@@ -180,14 +172,15 @@ public final class LauncherHelper {
}
}).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir());
return new HMCLGameLauncher(
repository,
version,
authInfo,
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
launchOptions,
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
: new HMCLProcessListener(repository, version, authInfo, launchOptions, launchingLatch, gameVersion.isPresent())
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
@@ -218,6 +211,7 @@ public final class LauncherHelper {
launchingLatch.await();
}).withStage("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.java",
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
@@ -234,7 +228,7 @@ public final class LauncherHelper {
launchingStepsPane.fireEvent(new DialogCloseEvent());
if (!success) {
Exception ex = executor.getException();
if (ex != null) {
if (!(ex instanceof CancellationException)) {
String message;
if (ex instanceof CurseCompletionException) {
if (ex.getCause() instanceof FileNotFoundException)
@@ -288,6 +282,25 @@ public final class LauncherHelper {
message = i18n("download.code.404", url);
else
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
(result, resolve, reject) -> {
if (CommandBuilder.setExecutionPolicy()) {
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
resolve.run();
} else {
LOG.warning("Failed to set ExecutionPolicy");
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
}
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
);
return;
} else if (ex instanceof AccessDeniedException) {
message = i18n("exception.access_denied", ((AccessDeniedException) ex).getFile());
} else {
message = StringUtils.getStackTrace(ex);
}
@@ -305,248 +318,278 @@ public final class LauncherHelper {
executor.start();
}
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
if (setting.isNotCheckJVM()) {
onAccept.run();
return;
}
boolean flag = false;
boolean java8required = false;
boolean newJavaRequired = false;
// Without onAccept called, the launching operation will be terminated.
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
flag = true;
if (setting.isNotCheckJVM()) {
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
.withStage("launch.state.java");
}
if (!flag && version.getJavaVersion() != null) {
if (java.getParsedVersion() < version.getJavaVersion().getMajorVersion()) {
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
return Task.composeAsync(() -> {
return setting.getJavaVersion(gameVersion, version);
}).thenComposeAsync(Schedulers.javafx(), javaVersion -> {
// Reset invalid java version
if (javaVersion == null) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
if (setting.isJavaAutoSelected()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
GameJavaVersion targetJavaVersion;
if (range.getMandatory().contains(VersionNumber.asVersion("16"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else if (range.getMandatory().contains(VersionNumber.asVersion("1.8.0_51"))) {
targetJavaVersion = GameJavaVersion.JAVA_8;
} else {
targetJavaVersion = null;
}
if (targetJavaVersion != null) {
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
.thenAcceptAsync(downloadedJavaVersion -> {
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
LOG.log(Level.WARNING, "Failed to download java", throwable);
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
return null;
});
}
} else {
MessageDialogPane dialog = new MessageDialogPane(
i18n("launch.advice.require_newer_java_version",
gameVersion.toString(),
version.getJavaVersion().getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION);
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
JFXButton linkButton = new JFXButton(i18n("download.external_link"));
linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/"));
linkButton.getStyleClass().add("dialog-accept");
dialog.addButton(linkButton);
setting.setJava(null);
setting.setDefaultJavaPath(null);
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
}
JFXButton yesButton = new JFXButton(i18n("button.ok"));
yesButton.setOnAction(event -> {
downloadJava(version.getJavaVersion(), profile)
.thenAcceptAsync(x -> {
try {
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
newAcceptableJava.ifPresent(setting::setJavaVersion);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Cannot list javas", e);
return Task.fromCompletableFuture(future);
} else {
return Task.completed(javaVersion);
}
}).thenComposeAsync(javaVersion -> {
return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version)));
}).thenComposeAsync(Schedulers.javafx(), javaVersions -> {
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
JavaVersionConstraint violatedMandatoryConstraint = null;
JavaVersionConstraint violatedSuggestedConstraint = null;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, javaVersion)) {
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
violatedMandatoryConstraint = constraint;
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
violatedSuggestedConstraint = constraint;
}
}
}
}
boolean suggested = false;
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(javaVersion);
Runnable breakAction = () -> {
future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
};
if (violatedMandatoryConstraint != null) {
if (suggestedJavaVersion != null) {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJavaVersion);
}, breakAction);
return Task.fromCompletableFuture(future);
} else {
switch (violatedMandatoryConstraint) {
case GAME_JSON:
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
.thenAcceptAsync(downloadedJavaVersion -> {
setting.setJavaVersion(downloadedJavaVersion);
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
LOG.log(Level.WARNING, "Failed to download java", throwable);
breakAction.run();
return null;
});
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_16:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
}, breakAction);
return null;
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
case VANILLA_LINUX_JAVA_8:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
} else {
break;
}
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
// Windows on ARM introduced translation support for x86-64 after 10.0.21277.
|| (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)) {
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
}
}, Platform::runLater).thenAccept(x -> onAccept.run());
});
yesButton.getStyleClass().add("dialog-accept");
dialog.addButton(yesButton);
JFXButton noButton = new JFXButton(i18n("button.cancel"));
noButton.getStyleClass().add("dialog-cancel");
dialog.addButton(noButton);
dialog.setCancelButton(noButton);
Controllers.dialog(dialog);
flag = true;
}
Controllers.dialog(i18n("launch.advice.vanilla_x86"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
} else {
break;
}
case LAUNCH_WRAPPER:
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
}
}
}
}
// Game later than 1.17 requires Java 16.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_16 && gameVersion.compareTo(VersionNumber.asVersion("1.17")) >= 0) {
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_16)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} else {
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
FXUtils.openLink("https://adoptopenjdk.net/");
}, null);
if (Architecture.SYSTEM_ARCH == Architecture.X86_64
&& javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.warning"), MessageType.ERROR, continueAction);
suggested = true;
}
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"), MessageType.ERROR, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept);
// 32-bit JVM cannot make use of too much memory.
if (javaVersion.getBits() == Bits.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), continueAction, breakAction);
suggested = true;
}
if (!suggested && violatedSuggestedConstraint != null) {
suggested = true;
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.warning"), MessageType.WARNING, continueAction);
return null;
case MODDED_JAVA_8:
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case VANILLA_JAVA_8_51:
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
}
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
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
&& 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());
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageType.INFORMATION, onAccept);
flag = true;
} else {
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, null);
flag = true;
}
}
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51");
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"), MessageType.WARNING, onAccept);
flag = true;
}
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
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.getPlatform())
.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.getPlatform())
.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"), MessageType.ERROR, onAccept);
flag = true;
// Cannot allocate too much memory exceeding free space.
if (!suggested && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, null);
suggested = 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) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), onAccept, null);
flag = true;
}
// Cannot allocate too much memory exceeding free space.
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), onAccept, null);
flag = true;
}
// Forge 2760~2773 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("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 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.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), onAccept, null);
flag = true;
// Forge 2760~2773 will crash game with LiteLoader.
if (!suggested) {
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 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.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, null);
suggested = true;
}
}
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
if (!flag) {
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), onAccept, null);
flag = true;
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
if (!suggested) {
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, null);
suggested = true;
}
}
}
if (!flag)
onAccept.run();
if (!suggested) {
future.complete(javaVersion);
}
return Task.fromCompletableFuture(future);
}).withStage("launch.state.java");
}
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<Void> future = new CompletableFuture<>();
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
link.setOnAction(e -> {
FXUtils.openLink("https://docs.microsoft.com/zh-cn/java/openjdk/download");
future.completeExceptionally(new CancellationException());
});
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version",
gameVersion,
javaVersion.getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.addAction(link)
.yesOrNo(() -> {
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
.thenAcceptAsync(downloadedJava -> {
future.complete(downloadedJava);
})
.exceptionally(throwable -> {
Throwable resolvedException = resolveException(throwable);
LOG.log(Level.WARNING, "Failed to download java", throwable);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
future.completeExceptionally(new CancellationException());
return null;
});
}, () -> {
future.completeExceptionally(new CancellationException());
}).build());
return future;
}
/**
* Directly start java downloading.
*
* @param javaVersion target Java version
* @param downloadProvider download provider
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
*/
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
profile.getDependency().getDownloadProvider()).executor(false);
executor.addTaskListener(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
TaskExecutor executor = JavaRepository.downloadJava(javaVersion, downloadProvider)
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
if (exception != null) {
future.completeExceptionally(exception);
} else {
future.complete(null);
future.complete(downloadedJava);
}
});
}
});
})
.executor(false);
javaDownloadingPane.setExecutor(executor, true);
Controllers.dialog(javaDownloadingPane);
executor.start();
return future;
}
@@ -582,8 +625,9 @@ public final class LauncherHelper {
class HMCLProcessListener implements ProcessListener {
private final HMCLGameRepository repository;
private final String version;
private final Version version;
private final Map<String, String> forbiddenTokens;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private LogWindow logWindow;
@@ -592,9 +636,10 @@ public final class LauncherHelper {
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CountDownLatch launchingLatch;
public HMCLProcessListener(HMCLGameRepository repository, String version, AuthInfo authInfo, CountDownLatch launchingLatch, boolean detectWindow) {
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
this.version = version;
this.launchOptions = launchOptions;
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
@@ -618,6 +663,11 @@ public final class LauncherHelper {
LOG.info("Launched process: " + command);
String classpath = process.getClasspath();
if (classpath != null) {
LOG.info("Process ClassPath: " + classpath);
}
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
@@ -703,43 +753,8 @@ public final class LauncherHelper {
if (!lwjgl) finishLaunch();
if (exitType != ExitType.NORMAL) {
repository.markVersionLaunchedAbnormally(version);
Platform.runLater(() -> {
if (logWindow == null) {
logWindow = new LogWindow();
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
}
switch (exitType) {
case JVM_ERROR:
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
break;
}
logWindow.showGameCrashReport(logs -> {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
LogExporter.exportLogs(logFile, repository, version, logs, new CommandBuilder().addAll(process.getCommands()).toString())
.thenRunAsync(() -> {
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
}, Schedulers.javafx())
.exceptionally(e -> {
LOG.log(Level.WARNING, "Failed to export game crash info", e);
return null;
});
});
});
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}
checkExit();
@@ -748,6 +763,7 @@ public final class LauncherHelper {
}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
@@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import java.nio.file.Path;
public class ManuallyCreatedModpackException extends Exception {
private final Path path;
public ManuallyCreatedModpackException(Path path) {
this.path = path;
}
public Path getPath() {
return path;
}
}
@@ -0,0 +1,61 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Profile profile;
private final Path zipFile;
private final Charset charset;
private final String name;
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
this.profile = profile;
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
}
@Override
public void execute() throws Exception {
Path subdirectory;
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
}
Path dest = Paths.get("externalgames").resolve(name);
setResult(dest);
new Unzipper(zipFile, dest)
.setSubDirectory(subdirectory.toString())
.setTerminateIfSubDirectoryNotExists()
.setEncoding(charset)
.unzip();
}
}
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -38,54 +39,97 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ZipFileSystem;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.toIterable;
public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
ignored.printStackTrace();
// ignore it, not a valid MCBBS modpack.
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
try {
return McbbsModpackManifest.readManifest(zfs, charset);
} catch (Exception ignored) {
// ignore it, not a valid MCBBS modpack.
}
try {
return CurseManifest.readCurseForgeModpackManifest(zfs, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(zfs, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(zfs, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
}
} catch (IOException ignored) {
}
try {
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
throw new ManuallyCreatedModpackException(file);
} catch (IOException e) {
// ignore it
}
throw new UnsupportedModpackException(file.toString());
}
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
Path root = fs.getPath("/");
if (isMinecraftDirectory(root)) return root;
try (Stream<Path> firstLayer = Files.list(root)) {
for (Path dir : toIterable(firstLayer)) {
if (isMinecraftDirectory(dir)) return dir;
try (Stream<Path> secondLayer = Files.list(dir)) {
for (Path subdir : toIterable(secondLayer)) {
if (isMinecraftDirectory(subdir)) return subdir;
}
} catch (IOException ignored) {
}
}
} catch (IOException ignored) {
}
throw new UnsupportedModpackException(modpackName);
}
private static boolean isMinecraftDirectory(Path path) {
return Files.isDirectory(path.resolve("versions")) &&
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
@@ -109,7 +153,7 @@ public final class ModpackHelper {
throw new UnsupportedModpackException();
}
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -129,10 +173,29 @@ public final class ModpackHelper {
};
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
}
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static boolean isExternalGameNameConflicts(String name) {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(name, location.toFile());
newProfile.setUseRelativePath(true);
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -151,36 +214,30 @@ public final class ModpackHelper {
}
};
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name));
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
default:
throw new UnsupportedModpackException();
}
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
switch (configuration.getType()) {
@@ -19,15 +19,19 @@ package org.jackhuang.hmcl.game;
import fi.iki.elonen.NanoHTTPD;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -37,13 +41,15 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MicrosoftAuthenticationServer extends NanoHTTPD implements MicrosoftService.OAuthSession {
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
public static String lastlyOpenedURL;
private MicrosoftAuthenticationServer(int port) {
private String idToken;
private OAuthServer(int port) {
super(port);
this.port = port;
@@ -59,22 +65,47 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
return future.get();
}
@Override
public String getIdToken() {
return idToken;
}
@Override
public Response serve(IHTTPSession session) {
if (session.getMethod() != Method.GET || !"/auth-response".equals(session.getUri())) {
if (!"/auth-response".equals(session.getUri())) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
Map<String, String> query = mapOf(NetworkUtils.parseQuery(session.getQueryParameterString()));
if (session.getMethod() == Method.POST) {
Map<String, String> files = new HashMap<>();
try {
session.parseBody(files);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read post data", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
}
} else if (session.getMethod() == Method.GET) {
// do nothing
} else {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
Logging.LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(MicrosoftAuthenticationServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
@@ -90,15 +121,21 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
public static class Factory implements MicrosoftService.OAuthCallback {
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
@Override
public MicrosoftService.OAuthSession startServer() throws IOException {
public OAuth.Session startServer() throws IOException, AuthenticationException {
if (StringUtils.isBlank(getClientId())) {
throw new MicrosoftAuthenticationNotSupportedException();
}
IOException exception = null;
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
try {
MicrosoftAuthenticationServer server = new MicrosoftAuthenticationServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
OAuthServer server = new OAuthServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
return server;
} catch (IOException e) {
exception = e;
@@ -107,10 +144,17 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
throw exception;
}
@Override
public void grantDeviceCode(String userCode, String verificationURI) {
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
}
@Override
public void openBrowser(String url) throws IOException {
lastlyOpenedURL = url;
FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override
@@ -126,4 +170,39 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
}
}
public static class GrantDeviceCodeEvent extends Event {
private final String userCode;
private final String verificationUri;
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
super(source);
this.userCode = userCode;
this.verificationUri = verificationUri;
}
public String getUserCode() {
return userCode;
}
public String getVerificationUri() {
return verificationUri;
}
}
public static class OpenBrowserEvent extends Event {
private final String url;
public OpenBrowserEvent(Object source, String url) {
super(source);
this.url = url;
}
public String getUrl() {
return url;
}
}
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
}
}
@@ -24,13 +24,7 @@ import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterDeletedException;
import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.*;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
@@ -40,7 +34,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
@@ -81,10 +75,12 @@ public final class Accounts {
}
}
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(new MicrosoftAuthenticationServer.Factory()));
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====
@@ -340,14 +336,21 @@ public final class Accounts {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials"))
if (remoteMessage.contains("Invalid credentials")) {
return i18n("account.failed.invalid_credentials");
else if (remoteMessage.contains("Invalid token"))
} else if (remoteMessage.contains("Invalid token")) {
return i18n("account.failed.invalid_token");
else if (remoteMessage.contains("Invalid username or password"))
} else if (remoteMessage.contains("Invalid username or password")) {
return i18n("account.failed.invalid_password");
else
} else {
return remoteMessage;
}
} else if ("ResourceException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("The requested resource is no longer available")) {
return i18n("account.failed.migration");
} else {
return remoteMessage;
}
}
return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) {
@@ -371,6 +374,10 @@ public final class Accounts {
return i18n("account.methods.microsoft.error.no_character");
} else if (exception instanceof MicrosoftService.NoXuiException) {
return i18n("account.methods.microsoft.error.add_family_probably");
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
return i18n("account.methods.microsoft.snapshot");
} else if (exception instanceof OAuthAccount.WrongAccountException) {
return i18n("account.failed.wrong_account");
} else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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
@@ -34,7 +34,6 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
@@ -83,6 +82,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("bgpath")
private StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("bgurl")
private StringProperty backgroundImageUrl = new SimpleStringProperty();
@SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@@ -149,15 +151,18 @@ public final class Config implements Cloneable, Observable {
@SerializedName("fontSize")
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@SerializedName("launcherFontFamily")
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -239,6 +244,18 @@ public final class Config implements Cloneable, Observable {
return backgroundImage;
}
public String getBackgroundImageUrl() {
return backgroundImageUrl.get();
}
public StringProperty backgroundImageUrlProperty() {
return backgroundImageUrl;
}
public void setBackgroundImageUrl(String backgroundImageUrl) {
this.backgroundImageUrl.set(backgroundImageUrl);
}
public EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}
@@ -487,6 +504,18 @@ public final class Config implements Cloneable, Observable {
return fontSize;
}
public String getLauncherFontFamily() {
return launcherFontFamily.get();
}
public StringProperty launcherFontFamilyProperty() {
return launcherFontFamily;
}
public void setLauncherFontFamily(String launcherFontFamily) {
this.launcherFontFamily.set(launcherFontFamily);
}
public int getLogLines() {
return logLines.get();
}
@@ -503,18 +532,6 @@ public final class Config implements Cloneable, Observable {
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();
}
@@ -550,4 +567,16 @@ public final class Config implements Cloneable, Observable {
public StringProperty preferredLoginTypeProperty() {
return preferredLoginType;
}
public boolean isTitleTransparent() {
return titleTransparent.get();
}
public BooleanProperty titleTransparentProperty() {
return titleTransparent;
}
public void setTitleTransparent(boolean titleTransparent) {
this.titleTransparent.set(titleTransparent);
}
}
@@ -19,15 +19,14 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.*;
import java.util.Map;
import java.util.logging.Level;
@@ -36,13 +35,16 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
public final class ConfigHolder {
private ConfigHolder() {}
private ConfigHolder() {
}
public static final String CONFIG_FILENAME = "hmcl.json";
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("config.json");
private static Path configLocation;
private static Config configInstance;
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
public static Config config() {
@@ -52,6 +54,13 @@ public final class ConfigHolder {
return configInstance;
}
public static GlobalConfig globalConfig() {
if (globalConfigInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return globalConfigInstance;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
@@ -68,6 +77,9 @@ public final class ConfigHolder {
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
Settings.init();
if (newlyCreated) {
@@ -84,9 +96,16 @@ public final class ConfigHolder {
}
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");
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
&& configLocation.toFile().canWrite()) {
// There are some serious problems with the implementation of Samba or OpenJDK
throw new SambaException();
} else {
// the config cannot be saved
// throw up the error now to prevent further data loss
throw new IOException("Config at " + configLocation + " is not writable");
}
}
}
@@ -95,7 +114,7 @@ public final class ConfigHolder {
try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath();
if (Files.isRegularFile(jarPath)) {
if (Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
@@ -145,7 +164,7 @@ public final class ConfigHolder {
return new Config();
}
private static InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
private static final InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToConfig(content);
} catch (IOException e) {
@@ -167,4 +186,48 @@ public final class ConfigHolder {
private static void saveConfigSync() throws IOException {
writeToConfig(configInstance.toJson());
}
// Global Config
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
}
}
LOG.info("Creating an empty global config");
return new GlobalConfig();
}
private static final InvocationDispatcher<String> globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToGlobalConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
}
});
private static void writeToGlobalConfig(String content) throws IOException {
LOG.info("Saving global config");
synchronized (GLOBAL_CONFIG_PATH) {
Files.write(GLOBAL_CONFIG_PATH, content.getBytes(UTF_8));
}
}
static void markGlobalConfigDirty() {
globalConfigWriter.accept(globalConfigInstance.toJson());
}
private static void saveGlobalConfigSync() throws IOException {
writeToConfig(globalConfigInstance.toJson());
}
}
@@ -26,11 +26,14 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -135,7 +138,7 @@ public final class DownloadProviders {
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
}
public static String localizeErrorMessage(Exception exception) {
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
@@ -147,9 +150,19 @@ public final class DownloadProviders {
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", url);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
} else if (exception instanceof CancellationException) {
return i18n("message.cancelled");
}
return StringUtils.getStackTrace(exception);
}
@@ -19,5 +19,8 @@ package org.jackhuang.hmcl.setting;
public enum EnumBackgroundImage {
DEFAULT,
CUSTOM
CUSTOM,
CLASSIC,
NETWORK,
TRANSLUCENT
}
@@ -0,0 +1,198 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
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.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Type;
import java.net.Proxy;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public class GlobalConfig implements Cloneable, Observable {
private static final Gson CONFIG_GSON = new GsonBuilder()
.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(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();
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) {
return null;
}
GlobalConfig instance = new GlobalConfig();
PropertyUtils.copyProperties(loaded, instance);
instance.unknownFields.putAll(loaded.unknownFields);
return instance;
}
private IntegerProperty agreementVersion = new SimpleIntegerProperty();
private StringProperty multiplayerToken = new SimpleStringProperty();
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
private final Map<String, Object> unknownFields = new HashMap<>();
private transient ObservableHelper helper = new ObservableHelper(this);
public GlobalConfig() {
PropertyUtils.attachListener(this, helper);
}
@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 GlobalConfig clone() {
return fromJson(this.toJson());
}
public int getAgreementVersion() {
return agreementVersion.get();
}
public IntegerProperty agreementVersionProperty() {
return agreementVersion;
}
public void setAgreementVersion(int agreementVersion) {
this.agreementVersion.set(agreementVersion);
}
public boolean isMultiplayerRelay() {
return multiplayerRelay.get();
}
public BooleanProperty multiplayerRelayProperty() {
return multiplayerRelay;
}
public void setMultiplayerRelay(boolean multiplayerRelay) {
this.multiplayerRelay.set(multiplayerRelay);
}
public int getMultiplayerAgreementVersion() {
return multiplayerAgreementVersion.get();
}
public IntegerProperty multiplayerAgreementVersionProperty() {
return multiplayerAgreementVersion;
}
public void setMultiplayerAgreementVersion(int multiplayerAgreementVersion) {
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
}
public String getMultiplayerToken() {
return multiplayerToken.get();
}
public StringProperty multiplayerTokenProperty() {
return multiplayerToken;
}
public void setMultiplayerToken(String multiplayerToken) {
this.multiplayerToken.set(multiplayerToken);
}
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"multiplayerToken",
"multiplayerAgreementVersion"
));
@Override
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
}
return jsonObject;
}
@Override
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (!knownFields.contains(entry.getKey())) {
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
}
}
return config;
}
}
}
@@ -0,0 +1,164 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.SerializedName;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class HMCLAccounts {
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
private HMCLAccounts() {
}
public static HMCLAccount getAccount() {
return account.get();
}
public static ObjectProperty<HMCLAccount> accountProperty() {
return account;
}
public static void setAccount(HMCLAccount account) {
HMCLAccounts.account.set(account);
}
public static Task<Void> login() {
String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID());
String scope = "openid offline_access";
return Task.supplyAsync(() -> {
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
mapOf(
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
pair("response_type", "id_token code"),
pair("response_mode", "form_post"),
pair("scope", scope),
pair("redirect_uri", session.getRedirectURI()),
pair("nonce", nonce)
)));
String code = session.waitFor();
// Authorization Code -> Token
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
.getString();
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
OAuth.AuthorizationResponse.class);
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
.header("Token-Type", response.tokenType)
.header("Access-Token", response.accessToken)
.header("Authorization-Provider", "microsoft")
.authorization("Bearer", session.getIdToken())
.getJson(HMCLAccountProfile.class);
return new HMCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken);
}).thenAcceptAsync(Schedulers.javafx(), account -> {
setAccount(account);
});
}
public static class HMCLAccount implements HttpRequest.Authorization {
private final String provider;
private final String nickname;
private final String email;
private final String role;
private final String idToken;
private final String tokenType;
private final String accessToken;
private final String refreshToken;
public HMCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) {
this.provider = provider;
this.nickname = nickname;
this.email = email;
this.role = role;
this.idToken = idToken;
this.tokenType = tokenType;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public String getProvider() {
return provider;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getRole() {
return role;
}
public String getIdToken() {
return idToken;
}
@Override
public String getTokenType() {
return tokenType;
}
@Override
public String getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
}
private static class HMCLAccountProfile {
@SerializedName("ID")
String id;
@SerializedName("Provider")
String provider;
@SerializedName("Email")
String email;
@SerializedName("NickName")
String nickname;
@SerializedName("Role")
String role;
}
}
@@ -0,0 +1,18 @@
package org.jackhuang.hmcl.setting;
public final class SambaException extends RuntimeException {
public SambaException() {
}
public SambaException(String message) {
super(message);
}
public SambaException(String message, Throwable cause) {
super(message, cause);
}
public SambaException(Throwable cause) {
super(cause);
}
}
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
@@ -55,7 +56,7 @@ public class Theme {
Theme(String name, String color) {
this.name = name;
this.color = color;
this.color = Objects.requireNonNull(color);
this.paint = Color.web(color);
}
@@ -79,9 +80,18 @@ public class Theme {
return isLight() ? Color.BLACK : Color.WHITE;
}
public String[] getStylesheets() {
public String[] getStylesheets(String overrideFontFamily) {
Color textFill = getForegroundColor();
String fontFamily;
if (System.getProperty("hmcl.font.override") != null) {
fontFamily = System.getProperty("hmcl.font.override");
} else if (overrideFontFamily != null) {
fontFamily = overrideFontFamily;
} else {
fontFamily = null;
}
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
@@ -93,7 +103,7 @@ public class Theme {
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", Optional.ofNullable(System.getProperty("hmcl.font.override")).map(fontFamily -> "-fx-font-family: " + fontFamily + ";").orElse("")));
.replace("%font%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse("")));
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
@@ -21,14 +21,16 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.IOException;
import java.lang.reflect.Type;
@@ -36,8 +38,11 @@ import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
/**
*
* @author huangyuhui
@@ -105,6 +110,15 @@ public final class VersionSetting implements Cloneable {
setDefaultJavaPath(null);
}
public boolean isJavaAutoSelected() {
return "Auto".equals(getJava());
}
public void setJavaAutoSelected() {
setJava("Auto");
setDefaultJavaPath(null);
}
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
/**
@@ -115,6 +129,10 @@ public final class VersionSetting implements Cloneable {
return defaultJavaPathProperty.get();
}
public StringProperty defaultJavaPathPropertyProperty() {
return defaultJavaPathProperty;
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
@@ -269,6 +287,24 @@ public final class VersionSetting implements Cloneable {
preLaunchCommandProperty.set(preLaunchCommand);
}
private final StringProperty postExitCommand = new SimpleStringProperty(this, "postExitCommand", "");
public StringProperty postExitCommandProperty() {
return postExitCommand;
}
/**
* The command that will be executed after game exits.
* Operating system relevant.
*/
public String getPostExitCommand() {
return postExitCommand.get();
}
public void setPostExitCommand(String postExitCommand) {
this.postExitCommand.set(postExitCommand);
}
// options
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
@@ -553,38 +589,50 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public JavaVersion getJavaVersion() throws InterruptedException {
return getJavaVersion(true);
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version) {
return getJavaVersion(gameVersion, version, true);
}
public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
// TODO: lazy initialization may result in UI suspension.
if (StringUtils.isBlank(getJava()))
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if (isUsesCustomJavaDir()) {
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
}
}).thenSupplyAsync(() -> {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform());
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
if ("Default".equals(getJava())) {
return JavaVersion.fromCurrentEnvironment();
} else if (isJavaAutoSelected()) {
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
} else if (isUsesCustomJavaDir()) {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
runInFX(() -> {
setJava("Default");
});
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
} catch (InterruptedException e) {
throw new CancellationException();
}
} else if (StringUtils.isNotBlank(getJava())) {
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 matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
});
}
public void setJavaVersion(JavaVersion java) {
@@ -602,6 +650,7 @@ public final class VersionSetting implements Cloneable {
minMemoryProperty.addListener(listener);
autoMemory.addListener(listener);
preLaunchCommandProperty.addListener(listener);
postExitCommand.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
@@ -636,6 +685,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setMinMemory(getMinMemory());
versionSetting.setAutoMemory(isAutoMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setPostExitCommand(getPostExitCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setNoJVMArgs(isNoJVMArgs());
@@ -673,6 +723,7 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("height", src.getHeight());
obj.addProperty("javaDir", src.getJavaDir());
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
obj.addProperty("postExitCommand", src.getPostExitCommand());
obj.addProperty("serverIp", src.getServerIp());
obj.addProperty("java", src.getJava());
obj.addProperty("wrapper", src.getWrapper());
@@ -17,11 +17,14 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
@@ -29,19 +32,14 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.DownloadPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
@@ -49,9 +47,7 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
@@ -64,6 +60,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -86,13 +83,6 @@ public final class Controllers {
});
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<DownloadListPage> modDownloadListPage = new Lazy<>(() -> {
return new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
{
state.set(State.fromTitle(i18n("modpack.download")));
}
};
});
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
@@ -130,11 +120,6 @@ public final class Controllers {
return rootPage.get();
}
// FXThread
public static DownloadListPage getModpackDownloadListPage() {
return modDownloadListPage.get();
}
// FXThread
public static MultiplayerPage getMultiplayerPage() {
return multiplayerPage.get();
@@ -195,12 +180,33 @@ public final class Controllers {
stage.setMinWidth(800 + 2 + 16); // bg width + border width*2 + shadow width*2
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
scene.getStylesheets().setAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.FULL_TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
agreementPane.setBody(new Label(i18n("launcher.agreement.hint")));
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
agreementLink.setOnAction(e -> FXUtils.openLink(Metadata.EULA_URL));
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
yesButton.getStyleClass().add("dialog-accept");
yesButton.setOnAction(e -> {
globalConfig().setAgreementVersion(1);
agreementPane.fireEvent(new DialogCloseEvent());
});
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
noButton.getStyleClass().add("dialog-cancel");
noButton.setOnAction(e -> {
System.exit(1);
});
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}
}
public static void dialog(Region content) {
@@ -213,7 +219,7 @@ public final class Controllers {
}
public static void dialog(String text, String title) {
dialog(text, title, MessageType.INFORMATION);
dialog(text, title, MessageType.INFO);
}
public static void dialog(String text, String title, MessageType type) {
@@ -221,7 +227,7 @@ public final class Controllers {
}
public static void dialog(String text, String title, MessageType type, Runnable ok) {
dialog(MessageDialogPane.ok(text, title, type, ok));
dialog(new MessageDialogPane.Builder(text, title, type).ok(ok).build());
}
public static void confirm(String text, String title, Runnable yes, Runnable no) {
@@ -229,15 +235,15 @@ public final class Controllers {
}
public static void confirm(String text, String title, MessageType type, Runnable yes, Runnable no) {
dialog(MessageDialogPane.yesOrNo(text, title, type, yes, no));
dialog(new MessageDialogPane.Builder(text, title, type).yesOrNo(yes, no).build());
}
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton) {
dialog(MessageDialogPane.actionOrCancel(text, title, type, actionButton, null));
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, null).build());
}
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
dialog(MessageDialogPane.actionOrCancel(text, title, type, actionButton, cancel));
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
@@ -268,14 +274,35 @@ public final class Controllers {
return pane;
}
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, Consumer<Region> onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setExecutor(executor);
dialog(pane);
executor.start();
return pane;
}
public static void navigate(Node node) {
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
decorator.navigate(node);
}
public static void showToast(String content) {
decorator.showToast(content);
}
public static void onHyperlinkAction(String href) {
if (href.startsWith("hmcl://")) {
if ("hmcl://settings/feedback".equals(href)) {
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
}
} else {
FXUtils.openLink(href);
}
}
public static boolean isStopped() {
return decorator == null;
}
@@ -285,7 +312,6 @@ public final class Controllers {
versionPage = null;
gameListPage = null;
settingsPage = null;
modDownloadListPage = null;
decorator = null;
stage = null;
scene = null;
@@ -17,11 +17,9 @@
*/
package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
import java.util.Optional;
import java.util.concurrent.CancellationException;
@@ -35,11 +33,23 @@ public final class DialogController {
}
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
if (account instanceof ClassicAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
ClassicAccountLoginDialog pane = new ClassicAccountLoginDialog((ClassicAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
} else if (account instanceof OAuthAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
@@ -39,13 +39,18 @@ import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -53,9 +58,17 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
@@ -63,6 +76,8 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BooleanSupplier;
@@ -103,9 +118,9 @@ public final class FXUtils {
value.addListener((a, b, c) -> consumer.accept(c));
}
public static <T> WeakChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
WeakChangeListener<T> listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c));
value.addListener(listener);
public static <T> ChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
ChangeListener<T> listener = (a, b, c) -> consumer.accept(c);
value.addListener(new WeakChangeListener<>(listener));
return listener;
}
@@ -114,7 +129,7 @@ public final class FXUtils {
onChange(value, consumer);
}
public static <T> WeakChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
public static <T> ChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
consumer.accept(value.getValue());
return onWeakChange(value, consumer);
}
@@ -319,7 +334,7 @@ public final class FXUtils {
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
animationKey = "FXUTILS.ANIMATION." + animationKey;
Object oldTimeline = node.getProperties().get(animationKey);
if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
if (timeline != null) timeline.play();
node.getProperties().put(animationKey, timeline);
}
@@ -621,6 +636,19 @@ public final class FXUtils {
};
}
public static ColumnConstraints getColumnFillingWidth() {
ColumnConstraints constraint = new ColumnConstraints();
constraint.setFillWidth(true);
return constraint;
}
public static ColumnConstraints getColumnHgrowing() {
ColumnConstraints constraint = new ColumnConstraints();
constraint.setFillWidth(true);
constraint.setHgrow(Priority.ALWAYS);
return constraint;
}
public static final Interpolator SINE = new Interpolator() {
@Override
protected double curve(double t) {
@@ -672,4 +700,50 @@ public final class FXUtils {
Controllers.showToast(i18n("message.copied"));
}
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader("<body>" + segment + "</body>")));
Element r = doc.getDocumentElement();
NodeList children = r.getChildNodes();
List<javafx.scene.Node> texts = new ArrayList<>();
for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node node = children.item(i);
if (node instanceof Element) {
Element element = (Element) node;
if ("a".equals(element.getTagName())) {
String href = element.getAttribute("href");
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
texts.add(hyperlink);
} else if ("b".equals(element.getTagName())) {
Text text = new Text(element.getTextContent());
text.getStyleClass().add("bold");
texts.add(text);
} else if ("br".equals(element.getTagName())) {
texts.add(new Text("\n"));
} else {
throw new IllegalArgumentException("unsupported tag " + element.getTagName());
}
} else {
texts.add(new Text(node.getTextContent()));
}
}
return texts;
} catch (SAXException | ParserConfigurationException | IOException e) {
LOG.log(Level.WARNING, "Failed to parse xml", e);
return Collections.singletonList(new Text(segment));
}
}
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
TextFlow tf = new TextFlow();
tf.getChildren().setAll(parseSegment(segment, hyperlinkAction));
return tf;
}
}
@@ -0,0 +1,368 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
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;
public class GameCrashWindow extends Stage {
private final Version version;
private final String memory;
private final String java;
private final LibraryAnalyzer analyzer;
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
private final ManagedProcess managedProcess;
private final DefaultGameRepository repository;
private final ProcessListener.ExitType exitType;
private final LaunchOptions launchOptions;
private final View view;
private final List<Pair<String, Log4jLevel>> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
this.version = version;
this.launchOptions = launchOptions;
this.logs = logs;
this.analyzer = LibraryAnalyzer.analyze(version);
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
? launchOptions.getJava().getVersion()
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
this.view = new View();
setScene(new Scene(view, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("game.crash.title"));
getIcons().add(newImage("/assets/img/icon.png"));
analyzeCrashReport();
}
private void analyzeCrashReport() {
loading.set(true);
CompletableFuture.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
Set<String> keywords = Collections.emptySet();
String crashReport = null;
try {
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read crash report", e);
}
if (crashReport == null) {
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
}
if (crashReport != null) {
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
}
return pair(
CrashReportAnalyzer.anaylze(rawLog),
keywords);
}).whenCompleteAsync((pair, exception) -> {
loading.set(false);
if (exception != null) {
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
reason.set(i18n("game.crash.reason.unknown"));
} else {
List<CrashReportAnalyzer.Result> results = pair.getKey();
Set<String> keywords = pair.getValue();
StringBuilder reasonText = new StringBuilder();
for (CrashReportAnalyzer.Result result : results) {
switch (result.getRule()) {
case TOO_OLD_JAVA:
reasonText.append(i18n("game.crash.reason.too_old_java",
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))))
.append("\n");
break;
case MOD_RESOLUTION_CONFLICT:
case MOD_RESOLUTION_MISSING:
case MOD_RESOLUTION_COLLECTION:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("sourcemod")),
parseFabricModId(result.getMatcher().group("destmod")),
parseFabricModId(result.getMatcher().group("destmod"))));
break;
case MOD_RESOLUTION_MISSING_MINECRAFT:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("mod")),
result.getMatcher().group("version")));
break;
case TWILIGHT_FOREST_OPTIFINE:
reasonText.append(i18n("game.crash.reason.mod", "OptiFine"));
break;
default:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
.toArray()))
.append("\n");
break;
}
}
if (results.isEmpty()) {
if (!keywords.isEmpty()) {
reason.set(i18n("game.crash.reason.stacktrace", String.join(", ", keywords)));
} else {
reason.set(i18n("game.crash.reason.unknown"));
}
feedbackLabel.setVisible(true);
} else {
reason.set(reasonText.toString());
}
}
}, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException);
}
private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?<modid>.*?) @ (?<version>.*?)}");
private String translateFabricModId(String modName) {
switch (modName) {
case "fabricloader":
return "Fabric";
case "fabric":
return "Fabric API";
case "minecraft":
return "Minecraft";
default:
return modName;
}
}
private String parseFabricModId(String modName) {
Matcher matcher = FABRIC_MOD_ID.matcher(modName);
if (matcher.find()) {
String modid = matcher.group("modid");
String version = matcher.group("version");
if ("[*]".equals(version)) {
return i18n("game.crash.reason.mod_resolution_mod_version.any", translateFabricModId(modid));
} else {
return i18n("game.crash.reason.mod_resolution_mod_version", translateFabricModId(modid), version);
}
}
return translateFabricModId(modName);
}
private void showLogWindow() {
LogWindow logWindow = new LogWindow();
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
logWindow.showNormal();
}
private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
CompletableFuture.supplyAsync(() ->
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.thenRunAsync(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
}, Schedulers.javafx())
.exceptionally(e -> {
LOG.log(Level.WARNING, "Failed to export game crash info", e);
return null;
});
}
private final class View extends VBox {
View() {
setStyle("-fx-background-color: white");
HBox titlePane = new HBox();
{
Label title = new Label();
HBox.setHgrow(title, Priority.ALWAYS);
switch (exitType) {
case JVM_ERROR:
title.setText(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
title.setText(i18n("launch.failed.exited_abnormally"));
break;
}
titlePane.setAlignment(Pos.CENTER);
titlePane.getStyleClass().addAll("jfx-tool-bar-second", "depth-1", "padding-8");
titlePane.getChildren().setAll(title);
}
HBox infoPane = new HBox(8);
{
infoPane.setPadding(new Insets(8));
infoPane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem version = new TwoLineListItem();
version.getStyleClass().setAll("two-line-item-second-large");
version.setTitle(i18n("archive.game_version"));
version.setSubtitle(GameCrashWindow.this.version.getId());
TwoLineListItem memory = new TwoLineListItem();
memory.getStyleClass().setAll("two-line-item-second-large");
memory.setTitle(i18n("settings.memory"));
memory.setSubtitle(GameCrashWindow.this.memory);
TwoLineListItem java = new TwoLineListItem();
java.getStyleClass().setAll("two-line-item-second-large");
java.setTitle("Java");
java.setSubtitle(GameCrashWindow.this.java);
TwoLineListItem os = new TwoLineListItem();
os.getStyleClass().setAll("two-line-item-second-large");
os.setTitle(i18n("system.operating_system"));
os.subtitleProperty().bind(GameCrashWindow.this.os);
TwoLineListItem arch = new TwoLineListItem();
arch.getStyleClass().setAll("two-line-item-second-large");
arch.setTitle(i18n("system.architecture"));
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
infoPane.getChildren().setAll(version, memory, java, os, arch);
}
HBox moddedPane = new HBox(8);
{
moddedPane.setPadding(new Insets(8));
moddedPane.setAlignment(Pos.CENTER_LEFT);
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
analyzer.getVersion(type).ifPresent(ver -> {
TwoLineListItem item = new TwoLineListItem();
item.getStyleClass().setAll("two-line-item-second-large");
item.setTitle(i18n("install.installer." + type.getPatchId()));
item.setSubtitle(ver);
moddedPane.getChildren().add(item);
});
}
}
VBox gameDirPane = new VBox(8);
{
TwoLineListItem gameDir = new TwoLineListItem();
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
TwoLineListItem javaDir = new TwoLineListItem();
javaDir.getStyleClass().setAll("two-line-item-second-large");
javaDir.setTitle(i18n("settings.game.java_directory"));
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
TwoLineListItem reason = new TwoLineListItem();
reason.getStyleClass().setAll("two-line-item-second-large", "wrap-text");
reason.setTitle(i18n("game.crash.reason"));
reason.subtitleProperty().bind(GameCrashWindow.this.reason);
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
gameDirPane.getChildren().setAll(gameDir, javaDir, reason);
}
HBox toolBar = new HBox();
{
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setButtonType(JFXButton.ButtonType.RAISED);
exportGameCrashInfoButton.getStyleClass().add("jfx-button-raised");
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
JFXButton logButton = new JFXButton(i18n("logwindow.title"));
logButton.setButtonType(JFXButton.ButtonType.RAISED);
logButton.getStyleClass().add("jfx-button-raised");
logButton.setOnMouseClicked(e -> showLogWindow());
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, feedbackLabel);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
}
}
}
@@ -18,13 +18,13 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
@@ -37,6 +37,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
@@ -153,24 +154,29 @@ public class InstallerItem extends Control {
super(control);
HBox hbox = new HBox();
getChildren().setAll(hbox);
JFXDepthManager.setDepth(hbox, 1);
hbox.getStyleClass().add("card");
hbox.getStyleClass().add("md-list-cell");
hbox.setPadding(new Insets(8));
RipplerContainer container = new RipplerContainer(hbox);
getChildren().setAll(container);
hbox.setAlignment(Pos.CENTER_LEFT);
if (control.imageUrl != null) {
hbox.getChildren().add(FXUtils.limitingSize(new ImageView(new Image(control.imageUrl, 32, 32, true, true)), 32, 32));
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
hbox.getChildren().add(node);
}
Label nameLabel = new Label();
nameLabel.setMouseTransparent(true);
hbox.getChildren().add(nameLabel);
nameLabel.setPrefWidth(80);
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
Label label = new Label();
label.setMouseTransparent(true);
hbox.getChildren().add(label);
label.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(label, Priority.ALWAYS);
@@ -213,11 +219,11 @@ public class InstallerItem extends Control {
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
if (clickable) {
hbox.onMouseClickedProperty().bind(control.action);
container.onMouseClickedProperty().bind(control.action);
hbox.setCursor(Cursor.HAND);
} else {
hbox.onMouseClickedProperty().unbind();
hbox.onMouseClickedProperty().set(null);
container.onMouseClickedProperty().unbind();
container.onMouseClickedProperty().set(null);
hbox.setCursor(Cursor.DEFAULT);
}
});
@@ -17,17 +17,19 @@
*/
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.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.control.Control;
import static org.jackhuang.hmcl.ui.construct.SpinnerPane.FAILED_ACTION;
public class ListPageBase<T> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final StringProperty failedReason = new SimpleStringProperty(this, "failed");
public ObservableList<T> getItems() {
return items.get();
@@ -52,4 +54,35 @@ public class ListPageBase<T> extends Control {
public BooleanProperty loadingProperty() {
return loading;
}
public String getFailedReason() {
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
}
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
return onFailedAction;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
}
@@ -25,7 +25,7 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.WeakChangeListener;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -83,7 +83,7 @@ public final class LogWindow extends Stage {
}
};
private final LogWindowImpl impl = new LogWindowImpl();
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
@@ -91,7 +91,7 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
getIcons().add(newImage("/assets/img/icon.png"));
@@ -73,6 +73,12 @@ public final class SVG {
height);
}
public static Node cancel(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z",
fill, width, height);
}
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",
@@ -216,6 +222,10 @@ public final class SVG {
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
}
public static Node alertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16", fill, width, height);
}
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
@@ -486,4 +496,46 @@ public final class SVG {
"M7,10L12,15L17,10H7Z",
fill, width, height);
}
public static Node restore(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z",
fill, width, height);
}
public static Node bug(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z",
fill, width, height);
}
public static Node discord(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
fill, width, height);
}
public static Node lan(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z",
fill, width, height);
}
public static Node thumbUpOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z",
fill, width, height);
}
public static Node thumbDownOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z",
fill, width, height);
}
public static Node selectAll(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z",
fill, width, height);
}
}
@@ -19,17 +19,18 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import java.util.List;
@@ -40,43 +41,39 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
spinnerPane.getStyleClass().add("large-spinner-pane");
BorderPane root = new BorderPane();
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
StackPane.setMargin(root, new Insets(10));
List<Node> toolbarButtons = initializeToolbar(skinnable);
if (!toolbarButtons.isEmpty()) {
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(toolbarButtons);
root.setTop(toolbar);
root.getContent().add(toolbar);
}
{
ScrollPane scrollPane = new ScrollPane();
ComponentList.setVgrow(scrollPane, Priority.ALWAYS);
scrollPane.setFitToWidth(true);
VBox content = new VBox();
content.setSpacing(10);
content.setPadding(new Insets(10));
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
root.setCenter(scrollPane);
root.getContent().add(scrollPane);
}
FXUtils.onChangeAndOperate(skinnable.loadingProperty(), loading -> {
if (loading) {
spinnerPane.showSpinner();
} else {
spinnerPane.hideSpinner();
}
});
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
@@ -24,28 +24,28 @@ import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import java.util.logging.Level;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(Runnable updateRunnable) {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
{
String url = CHANGELOG_URL + config().getUpdateChannel().channelName + ".html";
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
try {
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
engine.load(CHANGELOG_URL + remoteVersion.getChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String viewURL = engine.getLoadWorker().getMessage().trim();
if (!viewURL.startsWith(CHANGELOG_URL)) {
@@ -42,7 +42,7 @@ public class WebStage extends Stage {
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
getIcons().add(newImage("/assets/img/icon.png"));
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
@@ -20,7 +20,10 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
@@ -46,6 +49,7 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@@ -54,8 +58,6 @@ import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import static java.util.Collections.emptySet;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Logging.LOG;
@@ -135,6 +137,8 @@ public class AccountListItem extends RadioButton {
} else {
return createBooleanBinding(() -> true);
}
} else if (account instanceof OfflineAccount) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
}
@@ -145,6 +149,10 @@ public class AccountListItem extends RadioButton {
*/
@Nullable
public Task<?> uploadSkin() {
if (account instanceof OfflineAccount) {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (!(account instanceof YggdrasilAccount)) {
return null;
}
@@ -27,9 +27,8 @@ import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
@@ -37,29 +36,30 @@ import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.net.URI;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
import java.net.URI;
public class AccountListPage extends ListPageBase<AccountListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage"), -1));
public class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
private final ObservableList<AccountListItem> items;
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ListProperty<AuthlibInjectorServer> authServers = new SimpleListProperty<>(this, "authServers", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
public AccountListPage() {
setItems(MappedObservableList.create(accounts, AccountListItem::new));
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
items = MappedObservableList.create(accounts, AccountListItem::new);
selectedAccount = createSelectedItemPropertyFor(items, Account.class);
}
public ObjectProperty<Account> selectedAccountProperty() {
@@ -84,100 +84,87 @@ public class AccountListPage extends ListPageBase<AccountListItem> implements De
return new AccountListPageSkin(this);
}
private static class AccountListPageSkin extends SkinBase<AccountListPage> {
private static class AccountListPageSkin extends DecoratorAnimatedPageSkin<AccountListPage> {
private final ObservableList<AdvancedListItem> authServerItems;
public AccountListPageSkin(AccountListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
{
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
VBox boxMethods = new VBox();
{
VBox boxItemList = new VBox();
boxItemList.getStyleClass().add("advanced-list-box-content");
boxMethods.getStyleClass().add("advanced-list-box-content");
boxMethods.getChildren().add(new ClassTitle(i18n("account.create")));
FXUtils.setLimitWidth(boxMethods, 200);
boxItemList.getChildren().add(new ClassTitle(i18n("account.create")));
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG::account));
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
boxMethods.getChildren().add(offlineItem);
{
VBox boxMethods = new VBox();
FXUtils.setLimitWidth(boxMethods, 200);
AdvancedListItem mojangItem = new AdvancedListItem();
mojangItem.getStyleClass().add("navigation-drawer-item");
mojangItem.setActionButtonVisible(false);
mojangItem.setTitle(i18n("account.methods.yggdrasil"));
mojangItem.setLeftGraphic(wrap(SVG::mojang));
mojangItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
boxMethods.getChildren().add(mojangItem);
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG.account(Theme.blackFillBinding(), 24, 24)));
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
boxMethods.getChildren().add(offlineItem);
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setActionButtonVisible(false);
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftGraphic(wrap(SVG::microsoft));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
AdvancedListItem mojangItem = new AdvancedListItem();
mojangItem.getStyleClass().add("navigation-drawer-item");
mojangItem.setActionButtonVisible(false);
mojangItem.setTitle(i18n("account.methods.yggdrasil"));
mojangItem.setLeftGraphic(wrap(SVG.mojang(Theme.blackFillBinding(), 24, 24)));
mojangItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
boxMethods.getChildren().add(mojangItem);
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG::server));
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setActionButtonVisible(false);
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftGraphic(wrap(SVG.microsoft(Theme.blackFillBinding(), 24, 24)));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG.server(Theme.blackFillBinding(), 24, 24)));
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
item.setRightGraphic(btnRemove);
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
FXUtils.installFastTooltip(item, tooltip);
return item;
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
e.consume();
});
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
boxMethods.getChildren().add(boxAuthServers);
btnRemove.getStyleClass().add("toggle-icon4");
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
item.setRightGraphic(btnRemove);
boxItemList.getChildren().add(new ScrollPane(boxMethods));
}
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
FXUtils.installFastTooltip(item, tooltip);
left.setCenter(boxItemList);
return item;
});
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
boxMethods.getChildren().add(boxAuthServers);
}
AdvancedListItem addAuthServerItem = new AdvancedListItem();
{
AdvancedListItem addAuthServerItem = new AdvancedListItem();
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
addAuthServerItem.setTitle(i18n("account.injector.add"));
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
addAuthServerItem.setActionButtonVisible(false);
addAuthServerItem.setLeftGraphic(wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
addAuthServerItem.setLeftGraphic(wrap(SVG::plusCircleOutline));
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
BorderPane.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
left.setBottom(addAuthServerItem);
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
}
root.setLeft(left);
ScrollPane scrollPane = new ScrollPane(boxMethods);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
setLeft(scrollPane, addAuthServerItem);
}
ScrollPane scrollPane = new ScrollPane();
@@ -189,15 +176,13 @@ public class AccountListPage extends ListPageBase<AccountListItem> implements De
list.setSpacing(10);
list.getStyleClass().add("card-list");
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
Bindings.bindContent(list.getChildren(), skinnable.items);
scrollPane.setContent(list);
JFXScrollPane.smoothScrolling(scrollPane);
root.setCenter(scrollPane);
setCenter(scrollPane);
}
getChildren().setAll(root);
}
}
}
@@ -1,89 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.setting.Accounts;
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;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AccountLoginPane extends StackPane {
private final Account oldAccount;
private final Consumer<AuthInfo> success;
private final Runnable failed;
@FXML private Label lblUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
public AccountLoginPane(Account oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;
FXUtils.loadFXML(this, "/assets/fxml/account-login.fxml");
lblUsername.setText(oldAccount.getUsername());
txtPassword.setOnAction(e -> onAccept());
onEscPressed(this, this::onCancel);
}
@FXML
private void onAccept() {
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.supplyAsync(() -> oldAccount.logInWithPassword(password))
.whenComplete(Schedulers.javafx(), authInfo -> {
success.accept(authInfo);
fireEvent(new DialogCloseEvent());
progressBar.setVisible(false);
}, e -> {
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
if (e instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblCreationWarning.setText(Accounts.localizeErrorMessage(e));
}
progressBar.setVisible(false);
}).start();
}
@FXML
private void onCancel() {
failed.run();
fireEvent(new DialogCloseEvent());
}
}
@@ -0,0 +1,146 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.ClassicAccount;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.setting.Accounts;
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 org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ClassicAccountLoginDialog extends StackPane {
private final ClassicAccount oldAccount;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final JFXPasswordField txtPassword;
private final Label lblCreationWarning = new Label();
private final JFXProgressBar progressBar;
public ClassicAccountLoginDialog(ClassicAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;
progressBar = new JFXProgressBar();
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
progressBar.setVisible(false);
JFXDialogLayout dialogLayout = new JFXDialogLayout();
{
dialogLayout.setHeading(new Label(i18n("login.enter_password")));
}
{
VBox body = new VBox(15);
body.setPadding(new Insets(15, 0, 0, 0));
Label usernameLabel = new Label(oldAccount.getUsername());
txtPassword = new JFXPasswordField();
txtPassword.setOnAction(e -> onAccept());
txtPassword.getValidators().add(new RequiredValidator());
txtPassword.setLabelFloat(true);
txtPassword.setPromptText(i18n("account.password"));
body.getChildren().setAll(usernameLabel, txtPassword);
if (oldAccount instanceof YggdrasilAccount && !(oldAccount instanceof AuthlibInjectorAccount)) {
HBox linkPane = new HBox(8);
body.getChildren().add(linkPane);
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
}
dialogLayout.setBody(body);
}
{
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(e -> onAccept());
acceptButton.getStyleClass().add("dialog-accept");
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> onCancel());
cancelButton.getStyleClass().add("dialog-cancel");
dialogLayout.setActions(lblCreationWarning, acceptButton, cancelButton);
}
getChildren().setAll(dialogLayout);
onEscPressed(this, this::onCancel);
}
private void onAccept() {
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.supplyAsync(() -> oldAccount.logInWithPassword(password))
.whenComplete(Schedulers.javafx(), authInfo -> {
success.accept(authInfo);
fireEvent(new DialogCloseEvent());
progressBar.setVisible(false);
}, e -> {
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
if (e instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblCreationWarning.setText(Accounts.localizeErrorMessage(e));
}
progressBar.setVisible(false);
}).start();
}
private void onCancel() {
failed.run();
fireEvent(new DialogCloseEvent());
}
}
@@ -23,7 +23,9 @@ import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -43,7 +45,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
@@ -53,6 +55,7 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
@@ -62,6 +65,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import static java.util.Collections.emptyList;
@@ -87,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private final Pane detailsContainer;
private final BooleanProperty logging = new SimpleBooleanProperty();
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
private TaskExecutor loginTask;
@@ -216,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
@@ -260,23 +267,33 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
logging
? i18n("account.methods.microsoft.manual")
: i18n("account.methods.microsoft.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
runInFX(() -> deviceCode.set(value));
}));
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
box.getChildren().setAll(profileLink, birthLink);
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(hintPane, box);
@@ -415,17 +432,32 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory instanceof YggdrasilAccountFactory) {
HBox box = new HBox();
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/en-us/articles/360050865492-JAVA-Account-Migration-FAQ"));
GridPane.setColumnSpan(box, 2);
box.getChildren().setAll(migrationLink);
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
box.getChildren().setAll(migrationLink, migrationHowLink, purchaseLink);
add(box, 0, rowIndex);
rowIndex++;
}
if (factory instanceof OfflineAccountFactory) {
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
HBox linkPane = new HBox(purchaseLink);
GridPane.setColumnSpan(linkPane, 2);
add(linkPane, 0, rowIndex);
rowIndex++;
HBox box = new HBox();
MenuUpDownButton advancedButton = new MenuUpDownButton();
box.getChildren().setAll(advancedButton);
@@ -504,7 +536,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory instanceof AuthlibInjectorAccountFactory) {
return getAuthServer();
} else if (factory instanceof OfflineAccountFactory) {
return txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
UUID uuid = txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
return new OfflineAccountFactory.AdditionalData(uuid, null);
} else {
return null;
}
@@ -0,0 +1,104 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.OAuthAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OAuthAccountLoginDialog extends DialogPane {
private final OAuthAccount account;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
private final WeakListenerHolder holder = new WeakListenerHolder();
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
this.account = account;
this.success = success;
this.failed = failed;
setTitle(i18n("account.login.refresh"));
VBox vbox = new VBox(8);
Label usernameLabel = new Label(account.getUsername());
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(usernameLabel, hintPane, box);
setBody(vbox);
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
}
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
FXUtils.runInFX(() -> {
deviceCode.set(event);
});
}
@Override
protected void onAccept() {
setLoading();
Task.supplyAsync(account::logInWhenCredentialsExpired)
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
if (exception == null) {
success.accept(authInfo);
onSuccess();
} else {
LOG.log(Level.INFO, "Failed to login when credentials expired: " + account, exception);
onFailure(Accounts.localizeErrorMessage(exception));
}
}).start();
}
@Override
protected void onCancel() {
failed.run();
super.onCancel();
}
}
@@ -0,0 +1,211 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import java.io.File;
import java.util.Arrays;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OfflineAccountSkinPane extends StackPane {
private final OfflineAccount account;
private final MultiFileItem<Skin.Type> skinItem = new MultiFileItem<>();
private final JFXTextField cslApiField = new JFXTextField();
private final JFXComboBox<TextureModel> modelCombobox = new JFXComboBox<>();
private final FileSelector skinSelector = new FileSelector();
private final FileSelector capeSelector = new FileSelector();
private final InvalidationListener skinBinding;
public OfflineAccountSkinPane(OfflineAccount account) {
this.account = account;
getStyleClass().add("skin-pane");
JFXDialogLayout layout = new JFXDialogLayout();
getChildren().setAll(layout);
layout.setHeading(new Label(i18n("account.skin")));
BorderPane pane = new BorderPane();
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 300, 300, true);
StackPane canvasPane = new StackPane(canvas);
canvasPane.setPrefWidth(300);
canvasPane.setPrefHeight(300);
pane.setCenter(canvas);
canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
canvas.enableRotation(.5);
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
if (e.getDragboard().hasFiles()) {
File file = e.getDragboard().getFiles().get(0);
if (file.getAbsolutePath().endsWith(".png"))
e.acceptTransferModes(TransferMode.COPY);
}
});
canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> {
if (e.isAccepted()) {
File skin = e.getDragboard().getFiles().get(0);
Platform.runLater(() -> {
skinSelector.setValue(skin.getAbsolutePath());
skinItem.setSelectedData(Skin.Type.LOCAL_FILE);
});
}
});
StackPane skinOptionPane = new StackPane();
skinOptionPane.setMaxWidth(300);
VBox optionPane = new VBox(skinItem, skinOptionPane);
pane.setRight(optionPane);
layout.setBody(pane);
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
cslApiField.setValidators(new URLValidator());
skinItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
new MultiFileItem.Option<>("Steve", Skin.Type.STEVE),
new MultiFileItem.Option<>("Alex", Skin.Type.ALEX),
new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE),
new MultiFileItem.Option<>("LittleSkin", Skin.Type.LITTLE_SKIN),
new MultiFileItem.Option<>(i18n("account.skin.type.csl_api"), Skin.Type.CUSTOM_SKIN_LOADER_API)
));
modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName)));
modelCombobox.getItems().setAll(TextureModel.STEVE, TextureModel.ALEX);
if (account.getSkin() == null) {
skinItem.setSelectedData(Skin.Type.DEFAULT);
modelCombobox.setValue(TextureModel.STEVE);
} else {
skinItem.setSelectedData(account.getSkin().getType());
cslApiField.setText(account.getSkin().getCslApi());
modelCombobox.setValue(account.getSkin().getTextureModel());
skinSelector.setValue(account.getSkin().getLocalSkinPath());
capeSelector.setValue(account.getSkin().getLocalCapePath());
}
skinBinding = FXUtils.observeWeak(() -> {
getSkin().load(account.getUsername())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.log(Level.WARNING, "Failed to load skin", exception);
Controllers.showToast(i18n("message.failed"));
} else {
if (result == null || result.getSkin() == null) {
canvas.updateSkin(getDefaultTexture(), isDefaultSlim());
return;
}
canvas.updateSkin(new Image(result.getSkin().getInputStream()), result.getModel() == TextureModel.ALEX);
}
}).start();
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(0, 0, 0, 10));
gridPane.setHgap(16);
gridPane.setVgap(8);
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
switch (selectedData) {
case DEFAULT:
case STEVE:
case ALEX:
break;
case LITTLE_SKIN:
HintPane hint = new HintPane(MessageDialogPane.MessageType.INFO);
hint.setText(i18n("account.skin.type.little_skin.hint"));
gridPane.addRow(0, hint);
break;
case LOCAL_FILE:
gridPane.addRow(0, new Label(i18n("account.skin.model")), modelCombobox);
gridPane.addRow(1, new Label(i18n("account.skin")), skinSelector);
gridPane.addRow(2, new Label(i18n("account.cape")), capeSelector);
break;
case CUSTOM_SKIN_LOADER_API:
gridPane.addRow(0, new Label(i18n("account.skin.type.csl_api.location")), cslApiField);
break;
}
skinOptionPane.getChildren().setAll(gridPane);
});
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.getStyleClass().add("dialog-accept");
acceptButton.setOnAction(e -> {
account.setSkin(getSkin());
fireEvent(new DialogCloseEvent());
});
JFXHyperlink littleSkinLink = new JFXHyperlink(i18n("account.skin.type.little_skin"));
littleSkinLink.setOnAction(e -> FXUtils.openLink("https://mcskin.littleservice.cn/"));
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
layout.setActions(littleSkinLink, acceptButton, cancelButton);
}
private Skin getSkin() {
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue());
}
private boolean isDefaultSlim() {
return TextureModel.detectUUID(account.getUUID()) == TextureModel.ALEX;
}
private Image getDefaultTexture() {
if (isDefaultSlim()) {
return SkinCanvas.ALEX;
} else {
return SkinCanvas.STEVE;
}
}
}
@@ -63,6 +63,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
updateContent(newView);
if (previousNode == EMPTY_PANE) {
setMouseTransparent(false);
getChildren().setAll(newView);
return;
}
transition.init(this);
// runLater or "init" will not work
@@ -95,5 +101,8 @@ public class TransitionPane extends StackPane implements AnimationHandler {
getChildren().setAll(previousNode, currentNode);
}
private final StackPane EMPTY_PANE = new StackPane();
private final EmptyPane EMPTY_PANE = new EmptyPane();
public static class EmptyPane extends StackPane {
}
}
@@ -19,37 +19,26 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
public class AdvancedListBox extends ScrollPane {
private final VBox container = new VBox();
public class AdvancedListBox extends VBox {
{
setContent(container);
FXUtils.smoothScrolling(this);
setFitToHeight(true);
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.getStyleClass().add("advanced-list-box-content");
getStyleClass().add("advanced-list-box-content");
}
public AdvancedListBox add(Node child) {
if (child instanceof Pane || child instanceof AdvancedListItem)
container.getChildren().add(child);
getChildren().add(child);
else {
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
container.getChildren().add(pane);
getChildren().add(pane);
}
return this;
}
@@ -64,27 +53,27 @@ public class AdvancedListBox extends ScrollPane {
public AdvancedListBox add(int index, Node child) {
if (child instanceof Pane || child instanceof AdvancedListItem)
container.getChildren().add(index, child);
getChildren().add(index, child);
else {
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
container.getChildren().add(index, pane);
getChildren().add(index, pane);
}
return this;
}
public AdvancedListBox remove(Node child) {
container.getChildren().remove(indexOf(child));
getChildren().remove(indexOf(child));
return this;
}
public int indexOf(Node child) {
if (child instanceof Pane) {
return container.getChildren().indexOf(child);
return getChildren().indexOf(child);
} else {
for (int i = 0; i < container.getChildren().size(); ++i) {
Node node = container.getChildren().get(i);
for (int i = 0; i < getChildren().size(); ++i) {
Node node = getChildren().get(i);
if (node instanceof StackPane) {
ObservableList<Node> list = ((StackPane) node).getChildren();
if (list.size() == 1 && list.get(0) == child)
@@ -99,11 +88,7 @@ public class AdvancedListBox extends ScrollPane {
return add(new ClassTitle(category));
}
public void setSpacing(double spacing) {
container.setSpacing(spacing);
}
public void clear() {
container.getChildren().clear();
getChildren().clear();
}
}
@@ -41,7 +41,6 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
RipplerContainer container = new RipplerContainer(root);
HBox left = new HBox();
left.setAlignment(Pos.CENTER_LEFT);
left.setMouseTransparent(true);
TwoLineListItem item = new TwoLineListItem();
@@ -0,0 +1,37 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
public class AnnouncementCard extends VBox {
public AnnouncementCard(String title, String content) {
TextFlow tf = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
Label label = new Label(title);
label.getStyleClass().add("title");
getChildren().setAll(label, tf);
setSpacing(14);
getStyleClass().addAll("card", "announcement");
}
}
@@ -109,6 +109,7 @@ public class ComponentList extends Control {
public void onExpand() {
if (!expanded && lazyInitializer != null) {
lazyInitializer.accept(this);
setNeedsLayout(true);
}
expanded = true;
@@ -136,6 +137,9 @@ public class ComponentList extends Control {
if (node.getProperties().containsKey("ComponentList.vgrow")) {
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
}
if (node.getProperties().containsKey("ComponentList.noPadding")) {
cell.getStyleClass().add("no-padding");
}
return cell;
});
@@ -22,16 +22,14 @@ import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
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.layout.*;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
@@ -78,7 +76,9 @@ class ComponentListCell extends StackPane {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
BorderPane groupNode = new BorderPane();
getStyleClass().add("no-padding");
VBox groupNode = new VBox();
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
JFXButton expandButton = new JFXButton();
@@ -86,6 +86,7 @@ class ComponentListCell extends StackPane {
expandButton.getStyleClass().add("options-list-item-expand-button");
VBox labelVBox = new VBox();
labelVBox.setMouseTransparent(true);
labelVBox.setAlignment(Pos.CENTER_LEFT);
boolean overrideHeaderLeft = false;
@@ -110,59 +111,72 @@ class ComponentListCell extends StackPane {
}
}
groupNode.setLeft(labelVBox);
HBox right = new HBox();
right.setSpacing(16);
right.setAlignment(Pos.CENTER_RIGHT);
HBox header = new HBox();
header.setSpacing(16);
header.getChildren().add(labelVBox);
header.setPadding(new Insets(10, 16, 10, 16));
header.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(labelVBox, Priority.ALWAYS);
if (list instanceof ComponentSublist) {
Node rightNode = ((ComponentSublist) list).getHeaderRight();
if (rightNode != null)
right.getChildren().add(rightNode);
header.getChildren().add(rightNode);
}
right.getChildren().add(expandButton);
groupNode.setRight(right);
header.getChildren().add(expandButton);
RipplerContainer headerRippler = new RipplerContainer(header);
groupNode.getChildren().add(headerRippler);
VBox container = new VBox();
container.setPadding(new Insets(8, 0, 0, 0));
container.setPadding(new Insets(8, 16, 10, 16));
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container);
container.getChildren().setAll(content);
groupNode.setBottom(container);
groupNode.getChildren().add(container);
expandButton.setOnMouseClicked(e -> {
Runnable onExpand = () -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
expandAnimation.stop();
}
setExpanded(!isExpanded());
double newAnimatedHeight = content.prefHeight(-1) * (isExpanded() ? 1 : -1);
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
if (isExpanded()) {
updateClip(newHeight);
list.onExpand();
list.layout();
}
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
));
Platform.runLater(() -> {
double newAnimatedHeight = (content.prefHeight(-1) + 8 + 10) * (isExpanded() ? 1 : -1);
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
if (!isExpanded()) {
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
}
if (isExpanded()) {
updateClip(newHeight);
}
expandAnimation.play();
});
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
));
if (!isExpanded()) {
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
}
expandAnimation.play();
});
};
headerRippler.setOnMouseClicked(e -> onExpand.run());
expandButton.setOnMouseClicked(e -> onExpand.run());
expandedProperty().addListener((a, b, newValue) ->
expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(groupNode);
} else {
getStyleClass().remove("no-padding");
getChildren().setAll(content);
}
}
@@ -28,4 +28,7 @@ public interface DialogAware {
default void onDialogShown() {
}
default void onDialogClosed() {
}
}
@@ -0,0 +1,119 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DialogPane extends JFXDialogLayout {
private final StringProperty title = new SimpleStringProperty();
private final BooleanProperty valid = new SimpleBooleanProperty();
protected final SpinnerPane acceptPane = new SpinnerPane();
protected final JFXButton cancelButton = new JFXButton();
protected final Label warningLabel = new Label();
private final JFXProgressBar progressBar = new JFXProgressBar();
public DialogPane() {
Label titleLabel = new Label();
titleLabel.textProperty().bind(title);
setHeading(titleLabel);
getChildren().add(progressBar);
progressBar.setVisible(false);
StackPane.setMargin(progressBar, new Insets(-24.0D, -24.0D, -16.0D, -24.0D));
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
progressBar.setMaxWidth(Double.MAX_VALUE);
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(e -> onAccept());
acceptButton.disableProperty().bind(valid.not());
acceptButton.getStyleClass().add("dialog-accept");
acceptPane.getStyleClass().add("small-spinner-pane");
acceptPane.setContent(acceptButton);
cancelButton.setText(i18n("button.cancel"));
cancelButton.setOnAction(e -> onCancel());
cancelButton.getStyleClass().add("dialog-cancel");
onEscPressed(this, cancelButton::fire);
setActions(warningLabel, acceptPane, cancelButton);
}
protected JFXProgressBar getProgressBar() {
return progressBar;
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public boolean isValid() {
return valid.get();
}
public BooleanProperty validProperty() {
return valid;
}
public void setValid(boolean valid) {
this.valid.set(valid);
}
protected void onCancel() {
fireEvent(new DialogCloseEvent());
}
protected void onAccept() {
fireEvent(new DialogCloseEvent());
}
protected void setLoading() {
acceptPane.showSpinner();
warningLabel.setText("");
}
protected void onSuccess() {
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}
protected void onFailure(String msg) {
acceptPane.hideSpinner();
warningLabel.setText(msg);
}
}
@@ -0,0 +1,105 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FileSelector extends HBox {
private StringProperty value = new SimpleStringProperty();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
public String getValue() {
return value.get();
}
public StringProperty valueProperty() {
return value;
}
public void setValue(String value) {
this.value.set(value);
}
public String getChooserTitle() {
return chooserTitle;
}
public FileSelector setChooserTitle(String chooserTitle) {
this.chooserTitle = chooserTitle;
return this;
}
public boolean isDirectory() {
return directory;
}
public FileSelector setDirectory(boolean directory) {
this.directory = directory;
return this;
}
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public FileSelector() {
JFXTextField customField = new JFXTextField();
customField.textProperty().bindBidirectional(valueProperty());
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
selectButton.setOnMouseClicked(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
File dir = chooser.showDialog(Controllers.getStage());
if (dir != null)
customField.setText(dir.getAbsolutePath());
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null)
customField.setText(file.getAbsolutePath());
}
});
setAlignment(Pos.CENTER_LEFT);
setSpacing(3);
getChildren().addAll(customField, selectButton);
}
}
@@ -21,38 +21,57 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
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 org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class HintPane extends VBox {
private final Text label = new Text();
private final StringProperty text = new SimpleStringProperty(this, "text");
private final TextFlow flow = new TextFlow();
public HintPane() {
this(MessageDialogPane.MessageType.INFO);
}
public HintPane(MessageDialogPane.MessageType type) {
setFillWidth(true);
getStyleClass().add("hint");
getStyleClass().addAll("hint", type.name().toLowerCase());
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.getChildren().setAll(
SVG.informationOutline(Theme.blackFillBinding(), 16, 16),
new Label(i18n("message.info"))
);
TextFlow flow = new TextFlow();
switch (type) {
case INFO:
hbox.getChildren().add(SVG.informationOutline(Theme.blackFillBinding(), 16, 16));
break;
case ERROR:
hbox.getChildren().add(SVG.closeCircleOutline(Theme.blackFillBinding(), 16, 16));
break;
case SUCCESS:
hbox.getChildren().add(SVG.checkCircleOutline(Theme.blackFillBinding(), 16, 16));
break;
case WARNING:
hbox.getChildren().add(SVG.alertOutline(Theme.blackFillBinding(), 16, 16));
break;
case QUESTION:
hbox.getChildren().add(SVG.helpCircleOutline(Theme.blackFillBinding(), 16, 16));
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
hbox.getChildren().add(new Text(type.getDisplayName()));
flow.getChildren().setAll(label);
getChildren().setAll(hbox, flow);
label.textProperty().bind(text);
VBox.setMargin(flow, new Insets(2, 2, 0, 2));
label.fillProperty().bind(BindingMapping.of(disabledProperty()).map(disabled -> disabled ? new Color(0, 0, 0, 0.5) : Color.BLACK));
}
public String getText() {
@@ -66,4 +85,12 @@ public class HintPane extends VBox {
public void setText(String text) {
this.text.set(text);
}
public void setSegment(String segment) {
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
}
public void setChildren(Node... children) {
flow.getChildren().setAll(children);
}
}
@@ -0,0 +1,135 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S, T> extends TreeTableCell<S, T> {
private final CheckBox checkBox;
private boolean showLabel;
private ObservableValue<Boolean> booleanProperty;
public JFXCheckBoxTreeTableCell() {
this(null, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
this(getSelectedProperty, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox();
setGraphic(null);
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
}
private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
protected void invalidated() {
updateShowLabel();
}
};
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
@SuppressWarnings("unchecked")
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty) booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty) booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTreeTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
}
}
private void updateShowLabel() {
this.showLabel = converter != null;
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
}
private ObservableValue<?> getSelectedProperty() {
return getSelectedStateCallback() != null ?
getSelectedStateCallback().call(getIndex()) :
getTableColumn().getCellObservableValue(getIndex());
}
}
@@ -18,9 +18,9 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
@@ -28,20 +28,30 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public enum MessageType {
ERROR,
INFORMATION,
INFO,
WARNING,
QUESTION,
FINE,
SUCCESS;
public String getDisplayName() {
return i18n("message." + name().toLowerCase(Locale.ROOT));
}
}
@FXML
@@ -64,13 +74,13 @@ public final class MessageDialogPane extends StackPane {
this.title.setText(title);
switch (type) {
case INFORMATION:
case INFO:
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
break;
case ERROR:
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
break;
case FINE:
case SUCCESS:
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
break;
case WARNING:
@@ -90,7 +100,7 @@ public final class MessageDialogPane extends StackPane {
});
}
public void addButton(ButtonBase btn) {
public void addButton(Node btn) {
btn.addEventHandler(ActionEvent.ACTION, e -> fireEvent(new DialogCloseEvent()));
actions.getChildren().add(btn);
}
@@ -99,53 +109,97 @@ public final class MessageDialogPane extends StackPane {
cancelButton = btn;
}
public static MessageDialogPane ok(String text, String title, MessageType type, Runnable ok) {
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
if (ok != null) {
btnOk.setOnAction(e -> ok.run());
}
dialog.addButton(btnOk);
dialog.setCancelButton(btnOk);
return dialog;
public ButtonBase getCancelButton() {
return cancelButton;
}
public static MessageDialogPane yesOrNo(String text, String title, MessageType type, Runnable yes, Runnable no) {
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
public static class Builder {
private final MessageDialogPane dialog;
JFXButton btnYes = new JFXButton(i18n("button.yes"));
btnYes.getStyleClass().add("dialog-accept");
if (yes != null) {
btnYes.setOnAction(e -> yes.run());
public Builder(String text, String title, MessageType type) {
this.dialog = new MessageDialogPane(text, title, type);
}
dialog.addButton(btnYes);
JFXButton btnNo = new JFXButton(i18n("button.no"));
btnNo.getStyleClass().add("dialog-cancel");
if (no != null) {
btnNo.setOnAction(e -> no.run());
public Builder addAction(Node actionNode) {
dialog.addButton(actionNode);
return this;
}
dialog.addButton(btnNo);
dialog.setCancelButton(btnNo);
return dialog;
}
public static MessageDialogPane actionOrCancel(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
dialog.addButton(actionButton);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
if (cancel != null) {
btnCancel.setOnAction(e -> cancel.run());
public Builder ok(Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
if (ok != null) {
btnOk.setOnAction(e -> ok.run());
}
dialog.addButton(btnOk);
dialog.setCancelButton(btnOk);
return this;
}
dialog.addButton(btnCancel);
dialog.setCancelButton(btnCancel);
return dialog;
public Builder yesOrNo(Runnable yes, Runnable no) {
JFXButton btnYes = new JFXButton(i18n("button.yes"));
btnYes.getStyleClass().add("dialog-accept");
if (yes != null) {
btnYes.setOnAction(e -> yes.run());
}
dialog.addButton(btnYes);
JFXButton btnNo = new JFXButton(i18n("button.no"));
btnNo.getStyleClass().add("dialog-cancel");
if (no != null) {
btnNo.setOnAction(e -> no.run());
}
dialog.addButton(btnNo);
dialog.setCancelButton(btnNo);
return this;
}
public Builder actionOrCancel(ButtonBase actionButton, Runnable cancel) {
dialog.addButton(actionButton);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
if (cancel != null) {
btnCancel.setOnAction(e -> cancel.run());
}
dialog.addButton(btnCancel);
dialog.setCancelButton(btnCancel);
return this;
}
public Builder cancelOnTimeout(long timeoutMs) {
if (dialog.getCancelButton() == null) {
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
}
ButtonBase cancelButton = dialog.getCancelButton();
String originalText = cancelButton.getText();
Timer timer = Lang.getTimer();
timer.scheduleAtFixedRate(new TimerTask() {
long timeout = timeoutMs;
@Override
public void run() {
if (timeout <= 0) {
cancel();
runInFX(() -> {
cancelButton.fire();
});
return;
}
timeout -= 1000;
long currentTimeout = timeout;
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
}
}, 1000, 1000);
return this;
}
public MessageDialogPane build() {
return dialog;
}
}
}
@@ -17,12 +17,10 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.NamedArg;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -30,84 +28,31 @@ 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.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiFileItem<T> extends ComponentSublist {
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);
public class MultiFileItem<T> extends VBox {
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();
private final JFXTextField txtCustom = new JFXTextField();
private final JFXButton btnSelect = new JFXButton();
private final JFXRadioButton radioCustom = new JFXRadioButton();
private final BorderPane custom = new BorderPane();
private final VBox pane = new VBox();
private final boolean hasCustom;
private Consumer<Toggle> toggleSelectedListener;
@SuppressWarnings("unchecked")
public MultiFileItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
this.hasCustom = hasCustom;
BorderPane.setAlignment(txtCustom, Pos.CENTER_RIGHT);
btnSelect.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
btnSelect.setOnMouseClicked(e -> {
if (isDirectory()) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.titleProperty().bind(chooserTitle);
File dir = chooser.showDialog(Controllers.getStage());
if (dir != null)
txtCustom.setText(dir.getAbsolutePath());
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.titleProperty().bind(chooserTitle);
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null)
txtCustom.setText(file.getAbsolutePath());
}
});
radioCustom.textProperty().bind(customTitleProperty());
radioCustom.setToggleGroup(group);
txtCustom.disableProperty().bind(radioCustom.selectedProperty().not());
btnSelect.disableProperty().bind(radioCustom.selectedProperty().not());
custom.setLeft(radioCustom);
custom.setStyle("-fx-padding: 3;");
HBox right = new HBox();
right.setSpacing(3);
right.getChildren().addAll(txtCustom, btnSelect);
custom.setRight(right);
FXUtils.setLimitHeight(custom, 20);
pane.setStyle("-fx-padding: 0 0 10 0;");
pane.setSpacing(8);
if (hasCustom)
pane.getChildren().add(custom);
getContent().add(pane);
public MultiFileItem() {
setPadding(new Insets(0, 0, 10, 0));
setSpacing(8);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
@@ -117,7 +62,7 @@ public class MultiFileItem<T> extends ComponentSublist {
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == newValue)
.filter(it -> Objects.equals(it.getUserData(), newValue))
.findFirst();
if (!selecting.isPresent()) {
selecting = group.getToggles().stream()
@@ -129,117 +74,20 @@ public class MultiFileItem<T> extends ComponentSublist {
});
}
public Node createChildren(String title) {
return createChildren(title, null);
}
public Node createChildren(String title, T userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, T userData) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 20);
JFXRadioButton left = new JFXRadioButton(title);
left.setToggleGroup(group);
left.setUserData(userData);
pane.setLeft(left);
Label right = new Label(subtitle);
right.setWrapText(true);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
return pane;
}
public void loadChildren(Collection<Node> list) {
pane.getChildren().setAll(list);
if (hasCustom)
pane.getChildren().add(custom);
}
public void loadChildren(Collection<Node> list, T customUserData) {
loadChildren(list);
setCustomUserData(customUserData);
public void loadChildren(Collection<Option<T>> options) {
getChildren().setAll(options.stream()
.map(option -> option.createItem(group))
.collect(Collectors.toList()));
}
public ToggleGroup getGroup() {
return group;
}
public String getCustomTitle() {
return customTitle.get();
}
public StringProperty customTitleProperty() {
return customTitle;
}
public void setCustomTitle(String customTitle) {
this.customTitle.set(customTitle);
}
public String getChooserTitle() {
return chooserTitle.get();
}
public StringProperty chooserTitleProperty() {
return chooserTitle;
}
public void setChooserTitle(String chooserTitle) {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(T userData) {
radioCustom.setUserData(userData);
}
public boolean isCustomToggle(Toggle toggle) {
return radioCustom == toggle;
}
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
toggleSelectedListener = consumer;
}
public StringProperty customTextProperty() {
return txtCustom.textProperty();
}
public String getCustomText() {
return txtCustom.getText();
}
public void setCustomText(String customText) {
txtCustom.setText(customText);
}
public JFXTextField getTxtCustom() {
return txtCustom;
}
public boolean isDirectory() {
return directory.get();
}
public BooleanProperty directoryProperty() {
return directory;
}
public void setDirectory(boolean directory) {
this.directory.set(directory);
}
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public T getSelectedData() {
return selectedData.get();
}
@@ -263,4 +111,187 @@ public class MultiFileItem<T> extends ComponentSublist {
public void setFallbackData(T fallbackData) {
this.fallbackData.set(fallbackData);
}
public static class Option<T> {
protected final String title;
protected String subtitle;
protected final T data;
protected final BooleanProperty selected = new SimpleBooleanProperty();
protected final JFXRadioButton left = new JFXRadioButton();
public Option(String title, T data) {
this.title = title;
this.data = data;
}
public T getData() {
return data;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public Option<T> setSubtitle(String subtitle) {
this.subtitle = subtitle;
return this;
}
public boolean isSelected() {
return left.isSelected();
}
public BooleanProperty selectedProperty() {
return left.selectedProperty();
}
public void setSelected(boolean selected) {
left.setSelected(selected);
}
protected Node createItem(ToggleGroup group) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 30);
left.setText(title);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setToggleGroup(group);
left.setUserData(data);
pane.setLeft(left);
if (StringUtils.isNotBlank(subtitle)) {
Optional<String> shortSubtitle = StringUtils.truncate(subtitle);
Label right;
if (shortSubtitle.isPresent()) {
right = new Label(shortSubtitle.get());
right.setTooltip(new Tooltip(subtitle));
} else {
right = new Label(subtitle);
}
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
right.setWrapText(true);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
}
return pane;
}
}
public static class StringOption<T> extends Option<T> {
private JFXTextField customField = new JFXTextField();
public StringOption(String title, T data) {
super(title, data);
}
public String getValue() {
return customField.getText();
}
public StringProperty valueProperty() {
return customField.textProperty();
}
public void setValue(String value) {
customField.setText(value);
}
public StringOption<T> bindBidirectional(Property<String> property) {
customField.textProperty().bindBidirectional(property);
return this;
}
public StringOption<T> setValidators(ValidatorBase... validators) {
customField.setValidators(validators);
return this;
}
@Override
protected Node createItem(ToggleGroup group) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 30);
left.setText(title);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setToggleGroup(group);
left.setUserData(data);
pane.setLeft(left);
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
customField.disableProperty().bind(left.selectedProperty().not());
if (!customField.getValidators().isEmpty()) {
FXUtils.setValidateWhileTextChanged(customField, true);
}
pane.setRight(customField);
return pane;
}
}
public static class FileOption<T> extends Option<T> {
private FileSelector selector = new FileSelector();
public FileOption(String title, T data) {
super(title, data);
}
public String getValue() {
return selector.getValue();
}
public StringProperty valueProperty() {
return selector.valueProperty();
}
public void setValue(String value) {
selector.setValue(value);
}
public FileOption<T> setDirectory(boolean directory) {
selector.setDirectory(directory);
return this;
}
public FileOption<T> bindBidirectional(Property<String> property) {
selector.valueProperty().bindBidirectional(property);
return this;
}
public FileOption<T> setChooserTitle(String chooserTitle) {
selector.setChooserTitle(chooserTitle);
return this;
}
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return selector.getExtensionFilters();
}
@Override
protected Node createItem(ToggleGroup group) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(3));
FXUtils.setLimitHeight(pane, 30);
left.setText(title);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setToggleGroup(group);
left.setUserData(data);
pane.setLeft(left);
selector.disableProperty().bind(left.selectedProperty().not());
BorderPane.setAlignment(selector, Pos.CENTER_RIGHT);
pane.setRight(selector);
return pane;
}
}
}
@@ -50,6 +50,7 @@ public class Navigator extends TransitionPane {
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
if (init instanceof PageAware) ((PageAware) init).onPageShown();
initialized = true;
}
@@ -78,6 +79,7 @@ public class Navigator extends TransitionPane {
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
if (node instanceof PageAware) ((PageAware) node).onPageShown();
EventHandler<PageCloseEvent> handler = event -> close(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
@@ -108,7 +110,11 @@ public class Navigator extends TransitionPane {
Logging.LOG.info("Closed page " + from);
stack.pop();
Node poppedNode = stack.pop();
NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED);
poppedNode.fireEvent(exited);
if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden();
backable.set(canGoBack());
Node node = stack.peek();
@@ -202,6 +208,7 @@ public class Navigator extends TransitionPane {
};
public static class NavigationEvent extends Event {
public static final EventType<NavigationEvent> EXITED = new EventType<>("EXITED");
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
@@ -0,0 +1,111 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXToggleButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
public class OptionToggleButton extends StackPane {
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final BooleanProperty selected = new SimpleBooleanProperty();
public OptionToggleButton() {
getProperties().put("ComponentList.noPadding", true);
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 8, 8, 16));
RipplerContainer container = new RipplerContainer(pane);
getChildren().setAll(container);
VBox left = new VBox();
left.setMouseTransparent(true);
Label titleLabel = new Label();
titleLabel.textProperty().bind(title);
Label subtitleLabel = new Label();
subtitleLabel.setMouseTransparent(true);
subtitleLabel.textProperty().bind(subtitle);
pane.setLeft(left);
left.setAlignment(Pos.CENTER_LEFT);
JFXToggleButton toggleButton = new JFXToggleButton();
pane.setRight(toggleButton);
toggleButton.selectedProperty().bindBidirectional(selected);
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
container.setOnMouseClicked(e -> {
toggleButton.setSelected(!toggleButton.isSelected());
});
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
if (StringUtils.isNotBlank(subtitle)) {
left.getChildren().setAll(titleLabel, subtitleLabel);
} else {
left.getChildren().setAll(titleLabel);
}
});
}
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 isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
}
}
@@ -0,0 +1,26 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
public interface PageAware {
default void onPageShown() {
}
default void onPageHidden() {
}
}
@@ -17,108 +17,111 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PromptDialogPane extends StackPane {
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class PromptDialogPane extends DialogPane {
private final CompletableFuture<List<Builder.Question<?>>> future = new CompletableFuture<>();
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private VBox vbox;
@FXML
private Label title;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
private final Builder builder;
public PromptDialogPane(Builder builder) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
this.title.setText(builder.title);
this.builder = builder;
setTitle(builder.title);
GridPane body = new GridPane();
body.setVgap(8);
body.setHgap(16);
body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
setBody(body);
List<BooleanBinding> bindings = new ArrayList<>();
int rowIndex = 0;
for (Builder.Question<?> question : builder.questions) {
if (question instanceof Builder.StringQuestion) {
Builder.StringQuestion stringQuestion = (Builder.StringQuestion) question;
JFXTextField textField = new JFXTextField();
textField.textProperty().addListener((a, b, newValue) -> ((Builder.StringQuestion) question).value = textField.getText());
textField.setText(((Builder.StringQuestion) question).value);
textField.textProperty().addListener((a, b, newValue) -> stringQuestion.value = textField.getText());
textField.setText(stringQuestion.value);
textField.setValidators(((Builder.StringQuestion) question).validators.toArray(new ValidatorBase[0]));
if (stringQuestion.promptText != null) {
textField.setPromptText(stringQuestion.promptText);
}
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
if (StringUtils.isNotBlank(question.question.get())) {
body.addRow(rowIndex++, new Label(question.question.get()), textField);
} else {
GridPane.setColumnSpan(textField, 2);
body.addRow(rowIndex++, textField);
}
VBox.setMargin(textField, new Insets(0, 0, 20, 0));
vbox.getChildren().add(textField);
GridPane.setMargin(textField, new Insets(0, 0, 20, 0));
} else if (question instanceof Builder.BooleanQuestion) {
HBox hBox = new HBox();
GridPane.setColumnSpan(hBox, 2);
JFXCheckBox checkBox = new JFXCheckBox();
hBox.getChildren().setAll(checkBox);
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
checkBox.setText(question.question);
vbox.getChildren().add(hBox);
checkBox.setText(question.question.get());
body.addRow(rowIndex++, hBox);
} else if (question instanceof Builder.CandidatesQuestion) {
HBox hBox = new HBox();
JFXComboBox<String> comboBox = new JFXComboBox<>();
hBox.getChildren().setAll(comboBox);
comboBox.getItems().setAll(((Builder.CandidatesQuestion) question).candidates);
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
((Builder.CandidatesQuestion) question).value = newValue.intValue());
comboBox.getSelectionModel().select(0);
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
if (StringUtils.isNotBlank(question.question.get())) {
body.addRow(rowIndex++, new Label(question.question.get()), comboBox);
} else {
GridPane.setColumnSpan(comboBox, 2);
body.addRow(rowIndex++, comboBox);
}
vbox.getChildren().add(hBox);
} else if (question instanceof Builder.HintQuestion) {
HintPane pane = new HintPane();
GridPane.setColumnSpan(pane, 2);
pane.textProperty().bind(question.question);
body.addRow(rowIndex++, pane);
}
}
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x),
validProperty().bind(Bindings.createBooleanBinding(
() -> bindings.stream().allMatch(BooleanBinding::get),
bindings.toArray(new BooleanBinding[0])
));
}
acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner();
@Override
protected void onAccept() {
setLoading();
builder.callback.call(builder.questions, () -> {
acceptPane.hideSpinner();
future.complete(builder.questions);
fireEvent(new DialogCloseEvent());
}, msg -> {
acceptPane.hideSpinner();
lblCreationWarning.setText(msg);
});
builder.callback.call(builder.questions, () -> {
future.complete(builder.questions);
runInFX(this::onSuccess);
}, msg -> {
runInFX(() -> onFailure(msg));
});
onEscPressed(this, cancelButton::fire);
}
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {
@@ -141,26 +144,50 @@ public class PromptDialogPane extends StackPane {
}
public static class Question<T> {
public final String question;
public final StringProperty question = new SimpleStringProperty();
protected T value;
public Question(String question) {
this.question = question;
this.question.set(question);
}
public T getValue() {
return value;
}
public String getQuestion() {
return question.get();
}
public StringProperty questionProperty() {
return question;
}
public void setQuestion(String question) {
this.question.set(question);
}
}
public static class HintQuestion extends Question<Void> {
public HintQuestion(String hint) {
super(hint);
}
}
public static class StringQuestion extends Question<String> {
protected final List<ValidatorBase> validators;
protected String promptText;
public StringQuestion(String question, String defaultValue, ValidatorBase... validators) {
super(question);
this.value = defaultValue;
this.validators = Arrays.asList(validators);
}
public StringQuestion setPromptText(String promptText) {
this.promptText = promptText;
return this;
}
}
public static class CandidatesQuestion extends Question<Integer> {
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.Interpolator;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.NamedArg;
@@ -26,12 +28,7 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.css.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.Background;
@@ -41,6 +38,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.util.Lang;
import java.util.List;
@@ -104,6 +102,40 @@ public class RipplerContainer extends StackPane {
rectangle.widthProperty().bind(widthProperty());
rectangle.heightProperty().bind(heightProperty());
}));
setOnMouseEntered(e -> {
new Transition() {
{
setCycleCount(1);
setCycleDuration(Duration.millis(200));
setInterpolator(Interpolator.EASE_IN);
}
@Override
protected void interpolate(double frac) {
interpolateBackground(frac);
}
}.play();
});
setOnMouseExited(e -> {
new Transition() {
{
setCycleCount(1);
setCycleDuration(Duration.millis(200));
setInterpolator(Interpolator.EASE_OUT);
}
@Override
protected void interpolate(double frac) {
interpolateBackground(1 - frac);
}
}.play();
});
}
private void interpolateBackground(double frac) {
setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, frac * 0.04), CornerRadii.EMPTY, Insets.EMPTY)));
}
protected void updateChildren() {
@@ -21,6 +21,9 @@ import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
@@ -29,6 +32,7 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@DefaultProperty("content")
public class SpinnerPane extends Control {
@@ -85,6 +89,24 @@ public class SpinnerPane extends Control {
this.failedReason.set(failedReason);
}
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
return onFailedAction;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
@@ -105,7 +127,15 @@ public class SpinnerPane extends Control {
topPane.getChildren().setAll(spinner);
topPane.getStyleClass().add("notice-pane");
failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
failedPane.onMouseClickedProperty().bind(
BindingMapping.of(control.onFailedAction)
.map(actionHandler -> (e -> {
if (actionHandler != null) {
actionHandler.handle(new Event(FAILED_ACTION));
}
})));
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
@@ -146,4 +176,6 @@ public class SpinnerPane extends Control {
return reason;
}
}
public static final EventType<Event> FAILED_ACTION = new EventType<>(Event.ANY, "FAILED_ACTION");
}
@@ -39,7 +39,7 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TabHeader extends Control implements TabControl {
public class TabHeader extends Control implements TabControl, PageAware {
public TabHeader(Tab<?>... tabs) {
getStyleClass().setAll("tab-header");
@@ -70,6 +70,42 @@ public class TabHeader extends Control implements TabControl {
this.selectionModel.set(selectionModel);
}
public void select(Tab<?> tab) {
Tab<?> oldTab = getSelectionModel().getSelectedItem();
if (oldTab != null) {
if (oldTab.getNode() instanceof PageAware) {
((PageAware) oldTab.getNode()).onPageHidden();
}
}
tab.initializeIfNeeded();
if (tab.getNode() instanceof PageAware) {
((PageAware) tab.getNode()).onPageShown();
}
getSelectionModel().select(tab);
}
@Override
public void onPageShown() {
Tab<?> tab = getSelectionModel().getSelectedItem();
if (tab != null) {
if (tab.getNode() instanceof PageAware) {
((PageAware) tab.getNode()).onPageShown();
}
}
}
@Override
public void onPageHidden() {
Tab<?> tab = getSelectionModel().getSelectedItem();
if (tab != null) {
if (tab.getNode() instanceof PageAware) {
((PageAware) tab.getNode()).onPageHidden();
}
}
}
/**
* The position to place the tabs.
*/
@@ -61,6 +61,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -96,7 +97,7 @@ public final class TaskListPane extends StackPane {
@Override
public void onReady(Task<?> task) {
if (task instanceof Task.StageTask) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
});
@@ -143,14 +144,14 @@ public final class TaskListPane extends StackPane {
Platform.runLater(() -> {
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
listBox.add(listBox.indexOf(stageNode) + 1, node);
});
}
@Override
public void onFinished(Task<?> task) {
if (task instanceof Task.StageTask) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
});
@@ -167,7 +168,7 @@ public final class TaskListPane extends StackPane {
@Override
public void onFailed(Task<?> task, Throwable throwable) {
if (task instanceof Task.StageTask) {
if (task.getStage() != null) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
});
@@ -181,14 +182,29 @@ public final class TaskListPane extends StackPane {
}
@Override
public void onPropertiesUpdate(Map<String, Map<String, Object>> stageProperties) {
stageProperties.forEach((stage, properties) -> {
int count = tryCast(properties.get("count"), Integer.class).orElse(0),
total = tryCast(properties.get("total"), Integer.class).orElse(0);
if (total > 0)
Platform.runLater(() ->
stageNodes.stream().filter(x -> x.stage.equals(stage)).findAny().ifPresent(stageNode -> stageNode.updateCounter(count, total)));
});
public void onPropertiesUpdate(Task<?> task) {
if (task instanceof Task.CountTask) {
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(((Task.CountTask) task).getCountStage()))
.findAny()
.ifPresent(StageNode::count);
});
return;
}
if (task.getStage() != null) {
int total = tryCast(task.getProperties().get("total"), Integer.class).orElse(0);
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(task.getStage()))
.findAny()
.ifPresent(stageNode -> {
stageNode.setTotal(total);
});
});
}
}
});
}
@@ -197,6 +213,8 @@ public final class TaskListPane extends StackPane {
private final String stage;
private final Label title = new Label();
private final String message;
private int count = 0;
private int total = 0;
private boolean started = false;
public StageNode(String stage) {
@@ -242,6 +260,15 @@ public final class TaskListPane extends StackPane {
setLeft(FXUtils.limitingSize(SVG.check(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void count() {
updateCounter(++count, total);
}
public void setTotal(int total) {
this.total = total;
updateCounter(count, total);
}
public void updateCounter(int count, int total) {
if (total > 0)
title.setText(String.format("%s - %d/%d", message, count, total));
@@ -25,7 +25,6 @@ import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -52,8 +51,8 @@ public class TwoLineListItem extends VBox {
public TwoLineListItem() {
setMouseTransparent(true);
FlowPane firstLine = new FlowPane();
firstLine.setMaxWidth(Double.MAX_VALUE);
HBox firstLine = new HBox();
firstLine.getStyleClass().add("first-line");
Label lblTitle = new Label();
lblTitle.getStyleClass().add("title");
@@ -63,7 +62,7 @@ public class TwoLineListItem extends VBox {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
FlowPane.setMargin(tagLabel, new Insets(0, 8, 0, 0));
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
});
firstLineChildren = new AggregatedObservableList<>();
@@ -0,0 +1,51 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class URLValidator extends ValidatorBase {
private final boolean nullable;
public URLValidator() {
this(false);
}
public URLValidator(@NamedArg("nullable") boolean nullable) {
this(i18n("input.url"), nullable);
}
public URLValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else {
try {
new URL(textField.getText()).toURI();
hasErrors.set(false);
} catch (IOException | URISyntaxException e) {
hasErrors.set(true);
}
}
}
}
@@ -52,6 +52,7 @@ public class Decorator extends Control {
private final BooleanProperty canBack = new SimpleBooleanProperty(false);
private final BooleanProperty canClose = new SimpleBooleanProperty(false);
private final BooleanProperty showCloseAsHome = new SimpleBooleanProperty(false);
private final BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private Navigation.NavigationDirection navigationDirection = Navigation.NavigationDirection.START;
private StackPane drawerWrapper;
@@ -204,6 +205,18 @@ public class Decorator extends Control {
this.dragging.set(dragging);
}
public boolean isTitleTransparent() {
return titleTransparent.get();
}
public BooleanProperty titleTransparentProperty() {
return titleTransparent;
}
public void setTitleTransparent(boolean titleTransparent) {
this.titleTransparent.set(titleTransparent);
}
public ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonActionProperty() {
return onBackNavButtonAction;
}
@@ -0,0 +1,81 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
public class DecoratorAnimatedPage extends Control {
protected final VBox left = new VBox();
protected final StackPane center = new StackPane();
{
getStyleClass().add("gray-background");
}
protected void setLeft(Node... children) {
left.getChildren().setAll(children);
}
protected void setCenter(Node... children) {
center.getChildren().setAll(children);
}
public VBox getLeft() {
return left;
}
public StackPane getCenter() {
return center;
}
@Override
protected Skin<?> createDefaultSkin() {
return new DecoratorAnimatedPageSkin<>(this);
}
public static class DecoratorAnimatedPageSkin<T extends DecoratorAnimatedPage> extends SkinBase<T> {
protected DecoratorAnimatedPageSkin(T control) {
super(control);
BorderPane pane = new BorderPane();
pane.setLeft(control.left);
FXUtils.setLimitWidth(control.left, 200);
pane.setCenter(control.center);
getChildren().setAll(pane);
}
protected void setLeft(Node... children) {
getSkinnable().setLeft(children);
}
protected void setCenter(Node... children) {
getSkinnable().setCenter(children);
}
}
}
@@ -0,0 +1,90 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.scene.Node;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DecoratorAnimationProducer implements AnimationProducer {
@Override
public void init(AnimationHandler handler) {
}
@Override
public List<KeyFrame> animate(AnimationHandler handler) {
Node prev = handler.getPreviousNode();
Node next = handler.getCurrentNode();
if (prev instanceof TransitionPane.EmptyPane) {
return Collections.emptyList();
}
Duration halfDuration = handler.getDuration().divide(2);
List<KeyFrame> keyFrames = new ArrayList<>();
keyFrames.add(new KeyFrame(Duration.ZERO,
new KeyValue(prev.opacityProperty(), 1, Interpolator.EASE_BOTH)));
keyFrames.add(new KeyFrame(halfDuration,
new KeyValue(prev.opacityProperty(), 0, Interpolator.EASE_BOTH)));
if (prev instanceof DecoratorAnimatedPage) {
Node left = ((DecoratorAnimatedPage) prev).getLeft();
Node center = ((DecoratorAnimatedPage) prev).getCenter();
keyFrames.add(new KeyFrame(Duration.ZERO,
new KeyValue(left.translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(center.translateXProperty(), 0, Interpolator.EASE_BOTH)));
keyFrames.add(new KeyFrame(halfDuration,
new KeyValue(left.translateXProperty(), -30, Interpolator.EASE_BOTH),
new KeyValue(center.translateXProperty(), 30, Interpolator.EASE_BOTH)));
}
keyFrames.add(new KeyFrame(halfDuration,
new KeyValue(next.opacityProperty(), 0, Interpolator.EASE_BOTH)));
keyFrames.add(new KeyFrame(handler.getDuration(),
new KeyValue(next.opacityProperty(), 1, Interpolator.EASE_BOTH)));
if (next instanceof DecoratorAnimatedPage) {
Node left = ((DecoratorAnimatedPage) next).getLeft();
Node center = ((DecoratorAnimatedPage) next).getCenter();
keyFrames.add(new KeyFrame(halfDuration,
new KeyValue(left.translateXProperty(), -30, Interpolator.EASE_BOTH),
new KeyValue(center.translateXProperty(), 30, Interpolator.EASE_BOTH)));
keyFrames.add(new KeyFrame(handler.getDuration(),
new KeyValue(left.translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(center.translateXProperty(), 0, Interpolator.EASE_BOTH)));
}
return keyFrames;
}
@Override
public @Nullable AnimationProducer opposite() {
return null;
}
}
@@ -24,11 +24,13 @@ import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
@@ -43,6 +45,7 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -74,6 +77,7 @@ public class DecoratorController {
public DecoratorController(Stage stage, Node mainPage) {
decorator = new Decorator(stage);
decorator.setOnCloseButtonAction(Launcher::stopApplication);
decorator.titleTransparentProperty().bind(config().titleTransparentProperty());
navigator = new Navigator();
navigator.setOnNavigated(this::onNavigated);
@@ -138,13 +142,25 @@ public class DecoratorController {
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
.orElse(null);
}
if (config().getBackgroundImageType() == EnumBackgroundImage.NETWORK) {
if (!NetworkUtils.isURL(config().getBackgroundImageUrl())) {
image = loadDefaultBackgroundImage();
} else {
image = new Image(config().getBackgroundImageUrl(), true);
}
} else if (config().getBackgroundImageType() == EnumBackgroundImage.CLASSIC) {
image = newImage("/assets/img/background-classic.jpg");
} else if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
}
if (image == null) {
image = loadDefaultBackgroundImage();
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
},
config().backgroundImageTypeProperty(),
config().backgroundImageProperty()));
config().backgroundImageProperty(),
config().backgroundImageUrlProperty()));
}
private Image defaultBackground = newImage("/assets/img/background.jpg");
@@ -217,8 +233,10 @@ public class DecoratorController {
// ==== Navigation ====
public Navigator getNavigator() {
return navigator;
private static final DecoratorAnimationProducer animation = new DecoratorAnimationProducer();
public void navigate(Node node) {
navigator.navigate(node, animation);
}
private void close() {
@@ -354,6 +372,10 @@ public class DecoratorController {
if (dialog != null) {
dialogPane.pop(node);
if (node instanceof DialogAware) {
((DialogAware) node).onDialogClosed();
}
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
@@ -379,7 +401,7 @@ public class DecoratorController {
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
}
// ==== Authlib Injector DnD ====
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Animation;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
@@ -38,7 +37,6 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -57,7 +55,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
private final StackPane titleContainer;
private final Stage primaryStage;
private final TransitionPane navBarPane;
private final StackPane leftPane;
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean titleBarTransparent = true;
@@ -106,21 +103,10 @@ public class DecoratorSkin extends SkinBase<Decorator> {
parent.getChildren().add(wrapper);
// center node with a animation layer at bottom, a container layer at middle and a "welcome" layer at top.
// center node with an animation layer at bottom, a container layer at middle and a "welcome" layer at top.
StackPane container = new StackPane();
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
FXUtils.setOverflowHidden(container);
// animation layer at bottom
{
HBox layer = new HBox();
leftPane = new StackPane();
leftPane.setPrefWidth(0);
leftPane.getStyleClass().add("jfx-decorator-drawer");
layer.getChildren().setAll(leftPane);
container.getChildren().add(layer);
}
// content layer at middle
{
StackPane contentPlaceHolder = new StackPane();
@@ -153,7 +139,24 @@ public class DecoratorSkin extends SkinBase<Decorator> {
titleContainer = new StackPane();
titleContainer.setPickOnBounds(false);
titleContainer.getStyleClass().addAll("jfx-tool-bar", "background");
titleContainer.getStyleClass().addAll("jfx-tool-bar");
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
if (titleTransparent) {
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
container.backgroundProperty().unbind();
container.setBackground(null);
titleContainer.getStyleClass().remove("background");
titleContainer.getStyleClass().add("gray-background");
} else {
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
wrapper.backgroundProperty().unbind();
wrapper.setBackground(null);
titleContainer.getStyleClass().add("background");
titleContainer.getStyleClass().remove("gray-background");
}
});
control.capableDraggingWindow(titleContainer);
BorderPane titleBar = new BorderPane();
@@ -162,6 +165,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
Rectangle buttonsContainerPlaceHolder = new Rectangle();
{
navBarPane = new TransitionPane();
navBarPane.setId("decoratorTitleTransitionPane");
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
if (s == null) return;
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
@@ -179,22 +183,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
} else {
navBarPane.getChildren().setAll(node);
}
leftPane.prefWidthProperty().unbind();
if (s.getLeftPaneWidth() >= 0) {
FXUtils.playAnimation(leftPane, "animation",
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
} else {
Animation animation = FXUtils.playAnimation(leftPane, "animation1",
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, container.getWidth(), FXUtils.SINE);
if (animation != null) {
animation.setOnFinished(action -> {
if (animation.getStatus() != Animation.Status.STOPPED) {
leftPane.prefWidthProperty().bind(container.widthProperty());
}
});
}
}
});
titleBar.setCenter(navBarPane);
titleBar.setRight(buttonsContainerPlaceHolder);
@@ -21,15 +21,13 @@ import com.jfoenix.controls.JFXButton;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.Node;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.mod.DownloadManager;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -44,6 +42,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
@@ -58,12 +57,14 @@ import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DownloadPage extends BorderPane implements DecoratorPage {
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
private final TabHeader tab;
private final TabHeader.Tab<VersionsPage> newGameTab = new TabHeader.Tab<>("newGameTab");
@@ -78,10 +79,10 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
private WeakListenerHolder listenerHolder;
public DownloadPage() {
newGameTab.setNodeSupplier(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected));
modpackTab.setNodeSupplier(() -> {
DownloadListPage page = new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl);
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected)));
modpackTab.setNodeSupplier(loadVersionFor(() -> {
DownloadListPage page = new DownloadListPage(CurseForgeRemoteModRepository.MODPACKS, Versions::downloadModpackImpl);
JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack"));
installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED);
@@ -90,63 +91,59 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
page.getActions().add(installLocalModpackButton);
return page;
});
modTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MOD, (profile, version, file) -> download(profile, version, file, "mods"), true));
resourcePackTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_RESOURCE_PACK, (profile, version, file) -> download(profile, version, file, "resourcepacks")));
// customizationTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_CUSTOMIZATION, this::download));
worldTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_WORLD));
}));
modTab.setNodeSupplier(loadVersionFor(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.RESOURCE_PACKS, (profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
customizationTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.CUSTOMIZATIONS)));
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
Profiles.registerVersionsListener(this::loadVersions);
tab.getSelectionModel().select(newGameTab);
tab.select(newGameTab);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue.initializeIfNeeded()) {
if (newValue.getNode() instanceof VersionPage.VersionLoadable) {
((VersionPage.VersionLoadable) newValue.getNode()).loadVersion(Profiles.getSelectedProfile(), null);
}
}
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
{
AdvancedListBox sideBar = new AdvancedListBox()
.startCategory(i18n("download.game"))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("game"));
item.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 24, 24)));
item.setLeftGraphic(wrap(SVG::gamepad));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(newGameTab));
item.setOnAction(e -> tab.getSelectionModel().select(newGameTab));
})
.startCategory(i18n("download"))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("mods"));
item.setLeftGraphic(wrap(SVG.puzzle(Theme.blackFillBinding(), 24, 24)));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab));
item.setOnAction(e -> tab.getSelectionModel().select(modTab));
item.setOnAction(e -> tab.select(newGameTab));
})
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("modpack"));
settingsItem.setLeftGraphic(wrap(SVG.pack(Theme.blackFillBinding(), 24, 24)));
settingsItem.setLeftGraphic(wrap(SVG::pack));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modpackTab));
settingsItem.setOnAction(e -> tab.getSelectionModel().select(modpackTab));
settingsItem.setOnAction(e -> tab.select(modpackTab));
})
.startCategory(i18n("download.content"))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("mods"));
item.setLeftGraphic(wrap(SVG::puzzle));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab));
item.setOnAction(e -> tab.select(modTab));
})
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("resourcepack"));
item.setLeftGraphic(wrap(SVG.textureBox(Theme.blackFillBinding(), 24, 24)));
item.setLeftGraphic(wrap(SVG::textureBox));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
item.setOnAction(e -> tab.getSelectionModel().select(resourcePackTab));
item.setOnAction(e -> tab.select(resourcePackTab));
})
// .addNavigationDrawerItem(item -> {
// item.setTitle(i18n("download.curseforge.customization"));
// item.setLeftGraphic(wrap(SVG.script(Theme.blackFillBinding(), 24, 24)));
// item.setLeftGraphic(wrap(SVG::script));
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
// item.setOnAction(e -> tab.getSelectionModel().select(customizationTab));
// item.setOnAction(e -> tab.select(customizationTab));
// })
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("world"));
item.setLeftGraphic(wrap(SVG.earth(Theme.blackFillBinding(), 24, 24)));
item.setLeftGraphic(wrap(SVG::earth));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
item.setOnAction(e -> tab.getSelectionModel().select(worldTab));
item.setOnAction(e -> tab.select(worldTab));
});
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
@@ -155,7 +152,17 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
setCenter(transitionPane);
}
private void download(Profile profile, @Nullable String version, DownloadManager.Version file, String subdirectoryName) {
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
return () -> {
T node = nodeSupplier.get();
if (node instanceof VersionPage.VersionLoadable) {
((VersionPage.VersionLoadable) node).loadVersion(Profiles.getSelectedProfile(), null);
}
return node;
};
}
private void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
@@ -168,9 +175,13 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(exception -> {
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
}
@@ -182,24 +193,26 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
}
private void loadVersions(Profile profile) {
WeakListenerHolder listenerHolder = new WeakListenerHolder();
listenerHolder = new WeakListenerHolder();
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
if (modTab.isInitialized()) {
modTab.getNode().loadVersion(profile, null);
}
if (modpackTab.isInitialized()) {
modpackTab.getNode().loadVersion(profile, null);
}
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
}
if (customizationTab.isInitialized()) {
customizationTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
listenerHolder.add(FXUtils.onWeakChangeAndOperate(profile.selectedVersionProperty(), version -> {
if (modTab.isInitialized()) {
modTab.getNode().loadVersion(profile, null);
}
if (modpackTab.isInitialized()) {
modpackTab.getNode().loadVersion(profile, null);
}
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
}
if (customizationTab.isInitialized()) {
customizationTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
}));
}
});
}
@@ -209,6 +222,18 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
return state.getReadOnlyProperty();
}
public void showGameDownloads() {
tab.select(newGameTab);
}
public void showModDownloads() {
tab.select(modTab);
}
public void showWorldDownloads() {
tab.select(worldTab);
}
private class DownloadNavigator implements Navigation {
private final Map<String, Object> settings = new HashMap<>();
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -139,22 +140,24 @@ public class InstallersPage extends Control implements WizardPage {
BorderPane root = new BorderPane();
root.setPadding(new Insets(16));
VBox list = new VBox(8);
ComponentList list = new ComponentList();
list.getStyleClass().add("no-padding");
root.setCenter(list);
{
HBox versionNamePane = new HBox(8);
versionNamePane.setAlignment(Pos.CENTER_LEFT);
versionNamePane.getStyleClass().add("card");
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
versionNamePane.getChildren().add(new Label(i18n("archive.name")));
versionNamePane.setPadding(new Insets(20, 8, 20, 16));
control.txtName.setMaxWidth(300);
versionNamePane.getChildren().add(control.txtName);
list.getChildren().add(versionNamePane);
versionNamePane.getChildren().setAll(new Label(i18n("archive.name")), control.txtName);
list.getContent().add(versionNamePane);
}
{
VBox libraryPane = new VBox(control.group.getLibraries());
list.getContent().add(libraryPane);
}
list.getChildren().addAll(control.group.getLibraries());
{
JFXButton installButton = new JFXButton(i18n("button.install"));
@@ -20,34 +20,41 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LocalModpackPage extends StackPane implements WizardPage {
@@ -79,6 +86,9 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
@FXML
private SpinnerPane spinnerPane;
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
private Charset charset;
public LocalModpackPage(WizardController controller) {
this.controller = controller;
@@ -93,10 +103,20 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
txtModpackName.setText(name.get());
txtModpackName.setDisable(true);
} else {
txtModpackName.getValidators().addAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
FXUtils.onChangeAndOperate(installAsVersion, installAsVersion -> {
if (installAsVersion) {
txtModpackName.getValidators().setAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
} else {
txtModpackName.getValidators().setAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str) && Profiles.getProfiles().stream().noneMatch(p -> p.getName().equals(str))),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
}
});
btnInstall.disableProperty().bind(
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
.not());
@@ -120,23 +140,46 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
spinnerPane.showSpinner();
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
.thenApplyAsync(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.whenComplete(Schedulers.javafx(), manifest -> {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
.thenApplyAsync(encoding -> {
charset = encoding;
manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding);
return manifest;
})
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
if (exception instanceof ManuallyCreatedModpackException) {
spinnerPane.hideSpinner();
lblName.setText(selectedFile.getName());
installAsVersion.set(false);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
}
if (!name.isPresent()) {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
Controllers.confirm(i18n("modpack.type.manual.warning"), i18n("install.modpack"), MessageDialogPane.MessageType.WARNING,
() -> {},
controller::onEnd);
controller.getSettings().put(MODPACK_MANUALLY_CREATED, true);
} else if (exception != null) {
LOG.log(Level.WARNING, "Failed to read modpack manifest", exception);
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
Platform.runLater(controller::onEnd);
} else {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
}
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
Platform.runLater(controller::onEnd);
}).start();
}
@@ -149,6 +192,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
private void onInstall() {
if (!txtModpackName.validate()) return;
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
controller.getSettings().put(MODPACK_CHARSET, charset);
controller.onFinish();
}
@@ -167,4 +211,6 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
public static final String MODPACK_CHARSET = "MODPACK_CHARSET";
public static final String MODPACK_MANUALLY_CREATED = "MODPACK_MANUALLY_CREATED";
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
@@ -35,6 +36,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -72,11 +74,18 @@ public class ModpackInstallWizardProvider implements WizardProvider {
settings.put(PROFILE, profile);
}
private Task<Void> finishModpackInstallingAsync(Map<String, Object> settings) {
private Task<?> finishModpackInstallingAsync(Map<String, Object> settings) {
File selected = tryCast(settings.get(LocalModpackPage.MODPACK_FILE), File.class).orElse(null);
ServerModpackManifest serverModpackManifest = tryCast(settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST), ServerModpackManifest.class).orElse(null);
Modpack modpack = tryCast(settings.get(LocalModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(LocalModpackPage.MODPACK_NAME), String.class).orElse(null);
Charset charset = tryCast(settings.get(LocalModpackPage.MODPACK_CHARSET), Charset.class).orElse(null);
boolean isManuallyCreated = tryCast(settings.get(LocalModpackPage.MODPACK_MANUALLY_CREATED), Boolean.class).orElse(false);
if (isManuallyCreated) {
return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset);
}
if ((selected == null && serverModpackManifest == null) || modpack == null || name == null) return null;
if (updateVersion != null) {
@@ -90,7 +99,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
} else {
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
}
} catch (UnsupportedModpackException e) {
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
@@ -120,7 +129,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.INFORMATION, next);
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.SUCCESS, next);
}
} else {
UpdateInstallerWizardProvider.alertFailureMessage(exception, next);
@@ -19,10 +19,8 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
@@ -157,9 +155,15 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException ||
exception instanceof FabricInstallTask.UnsupportedFabricInstallationException) {
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof UnsupportedInstallationException) {
switch (((UnsupportedInstallationException) exception).getReason()) {
case UnsupportedInstallationException.FORGE_1_17_OPTIFINE_H1_PRE2:
Controllers.dialog(i18n("install.failed.optifine_forge_1.17"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
break;
default:
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
break;
}
} else if (exception instanceof DefaultDependencyManager.UnsupportedLibraryInstallerException) {
Controllers.dialog(i18n("install.failed.install_online"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof ArtifactMalformedException || exception instanceof ZipException) {
@@ -48,6 +48,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
@@ -279,6 +280,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@FXML
private void onSponsor() {
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/bmclapi_sponsor");
HMCLService.openRedirectLink("bmclapi_sponsor");
}
}
@@ -27,11 +27,7 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
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 javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
@@ -47,12 +43,11 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
@@ -189,10 +184,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
});
borderPane.setTop(hyperlink);
} else {
Label label = new Label(i18n("modpack.wizard.step.initialization.warning"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
borderPane.setTop(label);
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
pane.setText(i18n("modpack.wizard.step.initialization.warning"));
borderPane.setTop(pane);
}
{
@@ -211,93 +205,127 @@ public final class ModpackInfoPage extends Control implements WizardPage {
}
{
GridPane pane = new GridPane();
list.getContent().add(pane);
pane.setHgap(16);
pane.setVgap(8);
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
int rowIndex = 0;
JFXTextField txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.name);
txtModpackName.setLabelFloat(true);
txtModpackName.setPromptText(i18n("modpack.name"));
txtModpackName.getValidators().add(new RequiredValidator());
StackPane.setMargin(txtModpackName, insets);
list.getContent().add(txtModpackName);
validatingFields.add(txtModpackName);
}
pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName);
if (skinnable.options.isRequireFileApi()) {
JFXTextField txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
txtModpackFileApi.setLabelFloat(true);
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
if (skinnable.options.isValidateFileApi()) {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new Validator(s -> {
if (s.isEmpty()) {
return true;
}
try {
new URL(s).toURI();
return true;
} catch (IOException | URISyntaxException e) {
return false;
}
}));
StackPane.setMargin(txtModpackFileApi, insets);
list.getContent().add(txtModpackFileApi);
validatingFields.add(txtModpackFileApi);
}
{
JFXTextField txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
txtModpackAuthor.setLabelFloat(true);
txtModpackAuthor.setPromptText(i18n("archive.author"));
txtModpackAuthor.getValidators().add(new RequiredValidator());
StackPane.setMargin(txtModpackAuthor, insets);
list.getContent().add(txtModpackAuthor);
validatingFields.add(txtModpackAuthor);
}
pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor);
{
JFXTextField txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
txtModpackVersion.setLabelFloat(true);
txtModpackVersion.setPromptText(i18n("archive.version"));
txtModpackVersion.getValidators().add(new RequiredValidator());
StackPane.setMargin(txtModpackVersion, insets);
list.getContent().add(txtModpackVersion);
validatingFields.add(txtModpackVersion);
pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion);
if (skinnable.options.isRequireFileApi()) {
JFXTextField txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
validatingFields.add(txtModpackFileApi);
if (skinnable.options.isValidateFileApi()) {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new URLValidator(true));
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.minecraft_arguments")), txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.jvm_args")), txtJavaArguments);
}
if (skinnable.options.isRequireUrl()) {
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.url")), txtModpackUrl);
}
if (skinnable.options.isRequireOrigins()) {
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
validatingFields.add(txtMcbbs);
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.mcbbs")), txtMcbbs);
}
}
if (skinnable.options.isRequireMinMemory()) {
VBox pane = new VBox();
Label title = new Label(i18n("settings.memory"));
VBox.setMargin(title, new Insets(0, 0, 8, 0));
HBox lowerBoundPane = new HBox(8);
lowerBoundPane.setAlignment(Pos.CENTER);
VBox.setMargin(lowerBoundPane, new Insets(0, 0, 0, 16));
{
Label label = new Label(i18n("settings.memory.lower_bound"));
JFXSlider slider = new JFXSlider(0, 1, 0);
HBox.setMargin(slider, new Insets(0, 0, 0, 8));
HBox.setHgrow(slider, Priority.ALWAYS);
slider.setValueFactory(self -> Bindings.createStringBinding(() -> (int) (self.getValue() * 100) + "%", self.valueProperty()));
AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(skinnable.minMemory, minMemory -> {
changedByTextField.set(true);
slider.setValue(minMemory.intValue() * 1.0 / OperatingSystem.TOTAL_MEMORY);
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
skinnable.minMemory.set((int) (value.getValue().doubleValue() * OperatingSystem.TOTAL_MEMORY));
});
JFXTextField txtMinMemory = new JFXTextField();
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
FXUtils.setLimitWidth(txtMinMemory, 60);
validatingFields.add(txtMinMemory);
lowerBoundPane.getChildren().setAll(label, slider, txtMinMemory, new Label("MB"));
}
pane.getChildren().setAll(title, lowerBoundPane);
list.getContent().add(pane);
}
{
VBox pane = new VBox(8);
JFXTextArea area = new JFXTextArea();
area.textProperty().bindBidirectional(skinnable.description);
area.setLabelFloat(true);
area.setPromptText(i18n("modpack.desc"));
area.setMinHeight(400);
StackPane.setMargin(area, insets);
list.getContent().add(area);
}
if (skinnable.options.isRequireForceUpdate()) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
pane.getChildren().setAll(new Label(i18n("modpack.desc")), area);
list.getContent().add(pane);
JFXToggleButton button = new JFXToggleButton();
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
}
if (skinnable.options.isRequireAuthlibInjectorServer()) {
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
cboServers.setMaxWidth(Double.MAX_VALUE);
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
@@ -317,75 +345,17 @@ public final class ModpackInfoPage extends Control implements WizardPage {
list.getContent().add(pane);
}
if (skinnable.options.isRequireMinMemory()) {
JFXTextField txtMinMemory = new JFXTextField();
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
FXUtils.setLimitWidth(txtMinMemory, 300);
validatingFields.add(txtMinMemory);
if (skinnable.options.isRequireForceUpdate()) {
BorderPane pane = new BorderPane();
Label label = new Label(i18n("settings.min_memory"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
pane.setLeft(label);
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
pane.setRight(txtMinMemory);
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
list.getContent().add(pane);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
txtLaunchArguments.setLabelFloat(true);
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
StackPane.setMargin(txtLaunchArguments, insets);
list.getContent().add(txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
txtJavaArguments.setLabelFloat(true);
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
StackPane.setMargin(txtJavaArguments, insets);
list.getContent().add(txtJavaArguments);
}
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
BorderPane originPane = new BorderPane();
Label title = new Label(i18n("modpack.origin"));
originPane.setTop(title);
VBox container = new VBox();
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
originPane.setCenter(container);
list.getContent().add(originPane);
if (skinnable.options.isRequireUrl()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.url"));
pane.setLeft(left);
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.setRight(txtModpackUrl);
container.getChildren().add(pane);
}
if (skinnable.options.isRequireOrigins()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.mcbbs"));
pane.setLeft(left);
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
pane.setRight(txtMcbbs);
container.getChildren().add(pane);
validatingFields.add(txtMcbbs);
}
JFXToggleButton button = new JFXToggleButton();
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
}
{
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXScrollPane;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
@@ -25,7 +26,6 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -43,8 +43,8 @@ public class AboutPage extends StackPane {
IconedTwoLineListItem author = new IconedTwoLineListItem();
author.setImage(new Image("/assets/img/yellow_fish.jpg", 32, 32, false, true));
author.setTitle("huanghongxun");
author.setSubtitle("https://www.huangyuhui.net");
author.setExternalLink("https://www.huangyuhui.net");
author.setSubtitle(i18n("about.author.statement"));
author.setExternalLink("https://space.bilibili.com/1445341");
about.getContent().setAll(launcher, author);
}
@@ -63,10 +63,16 @@ public class AboutPage extends StackPane {
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
gamerteam.setTitle("gamerteam");
gamerteam.setImage(new Image("/assets/img/gamerteam.jpg", 32, 32, false, true));
gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
gamerteam.setExternalLink("http://www.zhaisoul.com/");
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
redLnn.setTitle("Red_lnn");
redLnn.setImage(new Image("/assets/img/red_lnn.jpg", 32, 32, false, true));
redLnn.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
mcbbs.setImage(new Image("/assets/img/chest.png", 32, 32, false, true));
@@ -80,19 +86,42 @@ public class AboutPage extends StackPane {
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem noin = new IconedTwoLineListItem();
noin.setImage(new Image("/assets/img/noin.png", 32, 32, false, true));
noin.setTitle(i18n("about.thanks_to.noin"));
noin.setSubtitle(i18n("about.thanks_to.noin.statement"));
noin.setExternalLink("https://noin.cn/");
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(new Image("/assets/img/github.png", 32, 32, false, true));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
thanks.getContent().setAll(yushijinhun, bangbang93, mcbbs, mcmod, noin, gamerteam, redLnn, contributors);
}
ComponentList community = new ComponentList();
{
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
users.setTitle(i18n("about.thanks_to.users"));
users.setSubtitle(i18n("about.thanks_to.users.statement"));
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
thanks.getContent().setAll(yushijinhun, bangbang93, mcbbs, mcmod, redLnn, users, contributors);
IconedTwoLineListItem discord = new IconedTwoLineListItem();
discord.setImage(new Image("/assets/img/discord.png", 32, 32, false, true));
discord.setTitle(i18n("about.community.discord"));
discord.setSubtitle(i18n("about.community.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
IconedTwoLineListItem kaiheila = new IconedTwoLineListItem();
kaiheila.setImage(new Image("/assets/img/kaiheila.jpeg", 32, 32, false, true));
kaiheila.setTitle(i18n("about.community.kaiheila"));
kaiheila.setSubtitle(i18n("about.community.kaiheila.statement"));
kaiheila.setExternalLink("https://kaihei.co/Kx7n3t");
community.getContent().setAll(users, discord, kaiheila);
}
ComponentList dep = new ComponentList();
@@ -132,22 +161,35 @@ public class AboutPage extends StackPane {
openNBT.setSubtitle("Copyright (C) 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT);
IconedTwoLineListItem jstun = new IconedTwoLineListItem();
jstun.setTitle("JSTUN");
jstun.setSubtitle("Copyright (c) 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
jstun.setExternalLink("https://github.com/tking/JSTUN");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
jstun.setTitle("minecraft-jfx-skin");
jstun.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
jstun.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
}
ComponentList legal = new ComponentList();
{
TwoLineListItem copyright = new TwoLineListItem();
IconedTwoLineListItem copyright = new IconedTwoLineListItem();
copyright.setTitle(i18n("about.copyright"));
copyright.setSubtitle(i18n("about.copyright.statement"));
copyright.setExternalLink("https://hmcl.huangyuhui.net/about/");
TwoLineListItem claim = new TwoLineListItem();
IconedTwoLineListItem claim = new IconedTwoLineListItem();
claim.setTitle(i18n("about.claim"));
claim.setSubtitle(i18n("about.claim.statement"));
claim.setExternalLink(Metadata.EULA_URL);
TwoLineListItem openSource = new TwoLineListItem();
IconedTwoLineListItem openSource = new IconedTwoLineListItem();
openSource.setTitle(i18n("about.open_source"));
openSource.setSubtitle(i18n("about.open_source.statement"));
openSource.setExternalLink("https://github.com/huanghongxun/HMCL");
legal.getContent().setAll(copyright, claim, openSource);
}
@@ -161,6 +203,9 @@ public class AboutPage extends StackPane {
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.community")),
community,
ComponentList.createComponentListTitle(i18n("about.dependency")),
dep,
@@ -169,9 +214,9 @@ public class AboutPage extends StackPane {
);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(content);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
JFXScrollPane.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
}
@@ -48,6 +48,7 @@ public class DownloadSettingsPage extends StackPane {
content.setPadding(new Insets(10));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
JFXScrollPane.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
getChildren().setAll(scrollPane);
@@ -153,7 +154,7 @@ public class DownloadSettingsPage extends StackPane {
}
{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(config().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
@@ -18,17 +18,22 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.HMCLAccounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -36,13 +41,17 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
@@ -50,8 +59,7 @@ 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 class FeedbackPage extends VBox {
private final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
public class FeedbackPage extends VBox implements PageAware {
private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList();
private final SpinnerPane spinnerPane = new SpinnerPane();
@@ -66,11 +74,14 @@ public class FeedbackPage extends VBox {
TwoLineListItem accountInfo = new TwoLineListItem();
HBox.setHgrow(accountInfo, Priority.ALWAYS);
accountInfo.titleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
accountInfo.subtitleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
accountInfo.titleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
.map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
accountInfo.subtitleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
.map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
JFXButton logButton = new JFXButton();
logButton.textProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.login") : i18n("account.logout")));
logButton.textProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
.map(account -> account == null ? i18n("account.login") : i18n("account.logout")));
logButton.setOnAction(e -> log());
loginPane.getChildren().setAll(accountInfo, logButton);
@@ -83,16 +94,21 @@ public class FeedbackPage extends VBox {
getChildren().add(searchPane);
JFXTextField searchField = new JFXTextField();
searchField.setOnAction(e -> search(searchField.getText()));
searchField.setOnAction(e -> search(searchField.getText(), "time", true));
HBox.setHgrow(searchField, Priority.ALWAYS);
searchField.setPromptText(i18n("search"));
JFXButton searchButton = new JFXButton();
searchButton.getStyleClass().add("toggle-icon4");
searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1));
searchButton.setOnAction(e -> addFeedback());
searchButton.setOnAction(e -> search(searchField.getText(), "time", true));
searchPane.getChildren().setAll(searchField, searchButton);
JFXButton addButton = new JFXButton();
addButton.getStyleClass().add("toggle-icon4");
addButton.setGraphic(SVG.plus(Theme.blackFillBinding(), -1, -1));
addButton.setOnAction(e -> addFeedback());
searchPane.getChildren().setAll(searchField, searchButton, addButton);
}
{
@@ -105,9 +121,10 @@ public class FeedbackPage extends VBox {
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton likeButton = new JFXButton();
private final JFXButton unlikeButton = new JFXButton();
private final HBox container;
{
HBox container = new HBox(8);
container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(content, Priority.ALWAYS);
@@ -115,10 +132,10 @@ public class FeedbackPage extends VBox {
setSelectable();
likeButton.getStyleClass().add("toggle-icon4");
likeButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
likeButton.setGraphic(FXUtils.limitingSize(SVG.thumbUpOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
unlikeButton.getStyleClass().add("toggle-icon4");
unlikeButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
unlikeButton.setGraphic(FXUtils.limitingSize(SVG.thumbDownOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
container.getChildren().setAll(content, likeButton, unlikeButton);
@@ -128,31 +145,61 @@ public class FeedbackPage extends VBox {
@Override
protected void updateControl(FeedbackResponse feedback, boolean empty) {
if (empty) return;
content.setTitle(feedback.getTitle());
content.setSubtitle(feedback.getContent());
content.getTags().add("#" + feedback.getId());
content.getTags().add(feedback.getAuthor());
content.getTags().add(feedback.getLauncherVersion());
content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
content.setSubtitle(feedback.getAuthor());
content.getTags().setAll(
"#" + feedback.getId(),
i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)),
i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US)));
container.setOnMouseClicked(e -> {
getFeedback(feedback.getId())
.thenAcceptAsync(Schedulers.javafx(), f -> {
Controllers.dialog(new ViewFeedbackDialog(f));
})
.start();
});
}
});
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
FeedbackResponse selectedItem = listView.getSelectionModel().getSelectedItem();
Controllers.dialog(new ViewFeedbackDialog(selectedItem));
});
getChildren().add(spinnerPane);
}
}
private void search(String keyword) {
@Override
public void onPageShown() {
search("", "time", false);
}
private void search(String keyword, String order, boolean showAll) {
HMCLAccounts.HMCLAccount account = HMCLAccounts.getAccount();
Task.supplyAsync(() -> {
return HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback", pair("s", keyword)).<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
}).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
Map<String, String> query = mapOf(
pair("keyword", keyword),
pair("order", order)
);
if (showAll) {
query.put("showAll", "1");
}
HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query));
if (account != null) {
req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider());
}
return req.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
spinnerPane.hideSpinner();
if (exception != null) {
if (exception instanceof ResponseCodeException) {
int responseCode = ((ResponseCodeException) exception).getResponseCode();
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
spinnerPane.setFailedReason(i18n("feedback.failed.permission"));
return;
} else if (responseCode == 429) {
spinnerPane.setFailedReason(i18n("feedback.failed.too_frequently"));
return;
}
}
spinnerPane.setFailedReason(i18n("feedback.failed"));
} else {
feedbacks.setAll(result);
@@ -160,18 +207,22 @@ public class FeedbackPage extends VBox {
}).start();
}
private Task<FeedbackResponse> getFeedback(int id) {
return Task.supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback/" + id).getJson(FeedbackResponse.class));
}
private void log() {
if (account.get() == null) {
if (HMCLAccounts.getAccount() == null) {
// login
Controllers.dialog(new LoginDialog());
} else {
// logout
account.set(null);
HMCLAccounts.setAccount(null);
}
}
private void addFeedback() {
if (account.get() == null) {
if (HMCLAccounts.getAccount() == null) {
Controllers.dialog(i18n("feedback.add.login"));
return;
}
@@ -179,143 +230,71 @@ public class FeedbackPage extends VBox {
Controllers.dialog(new AddFeedbackDialog());
}
private static class HMCLAccount {
private final String nickname;
private final String email;
private final String accessToken;
public HMCLAccount(String nickname, String email, String accessToken) {
this.nickname = nickname;
this.email = email;
this.accessToken = accessToken;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getAccessToken() {
return accessToken;
}
}
private static class HMCLLoginResponse {
private final int err;
private final String nickname;
private final String email;
private final String accessToken;
public HMCLLoginResponse(int err, String nickname, String email, String accessToken) {
this.err = err;
this.nickname = nickname;
this.email = email;
this.accessToken = accessToken;
}
public int getErr() {
return err;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getAccessToken() {
return accessToken;
}
public static final int ERR_OK = 0;
public static final int ERR_WRONG = 400;
}
private class LoginDialog extends JFXDialogLayout {
private final SpinnerPane spinnerPane = new SpinnerPane();
private final Label errorLabel = new Label();
private final BooleanProperty logging = new SimpleBooleanProperty();
public LoginDialog() {
setHeading(new Label(i18n("feedback.login")));
GridPane body = new GridPane();
ColumnConstraints fieldColumn = new ColumnConstraints();
fieldColumn.setFillWidth(true);
body.getColumnConstraints().setAll(new ColumnConstraints(), fieldColumn);
body.setVgap(8);
body.setHgap(8);
setBody(body);
JFXTextField usernameField = new JFXTextField();
usernameField.setValidators(new RequiredValidator());
body.addRow(0, new Label(i18n("account.username")), usernameField);
JFXPasswordField passwordField = new JFXPasswordField();
passwordField.setValidators(new RequiredValidator());
body.addRow(1, new Label(i18n("account.password")), passwordField);
JFXButton registerButton = new JFXButton();
registerButton.setText(i18n("account.register"));
registerButton.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/user/login"));
VBox vbox = new VBox(8);
setBody(vbox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
logging
? i18n("account.methods.microsoft.manual")
: i18n("account.methods.microsoft.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
}
});
vbox.getChildren().setAll(hintPane);
JFXButton loginButton = new JFXButton();
spinnerPane.setContent(loginButton);
loginButton.setText(i18n("account.login"));
loginButton.setOnAction(e -> login(usernameField.getText(), passwordField.getText()));
loginButton.setOnAction(e -> login());
JFXButton cancelButton = new JFXButton();
cancelButton.setText(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
setActions(errorLabel, registerButton, spinnerPane, cancelButton);
setActions(errorLabel, spinnerPane, cancelButton);
}
private void login(String username, String password) {
private void login() {
spinnerPane.showSpinner();
errorLabel.setText("");
Task.supplyAsync(() -> {
return HttpRequest.POST("https://hmcl.huangyuhui.net/api/user/login")
.json(mapOf(
pair("username", username),
pair("password", password)
)).getJson(HMCLLoginResponse.class);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
logging.set(true);
HMCLAccounts.login().whenComplete(Schedulers.javafx(), (result, exception) -> {
logging.set(false);
if (exception != null) {
if (exception instanceof IOException) {
if (exception instanceof ResponseCodeException && ((ResponseCodeException) exception).getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) {
errorLabel.setText(i18n("account.failed.invalid_password"));
} else {
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
}
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
} else if (exception instanceof JsonParseException) {
errorLabel.setText(i18n("account.failed.server_response_malformed"));
} else {
errorLabel.setText(exception.getClass().getName() + ": " + exception.getLocalizedMessage());
errorLabel.setText(Accounts.localizeErrorMessage(exception));
}
} else {
if (result.err == HMCLLoginResponse.ERR_OK) {
account.setValue(new HMCLAccount(result.getNickname(), result.getEmail(), result.getAccessToken()));
fireEvent(new DialogCloseEvent());
} else if (result.err == HMCLLoginResponse.ERR_WRONG) {
errorLabel.setText(i18n("account.failed.invalid_password"));
} else {
errorLabel.setText(i18n("account.failed", result.err));
}
fireEvent(new DialogCloseEvent());
}
}).start();
}
}
private static class AddFeedbackDialog extends JFXDialogLayout {
private static class AddFeedbackDialog extends DialogPane {
JFXTextField titleField = new JFXTextField();
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
JFXTextArea contentArea = new JFXTextArea();
public AddFeedbackDialog() {
setHeading(new Label(i18n("feedback.add")));
setTitle(i18n("feedback.add"));
GridPane body = new GridPane();
body.setVgap(8);
@@ -326,16 +305,14 @@ public class FeedbackPage extends VBox {
searchHintPane.setText(i18n("feedback.add.hint.search_before_add"));
body.addRow(0, searchHintPane);
HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFO);
GridPane.setColumnSpan(titleHintPane, 2);
titleHintPane.setText(i18n("feedback.add.hint.title"));
body.addRow(1, titleHintPane);
JFXTextField titleField = new JFXTextField();
titleField.setValidators(new RequiredValidator());
body.addRow(2, new Label(i18n("feedback.title")), titleField);
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
comboBox.setMaxWidth(-1);
comboBox.getItems().setAll(FeedbackType.values());
comboBox.getSelectionModel().select(0);
@@ -346,29 +323,46 @@ public class FeedbackPage extends VBox {
GridPane.setColumnSpan(contentLabel, 2);
body.addRow(4, contentLabel);
JFXTextArea contentArea = new JFXTextArea();
contentArea.setValidators(new RequiredValidator());
contentArea.setPromptText(i18n("feedback.add.hint.content"));
GridPane.setColumnSpan(contentArea, 2);
body.addRow(5, contentArea);
validProperty().bind(Bindings.createBooleanBinding(() -> {
return titleField.validate() && contentArea.validate();
}, titleField.textProperty(), contentArea.textProperty()));
setBody(body);
JFXButton okButton = new JFXButton();
okButton.setText(i18n("button.ok"));
okButton.setOnAction(e -> addFeedback(titleField.getText(), comboBox.getSelectionModel().getSelectedItem(), contentArea.getText()));
JFXButton cancelButton = new JFXButton();
cancelButton.setText(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
setActions(okButton, cancelButton);
}
private void addFeedback(String title, FeedbackType feedbackType, String content) {
fireEvent(new DialogCloseEvent());
// TODO
@Override
protected void onAccept() {
setLoading();
addFeedback(titleField.getText(), comboBox.getValue(), contentArea.getText())
.whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
onFailure(exception.getLocalizedMessage());
} else {
onSuccess();
}
})
.start();
}
private Task<?> addFeedback(String title, FeedbackType feedbackType, String content) {
return Task.runAsync(() -> {
HttpRequest.POST("https://hmcl.huangyuhui.net/api/feedback")
.json(mapOf(
pair("title", title),
pair("content", content),
pair("type", feedbackType.name().toLowerCase(Locale.ROOT)),
pair("launcher_version", Metadata.VERSION)
))
.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider())
.getString();
});
}
}
@@ -377,9 +371,11 @@ public class FeedbackPage extends VBox {
public ViewFeedbackDialog(FeedbackResponse feedback) {
BorderPane heading = new BorderPane();
TwoLineListItem left = new TwoLineListItem();
heading.setLeft(left);
left.setTitle(feedback.getTitle());
left.setSubtitle(feedback.getAuthor());
left.getTags().add("#" + feedback.getId());
left.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)));
left.getTags().add(feedback.getLauncherVersion());
left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
@@ -387,7 +383,17 @@ public class FeedbackPage extends VBox {
Label content = new Label(feedback.getContent());
content.setWrapText(true);
setBody(content);
TwoLineListItem response = new TwoLineListItem();
response.getStyleClass().setAll("two-line-item-second-large");
response.setTitle(i18n("feedback.response"));
response.setSubtitle(StringUtils.isBlank(feedback.getReason())
? i18n("feedback.response.empty")
: feedback.getReason());
VBox body = new VBox(content, response);
body.setSpacing(8);
setBody(body);
JFXButton okButton = new JFXButton();
okButton.setText(i18n("button.ok"));
@@ -402,18 +408,21 @@ public class FeedbackPage extends VBox {
private final String title;
private final String content;
private final String author;
@SerializedName("launcher_version")
private final String launcherVersion;
private final String gameVersion;
private final FeedbackType type;
private final FeedbackState state;
private final String reason;
public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, String gameVersion, FeedbackType type) {
public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, FeedbackType type, FeedbackState state, String reason) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
this.launcherVersion = launcherVersion;
this.gameVersion = gameVersion;
this.type = type;
this.state = state;
this.reason = reason;
}
public int getId() {
@@ -436,17 +445,27 @@ public class FeedbackPage extends VBox {
return launcherVersion;
}
public String getGameVersion() {
return gameVersion;
}
public FeedbackType getType() {
return type;
}
public FeedbackState getState() {
return state;
}
public String getReason() {
return reason;
}
}
private enum FeedbackType {
FEATURE_REQUEST,
BUG_REPORT
FEATURE,
BUG
}
private enum FeedbackState {
OPEN,
REJECTED,
ACCEPTED
}
}
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.controls.JFXScrollPane;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
@@ -43,7 +45,10 @@ public class HelpPage extends SpinnerPane {
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
setContent(content);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
JFXScrollPane.smoothScrolling(scrollPane);
setContent(scrollPane);
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
docPane.setTitle(i18n("help.doc"));
@@ -19,24 +19,24 @@ package org.jackhuang.hmcl.ui.main;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.PageAware;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings")));
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
private final TabHeader.Tab<SettingsPage> settingsTab = new TabHeader.Tab<>("settingsPage");
@@ -49,7 +49,7 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
private final TransitionPane transitionPane = new TransitionPane();
public LauncherSettingsPage() {
gameTab.setNodeSupplier(VersionSettingsPage::new);
gameTab.setNodeSupplier(() -> new VersionSettingsPage(true));
settingsTab.setNodeSupplier(SettingsPage::new);
personalizationTab.setNodeSupplier(PersonalizationPage::new);
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
@@ -57,13 +57,12 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
feedbackTab.setNodeSupplier(FeedbackPage::new);
sponsorTab.setNodeSupplier(SponsorPage::new);
aboutTab.setNodeSupplier(AboutPage::new);
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, sponsorTab, aboutTab);
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab);
tab.getSelectionModel().select(gameTab);
tab.select(gameTab);
gameTab.initializeIfNeeded();
gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
newValue.initializeIfNeeded();
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
@@ -71,53 +70,53 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.type.global.manage"));
settingsItem.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 24, 24)));
settingsItem.setLeftGraphic(wrap(SVG::gamepad));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
settingsItem.setOnAction(e -> tab.getSelectionModel().select(gameTab));
settingsItem.setOnAction(e -> tab.select(gameTab));
})
.startCategory(i18n("launcher"))
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.launcher.general"));
settingsItem.setLeftGraphic(wrap(SVG.applicationOutline(Theme.blackFillBinding(), 24, 24)));
settingsItem.setLeftGraphic(wrap(SVG::applicationOutline));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab));
settingsItem.setOnAction(e -> tab.getSelectionModel().select(settingsTab));
settingsItem.setOnAction(e -> tab.select(settingsTab));
})
.addNavigationDrawerItem(personalizationItem -> {
personalizationItem.setTitle(i18n("settings.launcher.appearance"));
personalizationItem.setLeftGraphic(wrap(SVG.styleOutline(Theme.blackFillBinding(), 24, 24)));
personalizationItem.setLeftGraphic(wrap(SVG::styleOutline));
personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab));
personalizationItem.setOnAction(e -> tab.getSelectionModel().select(personalizationTab));
personalizationItem.setOnAction(e -> tab.select(personalizationTab));
})
.addNavigationDrawerItem(downloadItem -> {
downloadItem.setTitle(i18n("download"));
downloadItem.setLeftGraphic(wrap(SVG.downloadOutline(Theme.blackFillBinding(), 24, 24)));
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
downloadItem.setOnAction(e -> tab.getSelectionModel().select(downloadTab));
downloadItem.setOnAction(e -> tab.select(downloadTab));
})
.startCategory(i18n("help"))
.addNavigationDrawerItem(helpItem -> {
helpItem.setTitle(i18n("help"));
helpItem.setLeftGraphic(wrap(SVG.helpCircleOutline(Theme.blackFillBinding(), 24, 24)));
helpItem.setLeftGraphic(wrap(SVG::helpCircleOutline));
helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab));
helpItem.setOnAction(e -> tab.getSelectionModel().select(helpTab));
helpItem.setOnAction(e -> tab.select(helpTab));
})
.addNavigationDrawerItem(feedbackItem -> {
feedbackItem.setTitle(i18n("feedback"));
feedbackItem.setLeftGraphic(wrap(SVG.messageAlertOutline(Theme.blackFillBinding(), 24, 24)));
feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline));
feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab));
feedbackItem.setOnAction(e -> tab.getSelectionModel().select(feedbackTab));
feedbackItem.setOnAction(e -> tab.select(feedbackTab));
})
.addNavigationDrawerItem(sponsorItem -> {
sponsorItem.setTitle(i18n("sponsor"));
sponsorItem.setLeftGraphic(wrap(SVG.handHearOutline(Theme.blackFillBinding(), 24, 24)));
sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline));
sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab));
sponsorItem.setOnAction(e -> tab.getSelectionModel().select(sponsorTab));
sponsorItem.setOnAction(e -> tab.select(sponsorTab));
})
.addNavigationDrawerItem(aboutItem -> {
aboutItem.setTitle(i18n("about"));
aboutItem.setLeftGraphic(wrap(SVG.informationOutline(Theme.blackFillBinding(), 24, 24)));
aboutItem.setLeftGraphic(wrap(SVG::informationOutline));
aboutItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(aboutTab));
aboutItem.setOnAction(e -> tab.getSelectionModel().select(aboutTab));
aboutItem.setOnAction(e -> tab.select(aboutTab));
});
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
@@ -126,9 +125,23 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
setCenter(transitionPane);
}
@Override
public void onPageShown() {
tab.onPageShown();
}
@Override
public void onPageHidden() {
tab.onPageHidden();
}
public void showGameSettings(Profile profile) {
gameTab.getNode().loadVersion(profile, null);
tab.getSelectionModel().select(gameTab);
tab.select(gameTab);
}
public void showFeedback() {
tab.select(feedbackTab);
}
@Override
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AnnouncementCard;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -73,8 +74,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final ObservableList<Node> versionNodes;
private Profile profile;
private StackPane updatePane;
private JFXButton menuButton;
private final VBox announcementPane;
private final StackPane updatePane;
private final JFXButton menuButton;
{
HBox titleNode = new HBox(8);
@@ -88,10 +90,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
titleLabel.getStyleClass().add("jfx-decorator-title");
titleNode.getChildren().setAll(titleIcon, titleLabel);
state.setValue(State.fromTitleNode(titleNode));
state.setValue(new State(null, titleNode, false, false, true));
setPadding(new Insets(20));
announcementPane = new VBox(16);
updatePane = new StackPane();
updatePane.setVisible(false);
updatePane.getStyleClass().add("bubble");
@@ -198,7 +202,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchPane.getChildren().setAll(launchButton, separator, menuButton);
}
getChildren().setAll(updatePane, launchPane);
getChildren().setAll(announcementPane, updatePane, launchPane);
menu.setMaxHeight(365);
menu.setMaxWidth(545);
@@ -212,6 +216,14 @@ public final class MainPage extends StackPane implements DecoratorPage {
Bindings.bindContent(menu.getContent(), versionNodes);
}
public MainPage() {
if (Metadata.isNightly()) {
announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.nightly.title"), i18n("update.channel.nightly.hint")));
} else if (Metadata.isDev()) {
announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.dev.title"), i18n("update.channel.dev.hint")));
}
}
private void doAnimation(boolean show) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
@@ -228,8 +240,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
private void launch() {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion());
Versions.launch(Profiles.getSelectedProfile());
}
private void onMenu() {
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
@@ -37,10 +39,11 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
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 org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.util.Collections;
import java.util.Arrays;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -52,13 +55,14 @@ public class PersonalizationPage extends StackPane {
content.setPadding(new Insets(10));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
JFXScrollPane.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
getChildren().setAll(scrollPane);
ComponentList themeList = new ComponentList();
{
ComponentList themeList = new ComponentList();
BorderPane themePane = new BorderPane();
themeList.getContent().setAll(themePane);
themeList.getContent().add(themePane);
Label left = new Label(i18n("settings.launcher.theme"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
@@ -73,35 +77,46 @@ public class PersonalizationPage extends StackPane {
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
config().setTheme(theme);
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets(config().getLauncherFontFamily()));
});
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")), themeList);
}
{
OptionToggleButton titleTransparentButton = new OptionToggleButton();
themeList.getContent().add(titleTransparentButton);
titleTransparentButton.selectedProperty().bindBidirectional(config().titleTransparentProperty());
titleTransparentButton.setTitle(i18n("settings.launcher.title_transparent"));
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")), themeList);
{
StackPane componentList = new StackPane();
ComponentList componentList = new ComponentList();
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>(true);
backgroundItem.setTitle(i18n("launcher.background"));
backgroundItem.setChooserTitle(i18n("launcher.background.choose"));
backgroundItem.setHasSubtitle(true);
backgroundItem.setCustomText(i18n("settings.custom"));
backgroundItem.setStyle("-fx-padding: 8 0 0 0");
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>();
ComponentSublist backgroundSublist = new ComponentSublist();
backgroundSublist.getContent().add(backgroundItem);
backgroundSublist.setTitle(i18n("launcher.background"));
backgroundSublist.setHasSubtitle(true);
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
), EnumBackgroundImage.CUSTOM);
backgroundItem.customTextProperty().bindBidirectional(config().backgroundImageProperty());
backgroundItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
new MultiFileItem.Option<>(i18n("launcher.background.classic"), EnumBackgroundImage.CLASSIC),
new MultiFileItem.Option<>(i18n("launcher.background.translucent"), EnumBackgroundImage.TRANSLUCENT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
.setChooserTitle(i18n("launcher.background.choose"))
.bindBidirectional(config().backgroundImageProperty()),
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
.setValidators(new URLValidator(true))
.bindBidirectional(config().backgroundImageUrlProperty())
));
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
backgroundSublist.subtitleProperty().bind(
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(config().backgroundImageProperty()));
componentList.getChildren().add(backgroundItem);
componentList.getContent().add(backgroundItem);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
}
@@ -176,21 +191,17 @@ public class PersonalizationPage extends StackPane {
{
HBox hBox = new HBox();
hBox.setSpacing(3);
hBox.setSpacing(8);
FontComboBox cboFont = new FontComboBox(12);
cboFont.valueProperty().bindBidirectional(config().fontFamilyProperty());
cboFont.valueProperty().bindBidirectional(config().launcherFontFamilyProperty());
JFXTextField txtFontSize = new JFXTextField();
FXUtils.setLimitWidth(txtFontSize, 50);
txtFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
SafeStringConverter.fromFiniteDouble()
.restrict(it -> it > 0)
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtFontSize)));
JFXButton clearButton = new JFXButton();
clearButton.getStyleClass().add("toggle-icon4");
clearButton.setGraphic(SVG.restore(Theme.blackFillBinding(), -1, -1));
clearButton.setOnAction(e -> config().setLauncherFontFamily(null));
hBox.getChildren().setAll(cboFont, txtFontSize);
hBox.getChildren().setAll(cboFont, clearButton);
borderPane.setRight(hBox);
}
@@ -198,8 +209,11 @@ public class PersonalizationPage extends StackPane {
Label lblFontDisplay = new Label("Hello Minecraft! Launcher");
lblFontDisplay.fontProperty().bind(Bindings.createObjectBinding(
() -> Font.font(config().getFontFamily(), config().getFontSize()),
config().fontFamilyProperty(), config().fontSizeProperty()));
() -> Font.font(config().getLauncherFontFamily(), 12),
config().launcherFontFamilyProperty()));
config().launcherFontFamilyProperty().addListener((a, b, newValue) -> {
Controllers.getScene().getStylesheets().setAll(config().getTheme().getStylesheets(newValue));
});
vbox.getChildren().add(lblFontDisplay);
@@ -17,9 +17,7 @@
*/
package org.jackhuang.hmcl.ui.main;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.beans.property.ReadOnlyObjectProperty;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -32,11 +30,12 @@ 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.SVG;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorTabPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.Versions;
@@ -52,18 +51,14 @@ import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorTabPage {
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
private MainPage mainPage = null;
private final TabHeader.Tab<MainPage> mainTab = new TabHeader.Tab<>("main");
public RootPage() {
setLeftPaneWidth(200);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
.register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
@@ -71,25 +66,13 @@ public class RootPage extends DecoratorTabPage {
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
mainTab.setNodeSupplier(this::getMainPage);
getTabs().setAll(mainTab);
getStyleClass().remove("gray-background");
getLeft().getStyleClass().add("gray-background");
}
@Override
public boolean back() {
if (mainTab.isSelected())
return true;
else {
getSelectionModel().select(mainTab);
return false;
}
}
@Override
protected void onNavigated(Node to) {
backableProperty().set(!(to instanceof MainPage));
super.onNavigated(to);
public ReadOnlyObjectProperty<State> stateProperty() {
return getMainPage().stateProperty();
}
@Override
@@ -131,7 +114,7 @@ public class RootPage extends DecoratorTabPage {
return mainPage;
}
private static class Skin extends SkinBase<RootPage> {
private static class Skin extends DecoratorAnimatedPageSkin<RootPage> {
protected Skin(RootPage control) {
super(control);
@@ -155,30 +138,28 @@ public class RootPage extends DecoratorTabPage {
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/bookshelf.png")).getKey());
gameItem.setLeftGraphic(wrap(SVG::viewList));
gameItem.setActionButtonVisible(false);
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
// forth item in left sidebar
AdvancedListItem downloadItem = new AdvancedListItem();
downloadItem
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/chest.png")).getKey());
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
downloadItem.setActionButtonVisible(false);
downloadItem.setTitle(i18n("download"));
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
// fifth item in left sidebar
AdvancedListItem multiplayerItem = new AdvancedListItem();
multiplayerItem
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
multiplayerItem.setLeftGraphic(wrap(SVG::lan));
multiplayerItem.setActionButtonVisible(false);
multiplayerItem.setTitle(i18n("multiplayer"));
multiplayerItem.setOnAction(e -> Controllers.navigate(Controllers.getMultiplayerPage()));
// sixth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
launcherSettingsItem.setLeftGraphic(wrap(SVG::gearOutline));
launcherSettingsItem.setActionButtonVisible(false);
launcherSettingsItem.setTitle(i18n("settings"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
@@ -191,23 +172,13 @@ public class RootPage extends DecoratorTabPage {
.add(gameListItem)
.add(gameItem)
.add(downloadItem)
.startCategory(i18n("settings.launcher.general").toLowerCase())
// .add(multiplayerItem)
.startCategory(i18n("settings.launcher.general").toUpperCase())
.add(multiplayerItem)
.add(launcherSettingsItem);
// the root page, with the sidebar in left, navigator in center.
BorderPane root = new BorderPane();
sideBar.setPrefWidth(200);
root.setLeft(sideBar);
{
control.transitionPane.getStyleClass().add("jfx-decorator-content-container");
control.transitionPane.getChildren().setAll(getSkinnable().getMainPage());
FXUtils.setOverflowHidden(control.transitionPane, 8);
root.setCenter(control.transitionPane);
}
getChildren().setAll(root);
setLeft(sideBar);
setCenter(getSkinnable().getMainPage());
}
}
@@ -21,8 +21,8 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.scene.control.ToggleGroup;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -43,7 +43,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Optional;
import java.util.logging.Level;
@@ -65,12 +64,8 @@ public final class SettingsPage extends SettingsView {
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
// ====
fileCommonLocation.loadChildren(Collections.singletonList(
fileCommonLocation.createChildren(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(config().commonDirectoryProperty());
fileCommonLocation.subtitleProperty().bind(
fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
@@ -105,14 +100,16 @@ public final class SettingsPage extends SettingsView {
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
lblUpdateNote.setWrappingWidth(470);
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());
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
updateChannel.addListener((a, b, newValue) -> {
UpdateChecker.requestCheckUpdate(newValue);
});
// ====
}
@@ -29,6 +29,7 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -38,6 +39,8 @@ import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import java.util.Arrays;
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;
@@ -45,9 +48,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class SettingsView extends StackPane {
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final ComponentSublist fileCommonLocationSublist;
protected final Label lblUpdate;
protected final Label lblUpdateSub;
protected final Text lblUpdateNote;
protected final JFXRadioButton chkUpdateStable;
protected final JFXRadioButton chkUpdateDev;
protected final JFXButton btnUpdate;
@@ -130,10 +133,8 @@ public abstract class SettingsView extends StackPane {
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);
TextFlow noteWrapper = new TextFlow(new Text(i18n("update.note")));
VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
@@ -143,22 +144,28 @@ public abstract class SettingsView extends StackPane {
}
{
fileCommonLocation = new MultiFileItem<>(true);
fileCommonLocation.setTitle(i18n("launcher.cache_directory"));
fileCommonLocation.setDirectory(true);
fileCommonLocation.setChooserTitle(i18n("launcher.cache_directory.choose"));
fileCommonLocation.setHasSubtitle(true);
fileCommonLocation.setCustomText("settings.custom");
fileCommonLocation = new MultiFileItem<>();
fileCommonLocationSublist = new ComponentSublist();
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setDirectory(true)
.bindBidirectional(config().commonDirectoryProperty())
));
{
JFXButton cleanButton = new JFXButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnMouseClicked(e -> clearCacheDirectory());
cleanButton.getStyleClass().add("jfx-button-border");
fileCommonLocation.setHeaderRight(cleanButton);
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
settingsPane.getContent().add(fileCommonLocation);
settingsPane.getContent().add(fileCommonLocationSublist);
}
{
Loaded 100 of 343 files, more files were not shown because too many files have changed in this diff. Show more