Compare commits

...
594 Commits
Author SHA1 Message Date
huanghongxun f67c3d5284 fix(launch): cannot launch game in path that contains CJK characters. Closes #1141. 2021-10-30 23:03:49 +08:00
Glavo 46385a8445 Do not delete soft links when exiting HMCL 2021-10-30 23:01:27 +08:00
Glavo 9f617c33f5 Support path containing unicode characters on Linux 2021-10-30 23:01:27 +08:00
Glavo 6aef034880 Avoid generating command lines repeatedly 2021-10-30 23:01:27 +08:00
huanghongxun c4941a01d6 fix(multiplayer): show "failed to test" instead of "testing". 2021-10-30 22:40:44 +08:00
huanghongxun 7380423eee fix(multiplayer): does not show correct kick message. 2021-10-30 22:13:32 +08:00
huanghongxun f35073ffb5 fix(ui): alignment. 2021-10-30 21:05:40 +08:00
huanghongxun f89ea56d86 fix(multiplayer): session name null. 2021-10-30 21:04:53 +08:00
huanghongxun a240b8ca9f fix(skin): Do not load authlib-injector when skin manually set to steve/alex. 2021-10-29 22:52:16 +08:00
huanghongxun 230ec2bc8e feat(update): show update dialog when found update. 2021-10-29 02:51:08 +08:00
huanghongxun a3c2642b93 feat(download): customize name of the mod to be downloaded. 2021-10-29 01:21:48 +08:00
huanghongxun de755d5495 feat(game): more version icon choices. 2021-10-29 01:21:48 +08:00
huanghongxun 08054f3026 feat(countly): initial 2021-10-27 00:21:42 +08:00
Glavo 026239903d Fix IllegalAccessError: tried to access protected method 2021-10-25 17:23:07 +08:00
Glavo a320fe4cc4 Use shadowJar minimize 2021-10-24 17:26:26 +08:00
vanja-san b122d4c5e4 Update I18N_ru.properties
Full text translation
2021-10-24 17:25:40 +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
huanghongxun 9fdf4f16c6 chore: remove Mod download page link in version page. 2021-09-13 00:16:55 +08:00
huanghongxun e821f7cb09 feat(auto-installing): download Fabric-API to mod folder. 2021-09-13 00:09:19 +08:00
huanghongxun 5c9deb129d feat(download): add import local modpack file button in modpack download page. 2021-09-12 23:38:47 +08:00
huanghongxun fa1c11db5c feat(feedback): remove feedback page. 2021-09-12 22:11:21 +08:00
huanghongxun 69b118eaec fix: incorrect max memory size. 2021-09-12 20:22:15 +08:00
huanghongxun f088bfa114 feat: search modrinth 2021-09-12 20:14:39 +08:00
huanghongxun f4c25003a0 feat: show error message when failed to refresh account. 2021-09-12 18:20:26 +08:00
huanghongxun 43ae5a24e5 fix: missing line separator before "cd". 2021-09-12 15:27:37 +08:00
Glavo fd27f16714 fix: ReflectionHelper missing method 'getHandle' (#1026) 2021-09-12 15:26:17 +08:00
Glavo beea7d0ae2 Export logs using UTF-8 (#1028) 2021-09-12 15:25:54 +08:00
huanghongxun eebd373aef fix: mcmod.png 2021-09-12 15:23:55 +08:00
huanghongxun b10330548e fix: doGet utf-8. #992. 2021-09-12 02:32:21 +08:00
huanghongxun 475d7f50f1 feat: directly show game download page in download page. 2021-09-12 02:23:56 +08:00
huanghongxun b689e68d46 fix: checkstyle and translations. 2021-09-12 01:46:09 +08:00
huanghongxun 87adbde965 feat(crash): some translations. 2021-09-12 01:41:33 +08:00
huanghongxun 5057691fb3 feat: open mcbbs/mcmod page in mod info/mod donwload page. 2021-09-12 01:41:32 +08:00
huanghongxun d7f5109a65 fix: cannot display all game versions in some cases of DownloadPage. Closes #1007. 2021-09-12 01:41:32 +08:00
huanghongxun 97f08bf74e feat: microsoft account migration link. 2021-09-12 01:41:31 +08:00
huanghongxun 1b3e4e5bd6 fix: Microsoft Account add family hint. 2021-09-12 01:41:30 +08:00
huanghongxun 0032242da6 feat: Mod chinese search 2021-09-12 01:41:30 +08:00
huanghongxun 0df1b62c3e chore: rename ModDownloadPage -> DownloadPage. 2021-09-12 01:41:29 +08:00
Glavo 8927260b75 适配 JEP 411:移除安全管理器操作 (#1000)
* Avoid calling AccessController.doPrivileged

* Keep the behavior unchanged before Java 17
2021-09-12 01:13:51 +08:00
Glavo edd4add76e 在缺失 javafx.web 的环境使用替代方案 (#1022)
* When WebView is missing, use JEditorPane instead

* Open update log with browser when WebView is missing

* Remove unnecessary prompt messages

* Update error message

* fix: checkstyle

* Add HyperlinkListener to JEditorPane
2021-09-12 01:12:50 +08:00
Glavo 2d543e69ff fix: Read instance.cfg with wrong encoding (#1023) 2021-09-12 01:11:49 +08:00
Glavo 275c14f556 在更多位置搜索 JRE (#1024)
* Search JDK in 'Program Files (ARM)'

* fix: HMCL_JRES should use the root path of JRE
2021-09-12 01:11:31 +08:00
huanghongxun f45e0abbf4 feat(mod): Chinese translations for mod names. 2021-09-11 20:08:38 +08:00
huanghongxun 2d8248461a feat(mod): Chinese translations for mod names. 2021-09-11 18:18:51 +08:00
huanghongxun e4847bbfcf Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2021-09-11 18:17:49 +08:00
Kaedehara Kazuha 127e069041 fix: fix grammatical errors & clean up (#1021) 2021-09-11 17:08:13 +08:00
ExDragine 87bf7c8d8d Fixed Chinese grammatical errors (#1020) 2021-09-11 15:43:37 +08:00
Glavo 513897406d Use GitHub Action to check for missing translations (#1019)
* Use GitHub Action to check for missing translations

* fix: missing translations

* fix: Traditional Chinese in I18N_zh_CN

* fix: I18N_zh
2021-09-11 14:39:55 +08:00
Glavo 4ace33bd80 fix: missing translations for mods.* 2021-09-11 13:12:08 +08:00
Jason_Khew c8ba03ae2b Fix MEMINFO_PATTERN is null 2021-09-11 13:11:46 +08:00
huanghongxun 784e22afa7 feat(crash-report): WIP: crash-report parser rules. 2021-09-10 23:22:27 +08:00
huanghongxun eedb9f148c feat: change directory to game dir in generated shell scripts. 2021-09-10 23:19:12 +08:00
huanghongxun 0256b8a971 feat: force application running on dedicated graphics card. So Minecraft can also run on dedicated graphics card too. 2021-09-10 21:21:51 +08:00
huanghongxun 95ea6610b1 feat: use available memory in Linux instead of free memory. 2021-09-10 20:58:40 +08:00
huanghongxun fd246b7bc6 feat(download): WIP: select game version in download page. 2021-09-09 23:51:24 +08:00
huanghongxun 98c6651793 feat: copy link when web browser cannot be opened. 2021-09-09 23:16:44 +08:00
huanghongxun 3327899cf6 feat(multiplayer): WIP: download cato. 2021-09-09 22:09:12 +08:00
huanghongxun 8c3904cdcc feat: modify UUID of offline accounts. Closes #1003. 2021-09-09 22:09:12 +08:00
huanghongxun 108df2293b fix: 411 Length Required when POST live.com. 2021-09-09 22:09:12 +08:00
huanghongxun 336d7ba221 fix: #1004. 2021-09-09 22:09:12 +08:00
Glavo 0fdac8a308 Use JDK 11 in gradle.yml 2021-09-09 14:12:08 +08:00
huanghongxun effc6d725a fix: missing translations for account.create.*. Closes #1010. 2021-09-09 14:08:46 +08:00
huanghongxun 7adc517872 fix: "does not support loading the entire remote list" 2021-09-09 14:08:46 +08:00
huanghongxun e385a85755 feat: WIP: FeedbackPage 2021-09-09 14:08:46 +08:00
huanghongxun 279b2d6f80 fix: #1002. 2021-09-09 01:19:12 +08:00
Glavo 71a845c1ed create release using github action 2021-09-08 23:15:42 +08:00
Glavo 8a908b06b6 fix typo 2021-09-08 15:06:12 +08:00
Glavo d94b3385c7 fix: DirectoryStream not closed 2021-09-08 15:06:12 +08:00
Glavo 1b72e43cce follow environment variables when finding JREs 2021-09-08 15:06:12 +08:00
Yuhui Huang 2e8d1a7702 Merge pull request #998 from Glavo/aarch64
支持在更多平台上自动补全 JavaFX
2021-09-08 15:05:45 +08:00
Glavo 6eeb2e922d Merge branch 'javafx' into aarch64 2021-09-08 14:56:25 +08:00
Glavo 36878c81aa update I18N 2021-09-08 10:17:20 +08:00
Glavo e4ac791569 exclude javafx.media on linux arm32 2021-09-08 09:53:26 +08:00
Glavo 8f591f99bd reorder dependencies 2021-09-08 09:38:45 +08:00
Glavo fc21e76507 use OpenJFX 17 2021-09-08 09:13:33 +08:00
Glavo ef118d3736 fix: Progress bar reaches 100% in advance 2021-09-07 22:23:30 +08:00
Glavo 47523db331 Pre-Touch OpenJFX dependencies 2021-09-07 21:35:35 +08:00
Glavo 7a2c52b34c Download OpenJFX on Linux ARM32 2021-09-07 20:07:11 +08:00
Glavo 3ff2284ccc Generating openjfx-dependencies.json using gradle task 2021-09-07 19:41:57 +08:00
Haowei Wen e7ce68d3cb remove useless files 2021-09-07 11:52:47 +08:00
Haowei Wen b78f5407cc redesign account list page sidebar 2021-09-07 00:46:46 +08:00
Haowei Wen b7997dfbce fix: error message overlapping in add account dialog 2021-09-06 22:45:37 +08:00
Haowei Wen d8943c266e fix: polish accounts list page 2021-09-06 21:56:17 +08:00
Haowei Wen 3051332165 feat: continue launching after adding account 2021-09-06 21:55:16 +08:00
Haowei Wen dc2911f7d8 fix: build 2021-09-06 20:17:03 +08:00
Haowei Wen 6a2ffa4f30 fix: inconsistent sidebar icon size & color 2021-09-06 19:27:10 +08:00
Haowei Wen 56bd2e0d95 feat: move auth servers to sidebar 2021-09-06 10:53:57 +08:00
huanghongxun d521d10a85 feat: WIP: multiplayer 2021-09-06 00:45:25 +08:00
huanghongxun c844a77f02 fix: sort mod download list page items. Closes #1001. 2021-09-05 21:48:29 +08:00
huanghongxun b3e3192876 feat: refine mod list ui. 2021-09-05 21:47:26 +08:00
huanghongxun 21e28ab038 feat: show more human-readable error message when no Java Edition profile existing in MicrosoftAccount. 2021-09-05 19:50:48 +08:00
huanghongxun 47540e7f91 fix: wrong i18n id 2021-09-05 19:50:05 +08:00
huanghongxun 3a406744c2 fix: padding around sponsor pane 2021-09-05 19:49:39 +08:00
huanghongxun 8a0faa72c1 fix: listview not filled the blank 2021-09-05 19:48:45 +08:00
Glavo 5d768fe64e update I18N text 2021-09-04 16:19:54 +08:00
huanghongxun 7166a809c3 feat: jstun? 2021-09-04 15:54:23 +08:00
huanghongxun c11d947099 FIx: too large spinner in accountlistitem. Closes #999. 2021-09-04 15:52:40 +08:00
huanghongxun 723f216e6b feat: JSTUN 2021-09-04 15:49:55 +08:00
huanghongxun 1000cc1ab0 feat: WIP: account login progress 2021-09-04 15:49:34 +08:00
Glavo d77a09042d fix ExceptionInInitializerError 2021-09-04 13:21:55 +08:00
Glavo 5310de88d5 select classifier according to the platform 2021-09-04 13:07:13 +08:00
Glavo ba33b927a7 update openjfx-dependencirs.json 2021-09-04 12:49:24 +08:00
huanghongxun 72ca0f20a1 feat: MicrosoftAccount: find skin by uuid instead of fetching profile. 2021-09-04 02:07:58 +08:00
huanghongxun bb15d72032 fix: add some missing tranditional Chinese translations. 2021-09-04 02:06:56 +08:00
huanghongxun cabba82118 feat: move custom commands settings to individual section. 2021-09-04 01:26:32 +08:00
Glavo 353d2794c9 improve FXUtils#openLink on Linux 2021-09-03 14:42:21 +08:00
Glavo faede16662 fix: LZMAInputStream not closed 2021-09-03 14:41:24 +08:00
Haowei Wen 665b0c4390 wip: account refactor 2021-09-03 13:02:29 +08:00
huanghongxun feb15a3c64 feat: change style of text field and combo box to match Chrome. 2021-09-03 03:56:10 +08:00
Glavo 050fa30a35 update README 2021-09-03 03:55:56 +08:00
Glavo 3314bf93b0 fix style 2021-09-03 03:55:56 +08:00
Glavo 7eed142a6f fix style 2021-09-03 03:55:56 +08:00
Glavo ac4de8b9fd fix style 2021-09-03 03:55:56 +08:00
Glavo 64a92b7768 ask the user whether to download javafx 2021-09-03 03:55:56 +08:00
Glavo cb49700ded use system look and feel 2021-09-03 03:55:56 +08:00
huanghongxun 243c9b83b9 feat: download concurrency settings. Closes #990. 2021-09-03 03:18:36 +08:00
huanghongxun b3d083877f fix: javafx bound property 2021-09-03 03:18:35 +08:00
huanghongxun 5d7c9cd15d feat: make scrollbar opaque. 2021-09-03 03:18:35 +08:00
huanghongxun 96987763a5 feat: WIP: Fabric API install 2021-09-03 03:18:34 +08:00
huanghongxun 57bb719cb8 feat: show account creation dialog when no account exists at launching game. 2021-09-03 03:18:33 +08:00
huanghongxun 981127c355 feat: set background to translucent white to make texts easier to read. 2021-09-03 03:18:32 +08:00
Haowei Wen 770a7411aa fix: scrollbar not properly displayed on jfx 16 2021-09-02 13:09:15 +08:00
Haowei Wen e428cc7117 fix: check if property is bound before setting value of ReadWriteComposedProperty 2021-09-01 16:43:07 +08:00
Haowei Wen 832c029db6 fix: can't run as named module 2021-09-01 16:43:06 +08:00
huanghongxun b700ad6b0c feat: refine download page. 2021-08-29 21:15:03 +08:00
huanghongxun 575f27cde1 feat: some sort of Minecraft CurseForge sections. 2021-08-29 17:19:35 +08:00
huanghongxun 48aab94fdf feat: balanced download provider. Closes #959. 2021-08-29 17:19:09 +08:00
huanghongxun 19bcf453a5 fix: cannot launch fabric 1.18 experimental version. Closes #950. 2021-08-29 17:18:07 +08:00
huanghongxun 0210b9fd3d feat: balanced download provider & show release date in download versions page & download page ui refactor. 2021-08-29 17:16:58 +08:00
huanghongxun a20331dfa8 fix: cannot build by latest IDEA. 2021-08-29 17:00:36 +08:00
huanghongxun 088b2f146f fix: AutoDownloadProvider not fully adapted file requests. 2021-08-29 17:00:15 +08:00
huanghongxun e9847918a9 fix: potential infinite loop when exporting game crash info.. 2021-08-29 08:35:22 +08:00
huanghongxun fcbfbd060e fix: Cannot launch modded 1.16 game from PCL. Closes #977. 2021-08-29 08:34:58 +08:00
huanghongxun f2ff523109 feat: [STDERR/] as error message. 2021-08-29 08:03:31 +08:00
huanghongxun 9cee8fa49f fix: compilation 2021-08-29 07:57:37 +08:00
huanghongxun 13038437fd fix: some stderr not recognized. 2021-08-29 07:57:15 +08:00
huanghongxun 8c641555cf fix: no longer replace player uuid in logs. 2021-08-29 07:54:53 +08:00
huanghongxun 4df2af82d1 fix: list-cell padding 2021-08-29 07:52:57 +08:00
huanghongxun 50ce3617be feat: download game in download page 2021-08-29 07:51:09 +08:00
huanghongxun fb78b8f573 feat: scroll thumb color 2021-08-29 07:50:17 +08:00
huanghongxun f8eae1f534 feat: WIP auto choose download source. 2021-08-29 06:57:24 +08:00
huanghongxun 1a0fd11e83 fix: incorrect isInitialized implementation. 2021-08-29 06:28:13 +08:00
huanghongxun d5e2bc455f fix: replace ${file.jarVersion} in mods.toml. 2021-08-29 06:26:37 +08:00
huanghongxun 23bb926acf feat: WIP: Download Page 2021-08-29 05:53:19 +08:00
huanghongxun 038cc97ecf fix: clicking global game settings should be navigated to launcher settings page. 2021-08-29 05:27:45 +08:00
huanghongxun b778d0a014 fix: memory related English translations missing. 2021-08-29 05:19:23 +08:00
huanghongxun de99213d8a feat: export necessary (debug.log, latest.log, launch script, hmcl.log and json) crash information when game got crashed. 2021-08-29 05:07:41 +08:00
huanghongxun 5a1ebe906d fix: no longer check linux for free memory size. 2021-08-29 04:27:31 +08:00
huanghongxun c23b9511b8 feat: option to use system glfw/OpenAL. Closes #958. 2021-08-29 04:26:12 +08:00
huanghongxun 6ee39aa302 fix: checkstyle 2021-08-29 02:01:40 +08:00
huanghongxun e3fb36c63f fix: discord channel is still necessary. 2021-08-29 01:51:22 +08:00
Haowei Wen dd6c192a93 do not alter version_name 2021-08-29 00:41:39 +08:00
Haowei Wen 798c67eaef display HMCL full name only in titlebar 2021-08-29 00:41:38 +08:00
huanghongxun 33d27f74fe feat: switch to KaiHeiLa instead of Discord 2021-08-29 00:37:31 +08:00
huanghongxun 680bcb8129 feat: show global game settings in launcher settings page. 2021-08-29 00:02:33 +08:00
huanghongxun 90ee87062f feat: new memory settings 2021-08-28 23:34:03 +08:00
huanghongxun 924743c622 feat: 3.4 2021-08-28 19:48:57 +08:00
huanghongxun 56865d602f fix: minimize. 2021-08-28 19:46:45 +08:00
huanghongxun dc32151501 fix: process microsoft account login error code. 2021-08-28 19:37:51 +08:00
huanghongxun b9382ce966 fix: macos also support 'nice' command. 2021-08-28 18:54:09 +08:00
huanghongxun 2de5c36df5 feat: process priority settings. 2021-08-28 18:53:24 +08:00
huanghongxun 3972683583 feat: allow cancel logging task. 2021-08-28 18:22:21 +08:00
huanghongxun fcafca2fdb feat: download settings page & appearance settings page. 2021-08-28 18:12:12 +08:00
huanghongxun 635351d58d feat: move doc link to help page. 2021-08-28 16:57:33 +08:00
huanghongxun dc3caea2f4 Merge commit '9bef8b432b9095bdb470f7a1e7c1ce8fcab37c01' into javafx 2021-08-28 16:41:29 +08:00
huanghongxun 9bef8b432b feat: account list page left pane 2021-08-28 16:35:28 +08:00
huanghongxun 6e45ea835f bulid: minimize jar 2021-08-28 16:29:36 +08:00
huanghongxun f1e4364ffa fix: background size 2021-08-28 16:29:35 +08:00
huanghongxun a560eff153 feat: title bar with color 2021-08-28 16:29:33 +08:00
huanghongxun e9bc1524f1 fix: no longer hide player id in game logs. 2021-08-28 16:29:31 +08:00
huanghongxun cc868656a8 fix: mess code in Microsoft login page. Closes #981. 2021-08-28 16:27:27 +08:00
huanghongxun 96e6554b37 fix: remove useless Content-Type header in GET request. Closes #984. 2021-08-28 16:26:19 +08:00
huanghongxun f869ddbbd2 feat: about page javafx 2021-08-28 15:22:38 +08:00
huanghongxun 6ebdfb0a26 feat: launch icon -> rocket-launch-outline 2021-08-28 15:22:18 +08:00
huanghongxun e2ac171a92 feat: account list page left pane 2021-08-28 15:13:23 +08:00
huanghongxun 9abca03e50 bulid: minimize jar 2021-08-28 01:29:26 +08:00
huanghongxun 63f4a3dc5c fix: background size 2021-08-28 01:29:13 +08:00
huanghongxun 34ceb1a29d feat: title bar with color 2021-08-28 01:24:36 +08:00
Haowei Wen 8230c28084 feat: press esc to go back 2021-08-27 23:24:57 +08:00
Haowei Wen a85d3b504f fix: main page account item not working properly 2021-08-27 22:57:49 +08:00
Haowei Wen 6ac3f68dfc feat: add param hmcl.openjfx.repo 2021-08-27 20:38:12 +08:00
Haowei Wen e37322d147 fix: NPE caused by uninitialized processPriority 2021-08-27 20:05:18 +08:00
huanghongxun f2dd7e4295 fix: no longer hide player id in game logs. 2021-08-27 00:19:37 +08:00
huanghongxun 647bbb38e8 feat: new background image 2021-08-26 03:53:17 +08:00
huanghongxun ff899db6b0 feat: rounded window corner 2021-08-26 03:53:09 +08:00
huanghongxun 8bf2a2fad3 fix: about 2021-08-26 03:52:53 +08:00
huanghongxun e9061ebc8b feat: process priority 2021-08-26 03:52:32 +08:00
huanghongxun 5edd7a7b72 feat: help page 2021-08-26 03:11:23 +08:00
huanghongxun bd343f49f6 feat: discord chat 2021-08-24 21:14:39 +08:00
huanghongxun f5a16ae005 fix: sponsor mess code 2021-08-24 03:44:12 +08:00
huanghongxun 837c7adaa7 fix: crash when profile outdated 2021-08-24 01:35:25 +08:00
huanghongxun a27df90fc5 fix: missing microsoft auth id 2021-08-23 11:44:10 +08:00
huanghongxun c18b0510bc feat: sponsor list 2021-08-23 00:30:07 +08:00
huanghongxun 7bf06684c4 feat: WIP: Sponsor page 2021-08-22 22:26:10 +08:00
huanghongxun f79465038b fix: remove debug statements 2021-08-22 22:18:02 +08:00
huanghongxun 54fe311a3d fix: checkstyle 2021-08-22 21:32:54 +08:00
huanghongxun 3b8a0989de fix: Microsoft Account refresh token 2021-08-22 21:29:08 +08:00
huanghongxun 5890f0c782 feat: authenticate Microsoft accounts via external browser 2021-08-22 20:35:30 +08:00
huanghongxun b39508922f fix: memory leak 2021-08-22 17:14:06 +08:00
huanghongxun 13034db3f9 fix: checkstyle 2021-08-22 17:10:46 +08:00
huanghongxun a21e303a8a chore: adjust order of account factory 2021-08-22 17:06:21 +08:00
huanghongxun 4bdacb66a6 feat: AddAccountPane tabularization 2021-08-22 17:05:10 +08:00
huanghongxun 2fea07fc6c revert: RootPage UI 2021-08-21 22:10:34 +08:00
huanghongxun 9741ba4b07 feat: show avatars of contributors in about page. 2021-08-21 19:57:58 +08:00
huanghongxun d2d0875c36 feat: WIP: new account look 2021-08-21 18:47:37 +08:00
huanghongxun 494d40c8b5 feat: WIP: change launcher ui font 2021-08-21 18:47:17 +08:00
huanghongxun 0b78033a87 feat: new about page 2021-08-21 18:46:28 +08:00
huanghongxun 66bb27c4dd fix: incorrect detected forge version. Closes #961. 2021-08-21 15:50:22 +08:00
huanghongxun f00afbe1b5 feat: support mcbbs modpack second edition. Closes #964. 2021-08-21 15:01:30 +08:00
huanghongxun 7da9d0f283 feat: cache detected game version 2021-08-21 14:41:56 +08:00
huanghongxun ddd0f83936 perf: no longer using JFXColorPicker to speed up page loading. 2021-08-21 00:59:53 +08:00
Haowei Wen c660b1d3f8 fix: version id checking not working properly 2021-08-20 23:13:11 +08:00
Yuhui Huang 2022af8a53 build: fix gradle script deprecations. 2021-08-20 15:58:44 +08:00
Haowei Wen b3564d9d27 fix #916 2021-08-20 07:58:06 +08:00
Haowei Wen 28d6f1f19a feat: press esc key to close dialog 2021-08-20 07:31:53 +08:00
Haowei Wen 588fe5cb5f fix: hide GameVersionList$1 task 2021-08-20 07:30:19 +08:00
Haowei Wen e138815c7b patch forge installer DOWNLOAD_MOJMAPS task to use mirrors 2021-08-20 07:30:19 +08:00
Haowei Wen 9bc10fb27d use separate tasks for forge installer processors 2021-08-20 07:30:19 +08:00
Haowei Wen 7d39d00a6a align add acount pane labels to left 2021-08-20 06:14:16 +08:00
Haowei Wen f448229e22 feat: search on enter in modpack search page 2021-08-20 04:48:22 +08:00
Haowei Wen d74d800d48 fix: trim modpack name when installing new modpack, fix #962 2021-08-20 03:12:58 +08:00
Haowei Wen 69ded66c92 fix: callExternalProcess() not displaying stderr 2021-08-19 02:11:14 +08:00
Haowei Wen 3c8d17b564 fix: files from maven.minecraftforge.net not downloaded from mirror 2021-08-19 00:47:37 +08:00
huanghongxun e61af71415 fix: cannot launch game with bootstraplauncher > 0.1.17. 2021-08-14 18:29:25 +08:00
huanghongxun 7d1f50519c fix: version.manage.manage.title not translated. Closes #956. 2021-08-14 17:03:32 +08:00
huanghongxun 6f5cc4d59d fix: crash in ModDownloadListPage. Closes #951. 2021-08-10 01:08:05 +08:00
huanghongxun 220cd144f5 fix: GameItem always show empty tag 2021-08-07 18:25:40 +08:00
huanghongxun 820c5a563a perf: slightly speed up VersionSettingsPage loading. 2021-08-07 18:14:51 +08:00
huanghongxun b2594122a8 feat: display mod description and website url. Closes #945. 2021-08-07 16:18:23 +08:00
huanghongxun 1ddcc23b89 feat: lazy initialize game mamangement popup menu. 2021-08-07 16:17:33 +08:00
huanghongxun d57a4655a6 perf: try to speed up css style lookup. 2021-08-07 14:30:55 +08:00
huanghongxun df4620518c fix: wrong thread. Closes #944. 2021-08-05 01:52:31 +08:00
huanghongxun b8ca9a3b09 fix: sort mod downloads. Closes #942. 2021-08-05 01:51:16 +08:00
Glavo 1ad8beb49e insert explicit 'System.gc()' call 2021-08-04 23:17:25 +08:00
huanghongxun 134f96be08 fix: ModDownloadListPage ListView should fill the blank. 2021-08-04 12:12:20 +08:00
huanghongxun cb03d9841d feat: delete button in version deleting dialog become red. 2021-08-04 01:54:45 +08:00
huanghongxun fe922e0bed fix: Check whether java 16 is chosen when launching 1.17 and javaVersion is not defined in version json. 2021-08-04 01:52:16 +08:00
huanghongxun 53c63db150 fix: ModListPage not loading mods. 2021-08-04 01:29:07 +08:00
huanghongxun 5ddb69077f feat: refine WorldListItem icons 2021-08-04 01:27:34 +08:00
huanghongxun 1fb9fa0af9 fix: icon of selected item should be highlighted. 2021-08-04 00:09:22 +08:00
huanghongxun d35fb9a486 fix: (modpack download): add default category. 2021-08-04 00:08:23 +08:00
Yuhui Huang 37fb9a0d65 feat: (modpack download): category & sort. 2021-08-03 22:47:53 +08:00
Yuhui Huang af7cf393dc feat: download mods and modpacks from CurseForge. 2021-08-03 22:07:19 +08:00
Yuhui Huang 7a20af462b feat: (WIP) Curse mod/modpack download 2021-08-02 23:11:30 +08:00
Yuhui Huang 0939ed819e feat: title bar swipe animation. 2021-08-02 16:46:45 +08:00
Yuhui Huang 5da4ad7a62 feat: refine GameListPage UI. 2021-08-02 16:16:21 +08:00
Yuhui Huang 123c863e3d feat: refine profile list UI. 2021-08-02 16:05:37 +08:00
Yuhui Huang 6fc0b13dc7 feat: refine UI icons. 2021-08-02 15:28:57 +08:00
Yuhui Huang 59bf8d3ebc style: fix checkstyle. 2021-08-02 15:09:34 +08:00
Yuhui Huang 805393b184 fix: GameListItem launch icon -> launchOutline icon. 2021-08-02 15:07:31 +08:00
Yuhui Huang 5d0d77aff4 style: fix checkstyle. 2021-08-02 15:03:39 +08:00
Yuhui Huang d2e4ddc876 fix: inconsistent svg icon size. 2021-08-02 15:02:54 +08:00
huanghongxun 5b7ca8c751 feat: redesign UI of game management. 2021-08-02 13:36:36 +08:00
huanghongxun 16c7558497 fix: active tab indicator animation aligned to Material Design standard. 2021-07-31 23:23:07 +08:00
huanghongxun 9b6b3f8938 fix: no longer validate Forge dependencies since provided checksums are wrong. Closes #913. 2021-07-31 02:10:04 +08:00
huanghongxun 331d8d1bb6 fix: does not remove the old library when updating the library 2021-07-31 02:02:06 +08:00
huanghongxun e5cca24740 fix: Version.jar becomes wrong value after resolvePreservingPatches 2021-07-31 02:01:17 +08:00
huanghongxun 6f40335322 feat: print more effective logs when OptiFine Patcher failed to patch game. 2021-07-31 02:00:13 +08:00
huanghongxun b6de9d4ea2 fix: failed to duplicate version when no jar 2021-07-31 00:56:26 +08:00
Yuhui Huang 5f8f01e0ab Revert "fix: Version.jar should be resolved to id of self if null instead of the id of ancestor.". Closes #934.
This reverts commit 5de150b06e.
2021-07-28 16:15:11 +08:00
xtex 0d69e944f6 fix: #930(Search new Java registry location first) 2021-07-25 17:29:44 +08:00
huanghongxun 1dc4e3f730 fix: 1.17 Forge auto installation 2021-07-25 17:29:00 +08:00
huanghongxun 14d1bccd14 fix: update condition check for whether the game is modded. 2021-07-25 15:58:22 +08:00
huanghongxun 5de150b06e fix: Version.jar should be resolved to id of self if null instead of the id of ancestor. 2021-07-25 01:21:08 +08:00
huanghongxun e34dd39b9a fix: malformed locale name. Closes #925. 2021-07-24 14:56:18 +08:00
Haowei Wen e7d5faeeac fix: DirectoryStream not closed
https://github.com/huanghongxun/HMCL/commit/dc3167472175d023878f9ae32230b93964588bc1#r53347194
2021-07-12 13:19:16 +08:00
huanghongxun 46a7269087 chore: ignore hmcl.json 2021-07-10 23:25:26 +08:00
huanghongxun dc31674721 fix: Do not throw exception when path to cached jvm installations does not exist. 2021-07-10 23:24:31 +08:00
Glavo afe6bc7456 用户 JVM 参数优先于默认参数 (#903)
* close #885: use the GC selected by user by default

* fix: default JVM args conflict with user args

* Options take precedence over JVM args

* compile huge methods by default

* update settings.advanced.jvm_args text

* Using JVM args suppression options

* update settings.advanced.jvm_args text
2021-07-10 23:10:04 +08:00
huanghongxun 378fd04463 feat: placeholders in user customized game arguments. Closes #920 2021-07-10 23:09:11 +08:00
huanghongxun 3e751ba5c6 chore: some comments on "libraries_directory" 2021-07-10 23:08:39 +08:00
Glavo 8fc1ec7bb8 替换对 JDK 内置 Pack200 的依赖,允许在 JDK14 及更高版本上构建项目 (#904)
* fix: Pack200 has been removed

* Automatically add pack200 to class path

* add license header to Pack200Utils.java
2021-07-10 22:44:13 +08:00
huanghongxun e5326a3c4d fix: switch to "library_directory" defined by official launcher 2021-07-10 21:22:57 +08:00
huanghongxun fe279234d8 feat: Support "invalid" hash code in download info 2021-07-10 21:16:34 +08:00
huanghongxun ef9b041310 feat: Support placeholders in version.json. Closes #923 2021-07-10 21:16:33 +08:00
Ghost_chu 9db576d4bc 修复在manifest中的因拼写问题造成的与文档不符的问题 (#921) 2021-07-09 13:02:17 +08:00
Haowei Wen a84971abfc update english translation 2021-06-24 21:05:30 +08:00
Haowei Wen a7a79db842 update copyright year in settings page 2021-06-24 21:00:38 +08:00
Haowei Wen d5da14fb1e remove welcome page (reduce ~300k size) 2021-06-24 20:43:29 +08:00
Haowei Wen 0d2e287f67 load welcome.png only when necessary
This saves ~200ms startup time.
2021-06-23 06:08:11 +08:00
Haowei Wen c329f1cafb log error when background can't be loaded 2021-06-23 04:57:01 +08:00
Haowei Wen ab1a2f6fcb fix: NPE in ModpackInfoPage 2021-06-15 00:34:50 +08:00
Haowei Wen 463dc56702 delete license-header.txt since it's not used 2021-06-14 19:12:38 +08:00
Haowei Wen 88e23cd0d2 feat: speed up openjfx dependencies checking
SHA-1 of dependencies are defined in openjfx-dependencies.json
to avoid network requests during startup.

openjfx-dependencies.json can be generated using
tools/generate-openjfx-dependencies.sh
2021-06-14 14:22:56 +08:00
Haowei Wen 9776aafab6 fix missing license in some files 2021-06-14 02:45:17 +08:00
Haowei Wen fbb5a03e5d feat: select authlib-injector server after added
* The most recently added server will be at the top.
* In Add Account dialog, after adding an authlib-injector server,
  it will be automatically selected.
2021-06-13 22:58:05 +08:00
huanghongxun 7c11ce53f3 fix: NPE. Closes #896. 2021-06-12 18:43:54 +08:00
huanghongxun 66f4c022d5 fix: ask user whether to download java16. 2021-06-12 18:43:53 +08:00
yaoxi-std 4b36450f9a changed language files, fixed #898 2021-06-12 16:53:24 +08:00
huanghongxun 36b58bd1bb feat: Support CurseForge modpack with Fabric as modloader. Closes #897. 2021-06-12 01:25:10 +08:00
Glavo 0296b6f73d fix: export modpack 2021-06-08 19:06:09 +08:00
huanghongxun 447afaa76f fix: typo 2021-05-30 03:39:29 +08:00
huanghongxun a0191aa7c2 fix: lint 2021-05-30 03:37:55 +08:00
huanghongxun e7c9fcf767 feat: checkstyle in GitHub Actions 2021-05-30 03:35:26 +08:00
huanghongxun fc017515c5 feat: check style & format 2021-05-30 03:34:44 +08:00
huanghongxun 6d75c4d34f chore: C++ google style 2021-05-30 02:42:39 +08:00
huanghongxun 258a6628e4 fix: Cannot start application when code page is 65001. Closes #814.
This commit may cause breaking change: changing working directory to the directory where the executable locates in.
2021-05-30 02:25:31 +08:00
huanghongxun 228695d511 fix: NPE 2021-05-29 17:22:06 +08:00
Glavo c2af530abb feat: lookup JRE installation in more places. 2021-05-29 17:18:56 +08:00
huanghongxun 69bc826e30 feat: BMCLAPI now supports downloading Java 16. 2021-05-29 17:17:00 +08:00
huanghongxun 5d0944ed19 feat: lookup JRE installation in more places. 2021-05-29 17:06:08 +08:00
寒兮 43f4da5dd3 fix: GITHUB_SHA build version NullPointException 2021-05-29 17:05:12 +08:00
寒兮 c6ab2cec35 fix: short commit version use lower case 2021-05-29 17:05:12 +08:00
寒兮 b0dc689ba7 fix: use GITHUB_SHA instead of the git command 2021-05-29 17:05:12 +08:00
寒兮 e11b1f37b9 chore: artifacts version use git short commit 2021-05-29 17:05:12 +08:00
huanghongxun 3e5fac4df6 feat: lookup JRE installation in more places. 2021-05-29 16:50:07 +08:00
huanghongxun e63fa40822 feat: run github actions on pull request 2021-05-29 16:49:24 +08:00
huanghongxun e01096bff0 fix: lint 2021-05-29 15:15:40 +08:00
yaoxi-std 2a0cfb1125 fixed bugs 2021-05-29 01:23:58 +08:00
yaoxi-std d4f0a04ccd change NativesDir into LaunchOptions 2021-05-29 01:23:58 +08:00
yaoxi-std 7cbe24b35b add: allow customize LWJGL path (runs on M1) 2021-05-29 01:23:58 +08:00
huanghongxun b7da21e5ce fix: not select downloaded Java automatically. Closes #886. 2021-05-27 00:17:33 +08:00
Glavo 0ae1411f0a fix #861: Unable open zipfs for signed exe file 2021-05-25 15:05:42 +08:00
huanghongxun 66889cac27 chore: version 3.3 2021-05-25 13:25:43 +08:00
huanghongxun 67bd2ac386 fix: typo 2021-05-25 13:23:09 +08:00
huanghongxun 1f278c2a49 feat: publish unsigned artifacts by GitHub Actions 2021-05-25 13:20:33 +08:00
huanghongxun 1674f1828f fix: hmclauncher 2021-05-25 13:14:43 +08:00
huanghongxun 472a0a039f fix: fuck lfs 2021-05-25 13:13:33 +08:00
509 changed files with 45916 additions and 7663 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
*.exe filter=lfs diff=lfs merge=lfs -text
+3 -2
View File
@@ -6,9 +6,10 @@
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [Discord频道](https://discord.gg/jVvC7HfM6U) [开黑啦频道](https://kaihei.co/Kx7n3t) 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://afdian.net/@huanghongxun) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
+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
+21
View File
@@ -0,0 +1,21 @@
name: Check Translations
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 test
run: ./gradlew checkTranslations
+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 }}
+15 -5
View File
@@ -1,6 +1,6 @@
name: Java CI
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -8,11 +8,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: 1.8
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-${{ env.SHORT_SHA }}
path: HMCL/build/libs
+44
View File
@@ -0,0 +1,44 @@
name: Create Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check and set HMCL version
run: |
export HMCL_VERSION=`echo ${{ github.ref }} | awk '{print substr($0,12)}'`
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
if [ -z `echo $HMCL_VERSION | grep "^[0-9]\.[0-9]\.[0-9]\{3\}\$"` ]; then
exit 1
fi
echo "HMCL_BUILD_NUMBER=`echo $HMCL_VERSION | awk '{print substr($0,5)}'`" >> $GITHUB_ENV
- name: Download artifacts
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
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
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
+10 -2
View File
@@ -10,8 +10,8 @@ hs_err_pid*
*.2
*.log
.mine*
/externalgames
NVIDIA
*.json
# gradle build
/build/
@@ -38,4 +38,12 @@ NVIDIA
# netbeans
.nb-gradle
*.exe
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
# test
/hmcl.json
/.hmcl.json
+3
View File
@@ -0,0 +1,3 @@
[submodule "JSTUN"]
path = JSTUN
url = https://github.com/huanghongxun/JSTUN
+2
View File
@@ -0,0 +1,2 @@
/data.csv
/data.json
+35 -10
View File
@@ -1,9 +1,11 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
}
}
@@ -19,18 +21,24 @@ import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
import org.glavo.pack200.Pack200
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
def dev = null
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
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'
@@ -38,6 +46,8 @@ mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation rootProject.files("lib/JFoenix.jar")
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
}
def digest(String algorithm, byte[] bytes) {
@@ -96,8 +106,10 @@ sourceSets {
}
compileJava11Java {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
if(JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
}
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
sourceCompatibility = 11
@@ -112,11 +124,22 @@ jar {
shadowJar {
classifier = null
minimize {
exclude(dependency('com.google.code.gson:.*:.*'))
exclude(dependency('com.github.steveice10:.*:.*'))
exclude(project(":HMCLTransformerDiscoveryService"))
}
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version,
'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',
'java.base/java.lang.reflect',
@@ -161,8 +184,10 @@ processResources {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
javaexec {
classpath = sourceSets.main.compileClasspath
mainClass = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
@@ -207,4 +232,4 @@ task makeExecutables(dependsOn: jar) doLast {
build.dependsOn makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
build.dependsOn makeExecutables
+138
View File
@@ -0,0 +1,138 @@
#
# 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/>.
#
import json
import codecs
import re
from urllib.parse import urlparse, parse_qs
S = ';'
MOD_SEPARATOR = ','
CURSEFORGE_PATTERN1 = re.compile(
r'^/minecraft/(mc-mods|customization|mc-addons|customization/configuration)/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN2 = re.compile(
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN3 = re.compile(
r'^/mc-mods/minecraft/(?P<modid>[\w-]+)(/(.*?))?$')
def parseCurseforge(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3]:
match = pattern.match(res.path)
if match:
return match.group('modid')
return ''
MCMOD_PATTERN = re.compile(
r'^http(s)?://www\.mcmod\.cn/class/(?P<modid>\d+)\.html$')
def parseMcmod(url):
match = MCMOD_PATTERN.match(url)
if match:
return match.group('modid')
return ''
MCBBS_HTML_PATTERN = re.compile(r'/thread-(?P<modid>\d+)-(\d+)-(\d+).html')
MCBBS_PHP_PATTERN = re.compile(r'')
def parseMcbbs(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
if res.netloc != 'www.mcbbs.net':
return ''
match = MCBBS_HTML_PATTERN.match(res.path)
if match:
return match.group('modid')
query = parse_qs(res.query)
if res.path == '/forum.php':
if 'mod' in query and 'tid' in query and query['mod'] == ['viewthread']:
return query['tid']
return ''
if __name__ == '__main__':
with codecs.open('data.json', mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
data = json.load(jsonfile)
for mod in data:
chinese_name = mod['name']['main']
sub_name = mod['name']['sub']
abbr = mod['name']['abbr']
if S in chinese_name:
print('Error! ' + chinese_name)
exit(1)
if S in sub_name:
print('Error! ' + chinese_name)
exit(1)
black_lists = [
'Master Chef'
]
curseforge_id = ''
mcmod_id = ''
mcbbs_id = ''
links = mod['links']['list']
if 'curseforge' in links and links['curseforge'] and sub_name not in black_lists and chinese_name not in black_lists:
for link in links['curseforge']:
curseforge_id = parseCurseforge(link['url'])
if curseforge_id != '':
break
if curseforge_id == '':
print('Error curseforge ' + chinese_name)
exit(1)
if 'mcmod' in links and links['mcmod']:
mcmod_id = parseMcmod(links['mcmod'][0]['url'])
if mcmod_id == '':
print('Error mcmod ' + chinese_name)
exit(1)
if 'mcbbs' in links and links['mcbbs']:
mcbbs_id = parseMcbbs(links['mcbbs'][0]['url'])
if mcbbs_id == '':
print('Error mcbbs ' + chinese_name)
exit(1)
mod_id = []
if 'list' in mod['modid']:
for id in mod['modid']['list']:
if MOD_SEPARATOR in id:
print('Error mod id!' + id)
exit(1)
mod_id.append(id)
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
outfile.write(
f'{curseforge_id}{S}{mcmod_id}{S}{mcbbs_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
print('Success!')
-2
View File
@@ -1,2 +0,0 @@
rootProject.name = 'HMCL'
@@ -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,11 +32,13 @@ 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.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.*;
import java.nio.file.Paths;
@@ -60,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()));
@@ -105,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"));
@@ -115,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);
}
@@ -160,10 +167,15 @@ public final class Launcher extends Application {
return null;
}
} else {
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
try {
File jarFile = new File(URLDecoder.decode(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
} catch (UnsupportedEncodingException e) {
LOG.log(Level.WARNING, "Failed to decode jar path", e);
return null;
}
}
if (result.isEmpty())
return null;
+28 -32
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;
@@ -43,29 +45,41 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
private Main() {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
// Use System look and feel
initLookAndFeel();
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"));
checkJavaFX();
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
Launcher.main(args);
}
private static void initLookAndFeel() {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
@@ -84,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.
*/
@@ -138,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]);
@@ -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
@@ -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;
@@ -29,15 +30,21 @@ import java.nio.file.Paths;
public final class Metadata {
private Metadata() {}
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String 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();
@@ -46,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();
}
}
@@ -0,0 +1,87 @@
/*
* 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.countly;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Countly {
private String deviceId;
private String endpoint;
private String serverURL;
public void sendMetric(String metrics) throws IOException {
HttpRequest.GET(serverURL + endpoint,
pair("begin_session", "1"),
pair("session_id", "1"),
pair("metrics", metrics),
pair("device_id", deviceId),
pair("timestamp", Long.toString(System.currentTimeMillis())),
pair("tz", Integer.toString(TimeZone.getDefault().getOffset(new Date().getTime()) / 60000)),
pair("hour", Integer.toString(currentHour())),
pair("dow", Integer.toString(currentDayOfWeek())),
pair("app_key", APP_KEY),
pair("sdk_name", "java-native"),
pair("sdk_version", "20.11.1"))
.getString();
}
private static int getTimezoneOffset() {
return TimeZone.getDefault().getOffset(new Date().getTime()) / 60000;
}
private static String getLocale() {
final Locale locale = Locale.getDefault();
return locale.getLanguage() + "_" + locale.getCountry();
}
private static int currentHour() {
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
private int currentDayOfWeek() {
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (day) {
case Calendar.SUNDAY:
return 0;
case Calendar.MONDAY:
return 1;
case Calendar.TUESDAY:
return 2;
case Calendar.WEDNESDAY:
return 3;
case Calendar.THURSDAY:
return 4;
case Calendar.FRIDAY:
return 5;
case Calendar.SATURDAY:
return 6;
}
return 0;
}
private static final String APP_KEY = "";
}
@@ -0,0 +1,104 @@
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public class CrashReport {
private final Thread thread;
private final Throwable throwable;
private final String stackTrace;
private boolean nonFatal;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
nonFatal = false;
}
public CrashReport setNonFatal() {
nonFatal = true;
return this;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
return true;
}
public Map<String, Object> getMetrics(long runningTime) {
return mapOf(
pair("_run", runningTime),
pair("_app_version", Metadata.VERSION),
pair("_os", OperatingSystem.SYSTEM_NAME),
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
pair("_disk_current", getDiskAvailable()),
pair("_disk_total", getDiskTotal()),
pair("_ram_current", getMemoryAvailable()),
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
pair("_error", stackTrace),
pair("_logs", Logging.getLogs()),
pair("_name", throwable.getLocalizedMessage()),
pair("_nonfatal", nonFatal)
);
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + thread + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
private static final Long BYTES_IN_MB = 1024L * 1024;
private static long getMemoryAvailable() {
Long total = Runtime.getRuntime().totalMemory();
Long availMem = Runtime.getRuntime().freeMemory();
return (total - availMem) / BYTES_IN_MB;
}
private static long getDiskAvailable() {
long total = 0, free = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
free += f.getUsableSpace();
}
return (total - free) / BYTES_IN_MB;
}
private static long getDiskTotal() {
long total = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
}
return total / BYTES_IN_MB;
}
}
@@ -64,7 +64,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
try {
// TODO: Dirty implementation here
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
FileUtils.writeText(optionsFile, "lang:zh_cn\nforceUnicodeFont:true\n");
FileUtils.writeText(optionsFile, "lang:zh_CN\nforceUnicodeFont:true\n");
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
}
@@ -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
@@ -24,28 +24,36 @@ import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionIconType;
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 org.jetbrains.annotations.Nullable;
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;
@@ -54,6 +62,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
@@ -88,6 +98,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();
@@ -104,8 +121,11 @@ 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
System.gc();
}
public void changeDirectory(File newDirectory) {
@@ -124,13 +144,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());
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
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(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
@@ -204,6 +237,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
@@ -213,6 +247,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
@Nullable
public VersionSetting getLocalVersionSettingOrCreate(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) {
vs = createLocalVersionSetting(id);
}
return vs;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
@@ -231,17 +274,21 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (id == null || !isLoaded())
return newImage("/assets/img/grass.png");
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (version.getMainClass() != null &&
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
VersionSetting vs = getLocalVersionSettingOrCreate(id);
VersionIconType iconType = Optional.ofNullable(vs).map(VersionSetting::getVersionIcon).orElse(VersionIconType.DEFAULT);
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
} else {
return newImage(iconType.getResourceUrl());
}
}
public boolean saveVersionSetting(String id) {
@@ -255,7 +302,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;
}
}
@@ -281,23 +328,22 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public boolean forbidsVersion(String id) {
return FORBIDDEN.contains(id);
}
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)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.getMaxMemory())
.setMaxMemory((int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setWidth(vs.getWidth())
@@ -305,8 +351,14 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
@@ -364,7 +416,42 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setPrettyPrinting()
.create();
private static final HashSet<String> FORBIDDEN = new HashSet<>(Arrays.asList("modpack", "minecraftinstance", "manifest"));
private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}";
// These version ids are forbidden because they may conflict with modpack configuration filenames
private static final Set<String> FORBIDDEN_VERSION_IDS = new HashSet<>(Arrays.asList(
"modpack", "minecraftinstance", "manifest"));
public static boolean isValidVersionId(String id) {
if (FORBIDDEN_VERSION_IDS.contains(id))
return false;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase()))
return false;
return OperatingSystem.isNameValid(id);
}
/**
* Returns true if the given version id conflicts with an existing version.
*/
public boolean versionIdConflicts(String id) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// on Windows, filenames are case-insensitive
for (String existingId : versions.keySet()) {
if (existingId.equalsIgnoreCase(id)) {
return true;
}
}
return false;
} else {
return versions.containsKey(id);
}
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
return auto ? Math.max(minimum, (long) (available * 0.8)) : minimum;
}
}
@@ -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,11 +18,14 @@
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 java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@@ -31,27 +34,37 @@ import java.nio.file.Path;
* @author huangyuhui
*/
public final class HMCLModpackManager {
private HMCLModpackManager() {
}
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @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);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
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 + ".");
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);
}
}
}
@@ -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
@@ -23,16 +23,13 @@ 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;
@@ -41,18 +38,15 @@ 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.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
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.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
@@ -63,14 +57,15 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
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;
@@ -95,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) {
@@ -129,11 +121,17 @@ public final class LauncherHelper {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
Optional<String> gameVersion = repository.getGameVersion(version);
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())
@@ -141,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(() -> {
@@ -170,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.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
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) {
@@ -208,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"))
@@ -224,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)
@@ -278,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);
}
@@ -295,201 +318,278 @@ public final class LauncherHelper {
executor.start();
}
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown"));
if (setting.isNotCheckJVM()) {
onAccept.run();
return;
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
.withStage("launch.state.java");
}
boolean flag = false;
boolean java8required = false;
boolean newJavaRequired = false;
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());
// Without onAccept called, the launching operation will be terminated.
if (setting.isJavaAutoSelected()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
GameJavaVersion targetJavaVersion;
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(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 (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 (!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());
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 {
Controllers.dialog(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), MessageType.WARNING, () -> {
downloadJava(version.getJavaVersion(), profile).thenAccept(x -> onAccept.run());
});
flag = true;
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
setting.setJava(null);
setting.setDefaultJavaPath(null);
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
}
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;
}
}
}
}
}
// 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);
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 {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept);
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;
}
}
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;
}
}
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.PLATFORM)
.filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (!java64.isPresent()) {
final boolean java8requiredFinal = java8required, newJavaRequiredFinal = newJavaRequired;
// Then find if other java version which satisfies requirements installed.
java64 = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.PLATFORM)
.filter(javaVersion -> {
if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8;
if (newJavaRequiredFinal) return javaVersion.getParsedVersion() >= JavaVersion.JAVA_8;
return true;
})
.max(Comparator.comparing(JavaVersion::getVersionNumber));
}
if (java64.isPresent()) {
setting.setJavaVersion(java64.get());
} else {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageType.ERROR, onAccept);
flag = true;
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;
}
}
// 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;
// 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;
}
}
// 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;
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;
}
}
}
if (!flag)
onAccept.run();
// 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;
}
// 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 (!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 (!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(executor.getException());
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;
}
@@ -525,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;
@@ -535,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;
@@ -545,9 +647,7 @@ public final class LauncherHelper {
forbiddenTokens = Collections.emptyMap();
else
forbiddenTokens = mapOf(
pair(authInfo.getAccessToken(), "<access token>"),
pair(UUIDTypeAdapter.fromUUID(authInfo.getUUID()), "<uuid>"),
pair(authInfo.getUsername(), "<player>")
pair(authInfo.getAccessToken(), "<access token>")
);
logs = new LinkedList<>();
@@ -563,10 +663,15 @@ 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();
logWindow.show();
logWindow.showNormal();
logWindowLatch.countDown();
});
}
@@ -648,27 +753,8 @@ public final class LauncherHelper {
if (!lwjgl) finishLaunch();
if (exitType != ExitType.NORMAL) {
repository.markVersionLaunchedAbnormally(version);
Platform.runLater(() -> {
if (logWindow == null) {
logWindow = new LogWindow();
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.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
}
logWindow.showGameCrashReport();
});
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}
checkExit();
@@ -677,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,82 @@
/*
* 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.task.Schedulers;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public final class LogExporter {
private LogExporter() {
}
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
List<String> versions = new ArrayList<>();
String currentVersionId = versionId;
HashSet<String> resolvedSoFar = new HashSet<>();
while (true) {
if (resolvedSoFar.contains(currentVersionId)) break;
resolvedSoFar.add(currentVersionId);
Version currentVersion = gameRepository.getVersion(currentVersionId);
versions.add(currentVersionId);
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
currentVersionId = currentVersion.getInheritsFrom();
} else {
break;
}
}
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
}
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
}
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
zipper.putTextFile(launchScript, OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
for (String id : versions) {
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
if (Files.exists(versionJson)) {
zipper.putFile(versionJson, id + ".json");
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, Schedulers.io());
}
}
@@ -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,24 +39,31 @@ 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 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 {
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
ignored.printStackTrace();
// ignore it, not a valid MCBBS modpack.
}
@@ -83,9 +91,40 @@ public final class ModpackHelper {
// 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 +148,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 +168,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 +209,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()) {
@@ -0,0 +1,208 @@
/*
* 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 fi.iki.elonen.NanoHTTPD;
import org.jackhuang.hmcl.auth.AuthenticationException;
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;
import java.util.logging.Level;
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 OAuthServer extends NanoHTTPD implements OAuth.Session {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
public static String lastlyOpenedURL;
private String idToken;
private OAuthServer(int port) {
super(port);
this.port = port;
}
@Override
public String getRedirectURI() {
return String.format("http://localhost:%d/auth-response", port);
}
@Override
public String waitFor() throws InterruptedException, ExecutionException {
return future.get();
}
@Override
public String getIdToken() {
return idToken;
}
@Override
public Response serve(IHTTPSession session) {
if (!"/auth-response".equals(session.getUri())) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
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(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, "");
}
thread(() -> {
try {
Thread.sleep(1000);
stop();
} catch (InterruptedException e) {
Logging.LOG.log(Level.SEVERE, "Failed to sleep for 1 second");
}
});
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
@Override
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 {
OAuthServer server = new OAuthServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
return server;
} catch (IOException e) {
exception = e;
}
}
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
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse(""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
}
}
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 {
}
}
@@ -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
@@ -20,18 +20,13 @@ package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -39,13 +34,15 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -233,34 +230,18 @@ public final class TexturesLoader {
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
.map(FXUtils::toFXImage);
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
.map(FXUtils::toFXImage);
} else {
return Bindings.createObjectBinding(
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
}
}
// ====
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
private static WritableImage toFXImage(BufferedImage image) {
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
PixelWriter pw = wr.getPixelWriter();
final int iw = image.getWidth();
final int ih = image.getHeight();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
}
}
return wr;
}
}
}
@@ -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
@@ -24,25 +24,19 @@ 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.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
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;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
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.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import java.io.IOException;
import java.nio.file.Paths;
@@ -81,11 +75,13 @@ 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(MicrosoftAccountLoginStage.INSTANCE));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_AUTHLIB_INJECTOR, FACTORY_MICROSOFT);
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 ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -328,4 +324,64 @@ public final class Accounts {
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")));
}
// ====
public static String localizeErrorMessage(Exception exception) {
if (exception instanceof NoCharacterException) {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return i18n("account.failed.connect_authentication_server");
} else if (exception instanceof ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials")) {
return i18n("account.failed.invalid_credentials");
} else if (remoteMessage.contains("Invalid token")) {
return i18n("account.failed.invalid_token");
} else if (remoteMessage.contains("Invalid username or password")) {
return i18n("account.failed.invalid_password");
} 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) {
return i18n("account.failed.injector_download_failure");
} else if (exception instanceof CharacterDeletedException) {
return i18n("account.failed.character_deleted");
} else if (exception instanceof InvalidSkinException) {
return i18n("account.skin.invalid_skin");
} else if (exception instanceof MicrosoftService.XboxAuthorizationException) {
long errorCode = ((MicrosoftService.XboxAuthorizationException) exception).getErrorCode();
if (errorCode == MicrosoftService.XboxAuthorizationException.ADD_FAMILY) {
return i18n("account.methods.microsoft.error.add_family");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.COUNTRY_UNAVAILABLE) {
return i18n("account.methods.microsoft.error.country_unavailable");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
return i18n("account.methods.microsoft.error.missing_xbox_account");
} else {
return i18n("account.methods.microsoft.error.unknown", errorCode);
}
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
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 {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
}
@@ -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);
@@ -122,8 +124,20 @@ public final class Config implements Cloneable, Observable {
@SerializedName("localization")
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("autoDownloadThreads")
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(false);
@SerializedName("downloadThreads")
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
@SerializedName("downloadType")
private StringProperty downloadType = new SimpleStringProperty("bmclapi");
private StringProperty downloadType = new SimpleStringProperty("mcbbs");
@SerializedName("autoChooseDownloadType")
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
@SerializedName("versionListSource")
private StringProperty versionListSource = new SimpleStringProperty("balanced");
@SerializedName("configurations")
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@@ -137,14 +151,20 @@ 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("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -227,6 +247,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();
}
@@ -383,6 +415,30 @@ public final class Config implements Cloneable, Observable {
return localization;
}
public boolean getAutoDownloadThreads() {
return autoDownloadThreads.get();
}
public BooleanProperty autoDownloadThreadsProperty() {
return autoDownloadThreads;
}
public void setAutoDownloadThreads(boolean autoDownloadThreads) {
this.autoDownloadThreads.set(autoDownloadThreads);
}
public int getDownloadThreads() {
return downloadThreads.get();
}
public IntegerProperty downloadThreadsProperty() {
return downloadThreads;
}
public void setDownloadThreads(int downloadThreads) {
this.downloadThreads.set(downloadThreads);
}
public String getDownloadType() {
return downloadType.get();
}
@@ -395,6 +451,30 @@ public final class Config implements Cloneable, Observable {
return downloadType;
}
public boolean isAutoChooseDownloadType() {
return autoChooseDownloadType.get();
}
public BooleanProperty autoChooseDownloadTypeProperty() {
return autoChooseDownloadType;
}
public void setAutoChooseDownloadType(boolean autoChooseDownloadType) {
this.autoChooseDownloadType.set(autoChooseDownloadType);
}
public String getVersionListSource() {
return versionListSource.get();
}
public void setVersionListSource(String versionListSource) {
this.versionListSource.set(versionListSource);
}
public StringProperty versionListSourceProperty() {
return versionListSource;
}
public ObservableMap<String, Profile> getConfigurations() {
return configurations;
}
@@ -427,6 +507,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();
}
@@ -443,18 +535,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();
}
@@ -490,4 +570,28 @@ 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);
}
public String getPromptedVersion() {
return promptedVersion.get();
}
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
public void setPromptedVersion(String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
}
@@ -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());
}
}
@@ -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
@@ -17,49 +17,102 @@
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.download.AdaptedDownloadProvider;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
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;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class DownloadProviders {
private DownloadProviders() {}
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
private static DownloadProvider currentDownloadProvider;
public static final Map<String, DownloadProvider> providersById;
public static final Map<String, DownloadProvider> rawProviders;
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
private static final MojangDownloadProvider MOJANG;
private static final BMCLAPIDownloadProvider BMCLAPI;
private static final BMCLAPIDownloadProvider MCBBS;
public static final String DEFAULT_PROVIDER_ID = "balanced";
public static final String DEFAULT_RAW_PROVIDER_ID = "mcbbs";
private static final InvalidationListener observer;
static {
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
MCBBS = new BMCLAPIDownloadProvider("https://download.mcbbs.net");
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", BMCLAPI),
pair("mcbbs", MCBBS)
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider(bmclapiRoot)),
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
pair("mirror", new AutoDownloadProvider(MCBBS, fileProvider)));
observer = FXUtils.observeWeak(() -> {
FetchTask.setDownloadExecutorConcurrency(
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
}
static void init() {
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(providersById.get(config().getDownloadType()))
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
if (!providersById.containsKey(versionListSource)) {
config().setVersionListSource(DEFAULT_PROVIDER_ID);
return;
}
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
DOWNLOAD_PROVIDER.setDownloadProviderCandidates(
});
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
if (!rawProviders.containsKey(downloadType)) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
return;
}
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
fileDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
providersById.values().stream().filter(x -> x != primary)
rawProviders.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
});
@@ -73,23 +126,44 @@ public final class DownloadProviders {
return DEFAULT_PROVIDER_ID;
}
public static AdaptedDownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
AdaptedDownloadProvider adaptedDownloadProvider = new AdaptedDownloadProvider();
DownloadProvider primary = Optional.ofNullable(providersById.get(primaryId))
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
return Optional.ofNullable(providersById.get(primaryId))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
adaptedDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
providersById.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
return adaptedDownloadProvider;
}
/**
* Get current primary preferred download provider
*/
public static AdaptedDownloadProvider getDownloadProvider() {
return DOWNLOAD_PROVIDER;
public static DownloadProvider getDownloadProvider() {
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
}
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", url);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), url);
} 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;
}
}
@@ -208,6 +208,24 @@ public final class Profile implements Observable {
Platform.runLater(observableHelper::invalidate);
}
public static class ProfileVersion {
private final Profile profile;
private final String version;
public ProfileVersion(Profile profile, String version) {
this.profile = profile;
this.version = version;
}
public Profile getProfile() {
return profile;
}
public String getVersion() {
return version;
}
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
@@ -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);
}
}
@@ -23,11 +23,11 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.Locale;
public class Settings {
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Settings {
private static Settings instance;
@@ -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;
@@ -41,7 +42,7 @@ public class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
public static final Color BLACK = Color.web("#292929");
public static final Color[] SUGGESTED_COLORS = new Color[]{
Color.web("#5C6BC0"), // blue
Color.web("#3D6DA3"), // blue
Color.web("#283593"), // dark blue
Color.web("#43A047"), // green
Color.web("#E67E22"), // orange
@@ -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,18 +80,30 @@ 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");
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%base-red%", Integer.toString((int)Math.ceil(paint.getRed() * 256)))
.replace("%base-green%", Integer.toString((int)Math.ceil(paint.getGreen() * 256)))
.replace("%base-blue%", Integer.toString((int)Math.ceil(paint.getBlue() * 256)))
.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);
@@ -0,0 +1,43 @@
/*
* 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;
public enum VersionIconType {
DEFAULT("/assets/img/grass.png"),
GRASS("/assets/img/grass.png"),
CHEST("/assets/img/chest.png"),
CHICKEN("/assets/img/chicken.png"),
COMMAND("/assets/img/command.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
FURNACE("/assets/img/furnace.png");
// Please append new items at last
private final String resourceUrl;
VersionIconType(String resourceUrl) {
this.resourceUrl = resourceUrl;
}
public String getResourceUrl() {
return resourceUrl;
}
}
@@ -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
@@ -21,12 +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.*;
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;
@@ -34,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
@@ -43,8 +50,6 @@ import java.util.stream.Collectors;
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public transient String id;
private boolean global = false;
public boolean isGlobal() {
@@ -66,7 +71,7 @@ public final class VersionSetting implements Cloneable {
* 1. Global settings.
* 2. Version settings.
* If a version claims that it uses global settings, its version setting will be disabled.
*
* <p>
* Defaults false because if one version uses global first, custom version file will not be generated.
*/
public boolean isUsesGlobal() {
@@ -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,10 +129,47 @@ public final class VersionSetting implements Cloneable {
return defaultJavaPathProperty.get();
}
public StringProperty defaultJavaPathPropertyProperty() {
return defaultJavaPathProperty;
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
/**
* 0 - .minecraft/versions/&lt;version&gt;/natives/<br/>
*/
private final ObjectProperty<NativesDirectoryType> nativesDirTypeProperty = new SimpleObjectProperty<>(this, "nativesDirType", NativesDirectoryType.VERSION_FOLDER);
public ObjectProperty<NativesDirectoryType> nativesDirTypeProperty() {
return nativesDirTypeProperty;
}
public NativesDirectoryType getNativesDirType() {
return nativesDirTypeProperty.get();
}
public void setNativesDirType(NativesDirectoryType nativesDirType) {
nativesDirTypeProperty.set(nativesDirType);
}
// Path to lwjgl natives directory
private final StringProperty nativesDirProperty = new SimpleStringProperty(this, "nativesDirProperty", "");
public StringProperty nativesDirProperty() {
return nativesDirProperty;
}
public String getNativesDir() {
return nativesDirProperty.get();
}
public void setNativesDir(String nativesDir) {
nativesDirProperty.set(nativesDir);
}
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public StringProperty javaDirProperty() {
@@ -204,6 +255,20 @@ public final class VersionSetting implements Cloneable {
minMemoryProperty.set(minMemory);
}
private final BooleanProperty autoMemory = new SimpleBooleanProperty(this, "autoMemory", true);
public boolean isAutoMemory() {
return autoMemory.get();
}
public BooleanProperty autoMemoryProperty() {
return autoMemory;
}
public void setAutoMemory(boolean autoMemory) {
this.autoMemory.set(autoMemory);
}
private final StringProperty preLaunchCommandProperty = new SimpleStringProperty(this, "precalledCommand", "");
public StringProperty preLaunchCommandProperty() {
@@ -222,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", "");
@@ -336,7 +419,7 @@ public final class VersionSetting implements Cloneable {
/**
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* <p>
* Format: ip:port or without port.
*/
public String getServerIp() {
@@ -373,7 +456,7 @@ public final class VersionSetting implements Cloneable {
/**
* The width of Minecraft window, defaults 800.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -395,7 +478,7 @@ public final class VersionSetting implements Cloneable {
/**
* The height of Minecraft window, defaults 480.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -443,6 +526,62 @@ public final class VersionSetting implements Cloneable {
gameDirProperty.set(gameDir);
}
private final ObjectProperty<ProcessPriority> processPriorityProperty = new SimpleObjectProperty<>(this, "processPriority", ProcessPriority.NORMAL);
public ObjectProperty<ProcessPriority> processPriorityProperty() {
return processPriorityProperty;
}
public ProcessPriority getProcessPriority() {
return processPriorityProperty.get();
}
public void setProcessPriority(ProcessPriority processPriority) {
processPriorityProperty.set(processPriority);
}
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
public boolean isUseNativeGLFW() {
return useNativeGLFW.get();
}
public BooleanProperty useNativeGLFWProperty() {
return useNativeGLFW;
}
public void setUseNativeGLFW(boolean useNativeGLFW) {
this.useNativeGLFW.set(useNativeGLFW);
}
private final BooleanProperty useNativeOpenAL = new SimpleBooleanProperty(this, "nativeOpenAL", false);
public boolean isUseNativeOpenAL() {
return useNativeOpenAL.get();
}
public BooleanProperty useNativeOpenALProperty() {
return useNativeOpenAL;
}
public void setUseNativeOpenAL(boolean useNativeOpenAL) {
this.useNativeOpenAL.set(useNativeOpenAL);
}
private final ObjectProperty<VersionIconType> versionIcon = new SimpleObjectProperty<>(this, "versionIcon", VersionIconType.DEFAULT);
public VersionIconType getVersionIcon() {
return versionIcon.get();
}
public ObjectProperty<VersionIconType> versionIconProperty() {
return versionIcon;
}
public void setVersionIcon(VersionIconType versionIcon) {
this.versionIcon.set(versionIcon);
}
// launcher settings
/**
@@ -464,38 +603,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.PLATFORM);
} 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) {
@@ -511,7 +662,9 @@ public final class VersionSetting implements Cloneable {
permSizeProperty.addListener(listener);
maxMemoryProperty.addListener(listener);
minMemoryProperty.addListener(listener);
autoMemory.addListener(listener);
preLaunchCommandProperty.addListener(listener);
postExitCommand.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
@@ -524,8 +677,14 @@ public final class VersionSetting implements Cloneable {
heightProperty.addListener(listener);
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
processPriorityProperty.addListener(listener);
useNativeGLFW.addListener(listener);
useNativeOpenAL.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
versionIcon.addListener(listener);
}
@Override
@@ -539,7 +698,9 @@ public final class VersionSetting implements Cloneable {
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setAutoMemory(isAutoMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setPostExitCommand(getPostExitCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setNoJVMArgs(isNoJVMArgs());
@@ -552,7 +713,12 @@ public final class VersionSetting implements Cloneable {
versionSetting.setHeight(getHeight());
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setProcessPriority(getProcessPriority());
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
versionSetting.setVersionIcon(getVersionIcon());
return versionSetting;
}
@@ -567,11 +733,13 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
obj.addProperty("permSize", src.getPermSize());
obj.addProperty("width", src.getWidth());
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());
@@ -582,8 +750,14 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
obj.addProperty("useNativeGLFW", src.isUseNativeGLFW());
obj.addProperty("useNativeOpenAL", src.isUseNativeOpenAL());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
return obj;
}
@@ -604,6 +778,7 @@ public final class VersionSetting implements Cloneable {
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
vs.setMaxMemory(maxMemoryN);
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
vs.setPermSize(Optional.ofNullable(obj.get("permSize")).map(JsonElement::getAsString).orElse(""));
vs.setWidth(Optional.ofNullable(obj.get("width")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
@@ -613,14 +788,20 @@ public final class VersionSetting implements Cloneable {
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(LauncherVisibility.HIDE.ordinal())]);
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setVersionIcon(VersionIconType.values()[Optional.ofNullable(obj.get("versionIcon")).map(JsonElement::getAsInt).orElse(0)]);
return vs;
}
@@ -3,7 +3,10 @@ package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public class AwtUtils {
public final class AwtUtils {
private AwtUtils() {
}
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
@@ -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
@@ -17,10 +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;
@@ -28,30 +32,35 @@ 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.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.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
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.account.AccountListPage;
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;
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.VersionPage;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File;
import java.util.List;
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;
@@ -61,10 +70,32 @@ public final class Controllers {
private static Scene scene;
private static Stage stage;
private static VersionPage versionPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static RootPage rootPage;
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
return accountListPage;
});
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
private Controllers() {
}
public static Scene getScene() {
return scene;
@@ -76,23 +107,37 @@ public final class Controllers {
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
return versionPage;
return versionPage.get();
}
// FXThread
public static GameListPage getGameListPage() {
return gameListPage.get();
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
return rootPage.get();
}
// FXThread
public static AuthlibInjectorServersPage getServersPage() {
if (serversPage == null)
serversPage = new AuthlibInjectorServersPage();
return serversPage;
public static MultiplayerPage getMultiplayerPage() {
return multiplayerPage.get();
}
// FXThread
public static LauncherSettingsPage getSettingsPage() {
return settingsPage.get();
}
// FXThread
public static AccountListPage getAccountListPage() {
return accountListPage.get();
}
// FXThread
public static DownloadPage getDownloadPage() {
return downloadPage.get();
}
// FXThread
@@ -103,13 +148,14 @@ public final class Controllers {
public static void onApplicationStop() {
config().setHeight(stageHeight.get());
config().setWidth(stageWidth.get());
stageHeight = null;
stageWidth = null;
}
public static void initialize(Stage stage) {
Logging.LOG.info("Start initializing application");
Controllers.stage = stage;
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
stage.setHeight(config().getHeight());
stageHeight.bind(stage.heightProperty());
@@ -130,16 +176,37 @@ public final class Controllers {
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
stage.setMinHeight(482 + 32);
stage.setMinWidth(802 + 32);
stage.setMinHeight(450 + 2 + 40 + 16); // bg height + border width*2 + toolbar height + shadow width*2
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.TITLE);
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) {
@@ -152,19 +219,31 @@ 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) {
dialog(text, title, type, null);
}
public static void dialog(String text, String title, MessageType type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, type, onAccept));
public static void dialog(String text, String title, MessageType type, Runnable ok) {
dialog(new MessageDialogPane.Builder(text, title, type).ok(ok).build());
}
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
public static void confirm(String text, String title, Runnable yes, Runnable no) {
confirm(text, title, MessageType.QUESTION, yes, no);
}
public static void confirm(String text, String title, MessageType type, Runnable yes, Runnable 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(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(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
@@ -195,8 +274,33 @@ 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() {
@@ -206,7 +310,8 @@ public final class Controllers {
public static void shutdown() {
rootPage = null;
versionPage = null;
serversPage = null;
gameListPage = null;
settingsPage = 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;
@@ -31,15 +29,29 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
private 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 -> {
res.set(it);
latch.countDown();
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);
Controllers.dialog(pane);
});
@@ -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
@@ -18,12 +18,11 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -34,15 +33,24 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.TransferMode;
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;
@@ -50,14 +58,26 @@ 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 java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UncheckedIOException;
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.*;
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;
@@ -68,6 +88,8 @@ import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
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 FXUtils {
private FXUtils() {
@@ -96,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;
}
@@ -107,11 +129,21 @@ 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);
}
public static InvalidationListener observeWeak(Runnable runnable, Observable... observables) {
InvalidationListener originalListener = observable -> runnable.run();
WeakInvalidationListener listener = new WeakInvalidationListener(originalListener);
for (Observable observable : observables) {
observable.addListener(listener);
}
runnable.run();
return originalListener;
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();
@@ -302,21 +334,24 @@ 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);
}
public static <T> void playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
public static <T> Animation playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
if (from == null) from = property.getValue();
if (duration == null || Objects.equals(duration, Duration.ZERO) || Objects.equals(from, to)) {
playAnimation(node, animationKey, null);
property.setValue(to);
return null;
} else {
playAnimation(node, animationKey, new Timeline(
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)),
new KeyFrame(duration, new KeyValue(property, to, interpolator))
));
);
playAnimation(node, animationKey, timeline);
return timeline;
}
}
@@ -349,16 +384,54 @@ public final class FXUtils {
}
}
public static void showFileInExplorer(Path file) {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
try {
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select", e);
}
break;
default:
// Currently unsupported.
break;
}
}
private static final String[] linuxBrowsers = {
"xdg-open",
"google-chrome",
"firefox",
"microsoft-edge",
"opera",
"konqueror",
"mozilla"
};
/**
* Open URL by java.awt.Desktop
* Open URL in browser
*
* @param link null is allowed but will be ignored
*/
public static void openLink(String link) {
if (link == null)
return;
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
if (java.awt.Desktop.isDesktopSupported()) {
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
}
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
}
}
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
@@ -370,8 +443,55 @@ public final class FXUtils {
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
}
});
});
}
}
public static void showWebDialog(String title, String content) {
showWebDialog(title, content, 800, 480);
}
public static void showWebDialog(String title, String content, int width, int height) {
try {
WebStage stage = new WebStage(width, height);
stage.getWebView().getEngine().loadContent(content);
stage.setTitle(title);
stage.showAndWait();
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
//noinspection ConstantConditions
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
final JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
frame.add(progressBar, BorderLayout.PAGE_START);
Schedulers.defaultScheduler().execute(() -> {
final JEditorPane pane = new JEditorPane("text/html", content);
pane.setEditable(false);
pane.addHyperlinkListener(event -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
openLink(event.getURL().toExternalForm());
}
});
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
});
});
frame.setVisible(true);
frame.toFront();
});
}
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
@@ -409,6 +529,7 @@ public final class FXUtils {
/**
* Bind combo box selection with given enum property bidirectionally.
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
*
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
@@ -418,6 +539,7 @@ public final class FXUtils {
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
unbindEnum(comboBox);
@SuppressWarnings("rawtypes")
ChangeListener<Number> listener = (a, b, newValue) ->
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
comboBox.getSelectionModel().select(property.getValue().ordinal());
@@ -428,12 +550,15 @@ public final class FXUtils {
/**
* Unbind combo box selection with given enum property bidirectionally.
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
*
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
* @see #bindEnum(JFXComboBox, Property)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void unbindEnum(JFXComboBox<?> comboBox) {
@SuppressWarnings("rawtypes")
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
@@ -441,6 +566,7 @@ public final class FXUtils {
/**
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
*
* @param url the url of image. The image resource should be a file within the jar.
* @return the image resource within the jar.
* @see org.jackhuang.hmcl.util.CrashReporter
@@ -510,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) {
@@ -528,4 +667,83 @@ public final class FXUtils {
popup.hide();
};
}
public static void onEscPressed(Node node, Runnable action) {
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
action.run();
e.consume();
}
});
}
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
public static WritableImage toFXImage(BufferedImage image) {
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
PixelWriter pw = wr.getPixelWriter();
final int iw = image.getWidth();
final int ih = image.getHeight();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
}
}
return wr;
}
public static void copyText(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text);
Clipboard.getSystemClipboard().setContent(content);
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.*;
@@ -50,6 +51,7 @@ public class InstallerItem extends Control {
private final String imageUrl;
public final StringProperty libraryVersion = new SimpleStringProperty();
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
public final StringProperty dependencyName = new SimpleStringProperty();
public final BooleanProperty incompatibleWithGame = new SimpleBooleanProperty();
public final BooleanProperty removable = new SimpleBooleanProperty();
public final BooleanProperty upgradable = new SimpleBooleanProperty(false);
@@ -69,6 +71,7 @@ public class InstallerItem extends Control {
imageUrl = "/assets/img/grass.png";
break;
case "fabric":
case "fabric-api":
imageUrl = "/assets/img/fabric.png";
break;
case "forge":
@@ -102,11 +105,12 @@ public class InstallerItem extends Control {
}
public static class InstallerItemGroup {
public InstallerItem game = new InstallerItem(MINECRAFT);
public InstallerItem fabric = new InstallerItem(FABRIC);
public InstallerItem forge = new InstallerItem(FORGE);
public InstallerItem liteLoader = new InstallerItem(LITELOADER);
public InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final InstallerItem game = new InstallerItem(MINECRAFT);
public final InstallerItem fabric = new InstallerItem(FABRIC);
public final InstallerItem fabricApi = new InstallerItem(FABRIC_API);
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
@@ -124,16 +128,23 @@ public class InstallerItem extends Control {
return null;
}, fabric.libraryVersion));
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
return null;
}, optiFine.libraryVersion, forge.libraryVersion));
for (InstallerItem fabric : new InstallerItem[]{fabric, fabricApi}) {
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
return null;
}, optiFine.libraryVersion, forge.libraryVersion));
}
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() == null) return FABRIC.getPatchId();
else return null;
}, fabric.libraryVersion));
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi};
}
}
@@ -143,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);
@@ -203,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());
}
};
}
@@ -61,10 +61,9 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
VBox vBox = new VBox();
{
vBox.getStyleClass().add("card-list");
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
vBox.setPadding(new Insets(15));
vBox.setSpacing(15);
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
@@ -25,17 +25,18 @@ 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;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.*;
import java.awt.*;
@@ -49,6 +50,7 @@ import java.util.ArrayDeque;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -81,13 +83,15 @@ 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;
private boolean stopCheckLogCount = false;
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"));
@@ -104,7 +108,9 @@ public final class LogWindow extends Stage {
if (!stopCheckLogCount) checkLogCount();
}
public void showGameCrashReport() {
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
this.impl.showCrashReport.set(true);
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
@@ -117,6 +123,11 @@ public final class LogWindow extends Stage {
show();
}
public void showNormal() {
this.impl.showCrashReport.set(false);
show();
}
private void shakeLogs() {
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
}
@@ -148,6 +159,7 @@ public final class LogWindow extends Stage {
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private JFXComboBox<String> cboLines = new JFXComboBox<>();
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
LogWindowImpl() {
getStyleClass().add("log-window");
@@ -204,6 +216,11 @@ public final class LogWindow extends Stage {
});
}
private void onExportGameCrashInfo() {
if (exportGameCrashInfoCallback == null) return;
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
}
@Override
protected Skin<?> createDefaultSkin() {
return new LogWindowSkin(this);
@@ -276,6 +293,7 @@ public final class LogWindow extends Stage {
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
listView.setCellFactory(x -> new ListCell<Log>() {
{
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region)listView.lookup(".clipped-container");
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
@@ -309,7 +327,15 @@ public final class LogWindow extends Stage {
}
{
BorderPane bottom = new BorderPane();
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
bottom.setLeft(exportGameCrashInfoButton);
HBox hBox = new HBox(3);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setPadding(new Insets(0, 3, 0, 3));
@@ -327,7 +353,7 @@ public final class LogWindow extends Stage {
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
vbox.getChildren().add(hBox);
vbox.getChildren().add(bottom);
}
}
}
+377 -33
View File
@@ -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
@@ -37,7 +37,8 @@ public final class SVG {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
if (fill != null) path.fillProperty().bind(fill);
if (fill != null)
path.fillProperty().bind(fill);
if (width < 0 || height < 0) {
StackPane pane = new StackPane(path);
@@ -46,45 +47,77 @@ public final class SVG {
}
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
double scale = Math.min(width / 24, height / 24);
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
}
// default fill: white, width: 20, height 20
// default fill: white, width: 24, height 24
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
return createSVGPath(
"M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z",
fill, width, height);
}
public static Node gearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z",
fill, width, height);
}
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width,
height);
}
public static Node 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", fill, width, height);
return createSVGPath(
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",
fill, width, height);
}
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z", fill, width, height);
return createSVGPath(
"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z",
fill, width, height);
}
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
return createSVGPath(
"M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z",
fill, width, height);
}
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z", fill, width, height);
return createSVGPath(
"M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z",
fill, width, height);
}
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill,
width, height);
}
public static Node deleteOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z",
fill, width, height);
}
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
return createSVGPath(
"M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z",
fill, width, height);
}
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
@@ -96,71 +129,129 @@ public final class SVG {
}
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
return createSVGPath(
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
fill, width, height);
}
public static Node launch(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
public static Node rocketLaunchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z",
fill, width, height);
}
public static Node launch2(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
public static Node launchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
fill, width, height);
}
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z", fill, width, height);
return createSVGPath(
"M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z",
fill, width, height);
}
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
return createSVGPath(
"M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z",
fill, width, height);
}
public static Node pencilOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z",
fill, width, height);
}
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
return createSVGPath(
"M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z",
fill, width, height);
}
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
return createSVGPath(
"M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z",
fill, width, height);
}
public static Node folderOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z",
fill, width, height);
}
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
return createSVGPath(
"M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z",
fill, width, height);
}
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z", fill, width, height);
return createSVGPath(
"M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z",
fill, width, height);
}
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z", fill, width, height);
return createSVGPath(
"M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z",
fill, width, height);
}
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", fill, width, height);
return createSVGPath(
"M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
fill, width, height);
}
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z", fill, width, height);
return createSVGPath(
"M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z",
fill, width, height);
}
public static Node helpCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z",
fill, width, height);
}
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
}
public static Node 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);
}
public static Node plusCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z",
fill, width, height);
}
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height);
return createSVGPath(
"M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z",
fill, width, height);
}
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z", fill, width, height);
return createSVGPath(
"M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z",
fill, width, height);
}
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
return createSVGPath(
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
fill, width, height);
}
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
@@ -172,7 +263,9 @@ public final class SVG {
}
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
return createSVGPath(
"M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z",
fill, width, height);
}
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
@@ -180,11 +273,20 @@ public final class SVG {
}
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width, height);
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width,
height);
}
public static Node wrench(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z", fill, width, height);
return createSVGPath(
"M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z",
fill, width, height);
}
public static Node wrenchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z",
fill, width, height);
}
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
@@ -192,6 +294,248 @@ public final class SVG {
}
public static Node hanger(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z", fill, width, height);
return createSVGPath(
"M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z",
fill, width, height);
}
public static Node puzzle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z",
fill, width, height);
}
public static Node cube(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z",
fill, width, height);
}
public static Node pack(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z",
fill, width, height);
}
public static Node textureBox(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z",
fill, width, height);
}
public static Node gamepad(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z",
fill, width, height);
}
public static Node fire(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z",
fill, width, height);
}
public static Node monitorScreenshot(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2",
fill, width, height);
}
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z",
fill, width, height);
}
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z",
fill, width, height);
}
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
fill, width, height);
}
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
fill, width, height);
}
public static Node informationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z",
fill, width, height);
}
public static Node handHearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z",
fill, width, height);
}
public static Node mojang(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z",
fill, width, height);
}
public static Node microsoft(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z",
fill, width, height);
}
public static Node accountOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z",
fill, width, height);
}
public static Node accountGroupOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z",
fill, width, height);
}
public static Node accountArrowRightOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z",
fill, width, height);
}
public static Node styleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z",
fill, width, height);
}
public static Node downloadOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z",
fill, width, height);
}
public static Node applicationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z",
fill, width, height);
}
public static Node earth(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
fill, width, height);
}
public static Node bell(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21",
fill, width, height);
}
public static Node contentSaveMoveOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z",
fill, width, height);
}
public static Node account(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z",
fill, width, height);
}
public static Node server(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z",
fill, width, height);
}
public static Node messageAlertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z",
fill, width, height);
}
public static Node checkCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z",
fill, width, height);
}
public static Node closeCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z",
fill, width, height);
}
public static Node clockOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z",
fill, width, height);
}
public static Node magnify(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z",
fill, width, height);
}
public static Node menuUp(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M7,15L12,10L17,15H7Z",
fill, width, height);
}
public static Node menuDown(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"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,37 +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);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
@@ -93,6 +96,15 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
return ret;
}
public static JFXButton createToolbarButton2(String text, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(creator.createIcon(Theme.blackFillBinding(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
@@ -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
@@ -24,29 +24,39 @@ 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 + remoteVersion.getChannel().channelName + ".html";
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String url = engine.getLoadWorker().getMessage().trim();
if (!url.startsWith(CHANGELOG_URL)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(url);
}
});
try {
WebEngine engine = webView.getEngine();
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)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(viewURL);
}
});
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed", e);
FXUtils.openLink(url);
}
setBody(webView);
}
@@ -60,6 +70,7 @@ public class UpgradeDialog extends JFXDialogLayout {
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
setActions(updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
}
}
@@ -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);
@@ -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
@@ -18,25 +18,34 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static javafx.beans.binding.Bindings.createStringBinding;
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@@ -45,25 +54,32 @@ public class AccountAdvancedListItem extends AdvancedListItem {
Account account = get();
if (account == null) {
titleProperty().unbind();
subtitleProperty().unbind();
imageView.imageProperty().unbind();
tooltip.textProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(newImage("/assets/img/craft_table.png"));
tooltip.setText("");
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
tooltip.setText(i18n("account.create"));
} else {
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account));
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.setText(account.getCharacter() + " " + accountTooltip(account));
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
subtitleProperty().bind(accountSubtitle(account));
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.textProperty().bind(accountTooltip(account));
}
}
};
public AccountAdvancedListItem() {
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
setActionButtonVisible(false);
setOnScroll(event -> {
Account current = account.get();
if (current == null) return;
@@ -82,23 +98,28 @@ public class AccountAdvancedListItem extends AdvancedListItem {
return account;
}
private static String accountSubtitle(Account account) {
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
private static ObservableValue<String> accountSubtitle(Account account) {
if (account instanceof AuthlibInjectorAccount) {
return ((AuthlibInjectorAccount) account).getServer().getName();
return BindingMapping.of(((AuthlibInjectorAccount) account).getServer(), AuthlibInjectorServer::getName);
} else {
return loginTypeName;
return createStringBinding(() -> getLocalizedLoginTypeName(getAccountFactory(account)));
}
}
private static String accountTooltip(Account account) {
private static ObservableValue<String> accountTooltip(Account account) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
return account.getUsername() + ", " + i18n("account.injector.server") + ": " + server.getName();
return Bindings.format("%s (%s) (%s)",
BindingMapping.of(account, Account::getCharacter),
account.getUsername(),
BindingMapping.of(server, AuthlibInjectorServer::getName));
} else if (account instanceof YggdrasilAccount) {
return account.getUsername();
return Bindings.format("%s (%s)",
BindingMapping.of(account, Account::getCharacter),
account.getUsername());
} else {
return "";
return BindingMapping.of(account, Account::getCharacter);
}
}
}
@@ -1,58 +0,0 @@
/*
* 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.account;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class AccountList extends ListPage<AccountListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
public AccountList() {
setItems(MappedObservableList.create(accounts, AccountListItem::new));
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public ListProperty<Account> accountsProperty() {
return accounts;
}
@Override
public void add() {
Controllers.dialog(new AddAccountPane());
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
}
@@ -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
@@ -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;
}
@@ -176,7 +184,7 @@ public class AccountListItem extends RadioButton {
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
if (e != null) {
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
Controllers.dialog(Accounts.localizeErrorMessage(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
}
});
}
@@ -30,9 +30,11 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
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.construct.SpinnerPane;
@@ -90,10 +92,17 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
btnRefresh.setOnMouseClicked(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
.whenComplete(ex -> spinnerRefresh.hideSpinner())
.whenComplete(Schedulers.javafx(), ex -> {
spinnerRefresh.hideSpinner();
if (ex != null) {
Controllers.showToast(Accounts.localizeErrorMessage(ex));
}
})
.start();
});
btnRefresh.getStyleClass().add("toggle-icon4");
@@ -0,0 +1,188 @@
/*
* 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.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
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.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;
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() {
items = MappedObservableList.create(accounts, AccountListItem::new);
selectedAccount = createSelectedItemPropertyFor(items, Account.class);
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public ListProperty<Account> accountsProperty() {
return accounts;
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
public ListProperty<AuthlibInjectorServer> authServersProperty() {
return authServers;
}
@Override
protected Skin<?> createDefaultSkin() {
return new AccountListPageSkin(this);
}
private static class AccountListPageSkin extends DecoratorAnimatedPageSkin<AccountListPage> {
private final ObservableList<AdvancedListItem> authServerItems;
public AccountListPageSkin(AccountListPage skinnable) {
super(skinnable);
{
VBox boxMethods = new VBox();
{
boxMethods.getStyleClass().add("advanced-list-box-content");
boxMethods.getChildren().add(new ClassTitle(i18n("account.create")));
FXUtils.setLimitWidth(boxMethods, 200);
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);
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 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);
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)));
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;
});
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
boxMethods.getChildren().add(boxAuthServers);
}
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));
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
}
ScrollPane scrollPane = new ScrollPane(boxMethods);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
setLeft(scrollPane, addAuthServerItem);
}
ScrollPane scrollPane = new ScrollPane();
VBox list = new VBox();
{
scrollPane.setFitToWidth(true);
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
list.getStyleClass().add("card-list");
Bindings.bindContent(list.getChildren(), skinnable.items);
scrollPane.setContent(list);
JFXScrollPane.smoothScrolling(scrollPane);
setCenter(scrollPane);
}
}
}
}
@@ -1,85 +0,0 @@
/*
* 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.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.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.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());
}
@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(AddAccountPane.accountException(e));
}
progressBar.setVisible(false);
}).start();
}
@FXML
private void onCancel() {
failed.run();
fireEvent(new DialogCloseEvent());
}
}
@@ -1,354 +0,0 @@
/*
* 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.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private Label lblUsername;
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private JFXButton btnAccept;
@FXML private JFXButton btnAddServer;
@FXML private JFXButton btnManageServer;
@FXML private SpinnerPane acceptPane;
@FXML private HBox linksContainer;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();
public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(Accounts.FACTORIES);
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
// try selecting the preferred login type
cboType.getSelectionModel().select(
cboType.getItems().stream()
.filter(type -> Accounts.getLoginType(type).equals(config().getPreferredLoginType()))
.findFirst()
.orElse(Accounts.FACTORY_OFFLINE));
btnAddServer.visibleProperty().bind(cboServers.visibleProperty());
btnManageServer.visibleProperty().bind(cboServers.visibleProperty());
cboServers.getItems().addListener(onInvalidating(this::checkIfNoServer));
checkIfNoServer();
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
// remember the last used login type
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
txtUsername.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresUsername, loginType));
lblUsername.visibleProperty().bind(txtUsername.visibleProperty());
txtPassword.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresPassword, loginType));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !( // consider the opposite situation: input is valid
(!txtUsername.isVisible() || txtUsername.validate()) &&
// invisible means the field is not needed, neither should it be validated
(!txtPassword.isVisible() || txtPassword.validate()) &&
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
),
txtUsername.textProperty(), txtPassword.textProperty(),
loginType, cboServers.getSelectionModel().selectedItemProperty(),
txtPassword.visibleProperty(), cboServers.visibleProperty()));
// authlib-injector links
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
.map(AddAccountPane::createHyperlinks)
.map(FXCollections::observableList));
Bindings.bindContent(linksContainer.getChildren(), links);
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
}
private boolean validateUsername(String username) {
AccountFactory<?> loginType = cboType.getSelectionModel().getSelectedItem();
if (loginType == Accounts.FACTORY_OFFLINE) {
return true;
} else if (loginType == Accounts.FACTORY_AUTHLIB_INJECTOR) {
AuthlibInjectorServer server = cboServers.getSelectionModel().getSelectedItem();
if (server != null && server.isNonEmailLogin()) {
return true;
}
}
return username.contains("@");
}
private static final String[] ALLOWED_LINKS = { "register" };
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
if (server == null) {
return emptyList();
}
Map<String, String> links = server.getLinks();
List<Hyperlink> result = new ArrayList<>();
for (String key : ALLOWED_LINKS) {
String value = links.get(key);
if (value != null) {
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
FXUtils.installSlowTooltip(link, value);
link.setOnAction(e -> FXUtils.openLink(value));
result.add(link);
}
}
return unmodifiableList(result);
}
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
}
}
private void checkIfNoServer() {
if (cboServers.getItems().isEmpty())
cboServers.getStyleClass().setAll("jfx-combo-box-warning");
else
cboServers.getStyleClass().setAll("jfx-combo-box");
}
/**
* Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}.
*/
private Object getAuthAdditionalData() {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
if (factory == Accounts.FACTORY_AUTHLIB_INJECTOR) {
return requireNonNull(cboServers.getSelectionModel().getSelectedItem(), "selected server cannot be null");
}
return null;
}
@FXML
private void onCreationAccept() {
if (btnAccept.isDisabled())
return;
acceptPane.showSpinner();
lblCreationWarning.setText("");
setDisable(true);
String username = txtUsername.getText();
String password = txtPassword.getText();
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.supplyAsync(() -> factory.create(new Selector(), username, password, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
// adding an already-added account
// instead of discarding the new account, we first remove the existing one then add the new one
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
// select the new account
Accounts.setSelectedAccount(account);
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblCreationWarning.setText(accountException(exception));
}
setDisable(false);
acceptPane.hideSpinner();
}).start();
}
@FXML
private void onCreationCancel() {
fireEvent(new DialogCloseEvent());
}
@FXML
private void onManageInjecterServers() {
fireEvent(new DialogCloseEvent());
Controllers.navigate(Controllers.getServersPage());
}
@FXML
private void onAddInjecterServer() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
private static class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
public Selector() {
setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnMouseClicked(e -> latch.countDown());
listBox.startCategory(i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
}
@Override
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
Platform.runLater(() -> {
for (GameProfile profile : profiles) {
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
Controllers.dialog(this);
});
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException();
return selectedProfile;
} catch (InterruptedException ignore) {
throw new NoSelectedCharacterException();
} finally {
runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
}
}
}
public static String accountException(Exception exception) {
if (exception instanceof NoCharacterException) {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return i18n("account.failed.connect_authentication_server");
} else if (exception instanceof ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials"))
return i18n("account.failed.invalid_credentials");
else if (remoteMessage.contains("Invalid token"))
return i18n("account.failed.invalid_token");
else if (remoteMessage.contains("Invalid username or password"))
return i18n("account.failed.invalid_password");
else
return remoteMessage;
}
return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) {
return i18n("account.failed.injector_download_failure");
} else if (exception instanceof CharacterDeletedException) {
return i18n("account.failed.character_deleted");
} else if (exception instanceof InvalidSkinException) {
return i18n("account.skin.invalid_skin");
} else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
}
@@ -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
@@ -38,6 +38,7 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
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;
@@ -68,6 +69,8 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
onEscPressed(this, this::onAddCancel);
}
@Override
@@ -1,67 +0,0 @@
/*
* 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.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import java.util.function.Consumer;
public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServer server;
private final Label lblServerName = new Label();
private final Label lblServerUrl = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.server = server;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerUrl.setStyle("-fx-font-size: 10;");
VBox center = new VBox();
BorderPane.setAlignment(center, Pos.CENTER);
center.getChildren().addAll(lblServerName, lblServerUrl);
setCenter(center);
JFXButton right = new JFXButton();
right.setOnMouseClicked(e -> deleteCallback.accept(this));
right.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(right, Pos.CENTER);
right.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
setRight(right);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.textProperty().bind(Bindings.createStringBinding(server::getName, server));
lblServerUrl.setText(server.getUrl());
}
public AuthlibInjectorServer getServer() {
return server;
}
}
@@ -1,56 +0,0 @@
/*
* 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.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.injector.manage.title")));
private final ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(itemsProperty(), serverItems);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
return new AuthlibInjectorServerItem(server,
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@Override
public void add() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
@Override
public ReadOnlyObjectWrapper<State> stateProperty() {
return state;
}
}
@@ -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());
}
}
@@ -0,0 +1,675 @@
/*
* 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.*;
import com.jfoenix.validation.base.ValidatorBase;
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;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextInputControl;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
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.OAuthServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
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;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
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;
import static java.util.Collections.unmodifiableList;
import static javafx.beans.binding.Bindings.bindContent;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.classPropertyFor;
public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private boolean showMethodSwitcher;
private AccountFactory<?> factory;
private final Label lblErrorMessage;
private final JFXButton btnAccept;
private final SpinnerPane spinner;
private final Node body;
private Node detailsPane; // AccountDetailsInputPane for Offline / Mojang / authlib-injector, Label for Microsoft
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;
public CreateAccountPane() {
this((AccountFactory<?>) null);
}
public CreateAccountPane(AccountFactory<?> factory) {
if (factory == null) {
showMethodSwitcher = true;
String preferred = config().getPreferredLoginType();
try {
factory = Accounts.getAccountFactory(preferred);
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_OFFLINE;
}
} else {
showMethodSwitcher = false;
}
this.factory = factory;
{
String title;
if (showMethodSwitcher) {
title = "account.create";
} else {
title = "account.create." + Accounts.getLoginType(factory);
}
setHeading(new Label(i18n(title)));
}
{
lblErrorMessage = new Label();
lblErrorMessage.setWrapText(true);
lblErrorMessage.setMaxWidth(400);
btnAccept = new JFXButton(i18n("account.login"));
btnAccept.getStyleClass().add("dialog-accept");
btnAccept.setOnAction(e -> onAccept());
spinner = new SpinnerPane();
spinner.getStyleClass().add("small-spinner-pane");
spinner.setContent(btnAccept);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
btnCancel.setOnAction(e -> onCancel());
onEscPressed(this, btnCancel::fire);
HBox hbox = new HBox(spinner, btnCancel);
hbox.setAlignment(Pos.CENTER_RIGHT);
setActions(lblErrorMessage, hbox);
}
if (showMethodSwitcher) {
TabControl.Tab<?>[] tabs = new TabControl.Tab[Accounts.FACTORIES.size()];
TabControl.Tab<?> selected = null;
for (int i = 0; i < tabs.length; i++) {
AccountFactory<?> f = Accounts.FACTORIES.get(i);
tabs[i] = new TabControl.Tab<>(Accounts.getLoginType(f), Accounts.getLocalizedLoginTypeName(f));
tabs[i].setUserData(f);
if (factory == f) {
selected = tabs[i];
}
}
TabHeader tabHeader = new TabHeader(tabs);
tabHeader.getStyleClass().add("add-account-tab-header");
tabHeader.setMinWidth(USE_PREF_SIZE);
tabHeader.setMaxWidth(USE_PREF_SIZE);
tabHeader.getSelectionModel().select(selected);
onChange(tabHeader.getSelectionModel().selectedItemProperty(),
newItem -> {
if (newItem == null)
return;
AccountFactory<?> newMethod = (AccountFactory<?>) newItem.getUserData();
config().setPreferredLoginType(Accounts.getLoginType(newMethod));
this.factory = newMethod;
initDetailsPane();
});
detailsContainer = new StackPane();
detailsContainer.setPadding(new Insets(15, 0, 0, 0));
VBox boxBody = new VBox(tabHeader, detailsContainer);
boxBody.setAlignment(Pos.CENTER);
body = boxBody;
setBody(body);
} else {
detailsContainer = new StackPane();
detailsContainer.setPadding(new Insets(10, 0, 0, 0));
body = detailsContainer;
setBody(body);
}
initDetailsPane();
setPrefWidth(560);
}
public CreateAccountPane(AuthlibInjectorServer authserver) {
this(Accounts.FACTORY_AUTHLIB_INJECTOR);
((AccountDetailsInputPane) detailsPane).selectAuthServer(authserver);
}
private void onAccept() {
spinner.showSpinner();
lblErrorMessage.setText("");
if (!(factory instanceof MicrosoftAccountFactory)) {
body.setDisable(true);
}
String username;
String password;
Object additionalData;
if (detailsPane instanceof AccountDetailsInputPane) {
AccountDetailsInputPane details = (AccountDetailsInputPane) detailsPane;
username = details.getUsername();
password = details.getPassword();
additionalData = details.getAdditionalData();
} else {
username = null;
password = null;
additionalData = null;
}
logging.set(true);
deviceCode.set(null);
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
// adding an already-added account
// instead of discarding the new account, we first remove the existing one then add the new one
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
// select the new account
Accounts.setSelectedAccount(account);
spinner.hideSpinner();
fireEvent(new DialogCloseEvent());
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblErrorMessage.setText(Accounts.localizeErrorMessage(exception));
}
body.setDisable(false);
spinner.hideSpinner();
}).executor(true);
}
private void onCancel() {
if (loginTask != null) {
loginTask.cancel();
}
fireEvent(new DialogCloseEvent());
}
private void initDetailsPane() {
if (detailsPane != null) {
btnAccept.disableProperty().unbind();
detailsContainer.getChildren().remove(detailsPane);
lblErrorMessage.setText("");
}
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
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"));
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);
detailsPane = vbox;
btnAccept.setDisable(false);
} else {
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
}
detailsContainer.getChildren().add(detailsPane);
}
private static class AccountDetailsInputPane extends GridPane {
// ==== authlib-injector hyperlinks ====
private static final String[] ALLOWED_LINKS = { "register" };
private static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
if (server == null) {
return emptyList();
}
Map<String, String> links = server.getLinks();
List<Hyperlink> result = new ArrayList<>();
for (String key : ALLOWED_LINKS) {
String value = links.get(key);
if (value != null) {
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
FXUtils.installSlowTooltip(link, value);
link.setOnAction(e -> FXUtils.openLink(value));
result.add(link);
}
}
return unmodifiableList(result);
}
// =====
private AccountFactory<?> factory;
private @Nullable JFXComboBox<AuthlibInjectorServer> cboServers;
private @Nullable JFXTextField txtUsername;
private @Nullable JFXPasswordField txtPassword;
private @Nullable JFXTextField txtUUID;
private BooleanBinding valid;
public AccountDetailsInputPane(AccountFactory<?> factory, Runnable onAction) {
this.factory = factory;
setVgap(22);
setHgap(15);
setAlignment(Pos.CENTER);
ColumnConstraints col0 = new ColumnConstraints();
col0.setMinWidth(USE_PREF_SIZE);
getColumnConstraints().add(col0);
ColumnConstraints col1 = new ColumnConstraints();
col1.setHgrow(Priority.ALWAYS);
getColumnConstraints().add(col1);
int rowIndex = 0;
if (factory instanceof AuthlibInjectorAccountFactory) {
Label lblServers = new Label(i18n("account.injector.server"));
setHalignment(lblServers, HPos.LEFT);
add(lblServers, 0, rowIndex);
cboServers = new JFXComboBox<>();
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(
() -> Platform.runLater( // the selection will not be updated as expected if we call it immediately
cboServers.getSelectionModel()::selectFirst)));
cboServers.getSelectionModel().selectFirst();
cboServers.setPromptText(i18n("account.injector.empty"));
BooleanBinding noServers = createBooleanBinding(cboServers.getItems()::isEmpty, cboServers.getItems());
classPropertyFor(cboServers, "jfx-combo-box-warning").bind(noServers);
classPropertyFor(cboServers, "jfx-combo-box").bind(noServers.not());
HBox.setHgrow(cboServers, Priority.ALWAYS);
HBox.setMargin(cboServers, new Insets(0, 10, 0, 0));
cboServers.setMaxWidth(Double.MAX_VALUE);
HBox linksContainer = new HBox();
linksContainer.setAlignment(Pos.CENTER);
onChangeAndOperate(cboServers.valueProperty(), server -> linksContainer.getChildren().setAll(createHyperlinks(server)));
linksContainer.setMinWidth(USE_PREF_SIZE);
JFXButton btnAddServer = new JFXButton();
btnAddServer.setGraphic(SVG.plus(Theme.blackFillBinding(), 20, 20));
btnAddServer.getStyleClass().add("toggle-icon4");
btnAddServer.setOnAction(e -> {
Controllers.dialog(new AddAuthlibInjectorServerPane());
});
HBox boxServers = new HBox(cboServers, linksContainer, btnAddServer);
add(boxServers, 1, rowIndex);
rowIndex++;
}
if (factory.getLoginType().requiresUsername) {
Label lblUsername = new Label(i18n("account.username"));
setHalignment(lblUsername, HPos.LEFT);
add(lblUsername, 0, rowIndex);
txtUsername = new JFXTextField();
txtUsername.setValidators(
new RequiredValidator(),
new Validator(i18n("input.email"), username -> {
if (requiresEmailAsUsername()) {
return username.contains("@");
} else {
return true;
}
}));
setValidateWhileTextChanged(txtUsername, true);
txtUsername.setOnAction(e -> onAction.run());
add(txtUsername, 1, rowIndex);
rowIndex++;
}
if (factory.getLoginType().requiresPassword) {
Label lblPassword = new Label(i18n("account.password"));
setHalignment(lblPassword, HPos.LEFT);
add(lblPassword, 0, rowIndex);
txtPassword = new JFXPasswordField();
txtPassword.setValidators(new RequiredValidator());
setValidateWhileTextChanged(txtPassword, true);
txtPassword.setOnAction(e -> onAction.run());
add(txtPassword, 1, rowIndex);
rowIndex++;
}
if (factory instanceof YggdrasilAccountFactory) {
HBox box = new HBox();
GridPane.setColumnSpan(box, 2);
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);
advancedButton.setText(i18n("settings.advanced"));
GridPane.setColumnSpan(box, 2);
add(box, 0, rowIndex);
rowIndex++;
Label lblUUID = new Label(i18n("account.methods.offline.uuid"));
lblUUID.managedProperty().bind(advancedButton.selectedProperty());
lblUUID.visibleProperty().bind(advancedButton.selectedProperty());
setHalignment(lblUUID, HPos.LEFT);
add(lblUUID, 0, rowIndex);
txtUUID = new JFXTextField();
txtUUID.managedProperty().bind(advancedButton.selectedProperty());
txtUUID.visibleProperty().bind(advancedButton.selectedProperty());
txtUUID.setValidators(new UUIDValidator());
txtUUID.promptTextProperty().bind(BindingMapping.of(txtUsername.textProperty()).map(name -> OfflineAccountFactory.getUUIDFromUserName(name).toString()));
txtUUID.setOnAction(e -> onAction.run());
add(txtUUID, 1, rowIndex);
rowIndex++;
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.managedProperty().bind(advancedButton.selectedProperty());
hintPane.visibleProperty().bind(advancedButton.selectedProperty());
hintPane.setText(i18n("account.methods.offline.uuid.hint"));
GridPane.setColumnSpan(hintPane, 2);
add(hintPane, 0, rowIndex);
rowIndex++;
}
valid = new BooleanBinding() {
{
if (cboServers != null)
bind(cboServers.valueProperty());
if (txtUsername != null)
bind(txtUsername.textProperty());
if (txtPassword != null)
bind(txtPassword.textProperty());
if (txtUUID != null)
bind(txtUUID.textProperty());
}
@Override
protected boolean computeValue() {
if (cboServers != null && cboServers.getValue() == null)
return false;
if (txtUsername != null && !txtUsername.validate())
return false;
if (txtPassword != null && !txtPassword.validate())
return false;
if (txtUUID != null && !txtUUID.validate())
return false;
return true;
}
};
}
private boolean requiresEmailAsUsername() {
if (factory instanceof YggdrasilAccountFactory) {
return true;
} else if ((factory instanceof AuthlibInjectorAccountFactory) && cboServers != null) {
AuthlibInjectorServer server = cboServers.getValue();
if (server != null && !server.isNonEmailLogin()) {
return true;
}
}
return false;
}
public Object getAdditionalData() {
if (factory instanceof AuthlibInjectorAccountFactory) {
return getAuthServer();
} else if (factory instanceof OfflineAccountFactory) {
UUID uuid = txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
return new OfflineAccountFactory.AdditionalData(uuid, null);
} else {
return null;
}
}
public @Nullable AuthlibInjectorServer getAuthServer() {
return cboServers == null ? null : cboServers.getValue();
}
public @Nullable String getUsername() {
return txtUsername == null ? null : txtUsername.getText();
}
public @Nullable String getPassword() {
return txtPassword == null ? null : txtPassword.getText();
}
public BooleanBinding validProperty() {
return valid;
}
public void selectAuthServer(AuthlibInjectorServer authserver) {
cboServers.getSelectionModel().select(authserver);
}
public void focus() {
if (txtUsername != null) {
txtUsername.requestFocus();
}
}
}
private static class DialogCharacterSelector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null;
public DialogCharacterSelector() {
setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel"));
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnAction(e -> latch.countDown());
listBox.startCategory(i18n("account.choose"));
setCenter(listBox);
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_RIGHT);
hbox.getChildren().add(cancel);
setBottom(hbox);
onEscPressed(this, cancel::fire);
}
@Override
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
Platform.runLater(() -> {
for (GameProfile profile : profiles) {
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
FXUtils.limitSize(portraitView, 32, 32);
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
});
listBox.add(accountItem);
}
Controllers.dialog(this);
});
try {
latch.await();
if (selectedProfile == null)
throw new NoSelectedCharacterException();
return selectedProfile;
} catch (InterruptedException ignored) {
throw new NoSelectedCharacterException();
} finally {
Platform.runLater(() -> fireEvent(new DialogCloseEvent()));
}
}
}
@Override
public void onDialogShown() {
if (detailsPane instanceof AccountDetailsInputPane) {
((AccountDetailsInputPane) detailsPane).focus();
}
}
private static class UUIDValidator extends ValidatorBase {
public UUIDValidator() {
this(i18n("account.methods.offline.uuid.malformed"));
}
public UUIDValidator(@NamedArg("message") String message) {
super(message);
}
@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(false);
return;
}
try {
UUIDTypeAdapter.fromString(textField.getText());
hasErrors.set(false);
} catch (IllegalArgumentException ignored) {
hasErrors.set(true);
}
}
}
private static final String MICROSOFT_ACCOUNT_EDIT_PROFILE_URL = "https://support.microsoft.com/zh-cn/account-billing/%E5%A6%82%E4%BD%95%E6%9B%B4%E6%94%B9-microsoft-%E5%B8%90%E6%88%B7%E4%B8%8A%E7%9A%84%E5%87%BA%E7%94%9F%E6%97%A5%E6%9C%9F-837badbc-999e-54d2-2617-d19206b9540a";
}
@@ -1,72 +0,0 @@
/*
* 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.account;
import javafx.application.Platform;
import javafx.stage.Modality;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.ui.WebStage;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import static org.jackhuang.hmcl.Launcher.COOKIE_MANAGER;
public class MicrosoftAccountLoginStage extends WebStage implements MicrosoftService.WebViewCallback {
public static final MicrosoftAccountLoginStage INSTANCE = new MicrosoftAccountLoginStage();
CompletableFuture<String> future;
Predicate<String> urlTester;
public MicrosoftAccountLoginStage() {
super(600, 600);
initModality(Modality.APPLICATION_MODAL);
titleProperty().bind(webEngine.titleProperty());
webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
if (urlTester != null && urlTester.test(newValue)) {
future.complete(newValue);
hide();
}
});
showingProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
if (future != null) {
future.completeExceptionally(new InterruptedException());
}
future = null;
urlTester = null;
}
});
}
@Override
public CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL) {
Platform.runLater(() -> {
COOKIE_MANAGER.getCookieStore().removeAll();
webEngine.load(initialURL);
show();
});
this.future = new CompletableFuture<>();
this.urlTester = urlTester;
return future;
}
}
@@ -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;
}
}
}
@@ -23,7 +23,10 @@ import javafx.util.Duration;
public interface AnimationHandler {
Duration getDuration();
Pane getCurrentRoot();
Node getPreviousNode();
Node getCurrentNode();
}
@@ -173,7 +173,49 @@ public enum ContainerAnimations {
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))));
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
SWIPE_LEFT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
SWIPE_RIGHT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))));
private final AnimationProducer animationProducer;
private ContainerAnimations opposite;
@@ -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,63 +19,61 @@ 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;
public class AdvancedListBox extends ScrollPane {
private final VBox container = new VBox();
import java.util.function.Consumer;
public class AdvancedListBox extends VBox {
{
setContent(container);
FXUtils.smoothScrolling(this);
setFitToHeight(true);
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.setSpacing(5);
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;
}
public AdvancedListBox addNavigationDrawerItem(Consumer<AdvancedListItem> fn) {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setActionButtonVisible(false);
fn.accept(item);
return add(item);
}
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)
@@ -90,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();
}
}
@@ -24,10 +24,16 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.util.Pair.pair;
public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
@@ -39,16 +45,16 @@ public class AdvancedListItem extends Control {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
public Image getImage() {
return image.get();
public Node getLeftGraphic() {
return leftGraphic.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
public ObjectProperty<Node> leftGraphicProperty() {
return leftGraphic;
}
public void setImage(Image image) {
this.image.set(image);
public void setLeftGraphic(Node leftGraphic) {
this.leftGraphic.set(leftGraphic);
}
public Node getRightGraphic() {
@@ -134,4 +140,21 @@ public class AdvancedListItem extends Control {
protected Skin<?> createDefaultSkin() {
return new AdvancedListItemSkin(this);
}
public static Pair<Node, ImageView> createImageView(Image image) {
return createImageView(image, 32, 32);
}
public static Pair<Node, ImageView> createImageView(Image image, double width, double height) {
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, width);
FXUtils.setLimitHeight(imageViewContainer, height);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, width, height);
imageView.setPreserveRatio(true);
imageView.setImage(image);
imageViewContainer.getChildren().setAll(imageView);
return pair(imageViewContainer, imageView);
}
}
@@ -18,16 +18,10 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -36,60 +30,37 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
public AdvancedListItemSkin(AdvancedListItem skinnable) {
super(skinnable);
StackPane stackPane = new StackPane();
stackPane.getStyleClass().add("container");
RipplerContainer container = new RipplerContainer(stackPane);
FXUtils.onChangeAndOperate(skinnable.activeProperty(), active -> {
skinnable.pseudoClassStateChanged(SELECTED, active);
});
BorderPane root = new BorderPane();
root.getStyleClass().add("container");
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
HBox left = new HBox();
left.setAlignment(Pos.CENTER);
left.setMouseTransparent(true);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
TwoLineListItem item = new TwoLineListItem();
root.setCenter(item);
item.setMouseTransparent(true);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.imageProperty().bind(skinnable.imageProperty());
imageViewContainer.getChildren().setAll(imageView);
VBox vbox = new VBox();
vbox.setAlignment(Pos.CENTER_LEFT);
vbox.setPadding(new Insets(0, 0, 0, 10));
Label title = new Label();
title.textProperty().bind(skinnable.titleProperty());
title.setMaxWidth(90);
title.setStyle("-fx-font-size: 15;");
title.setTextAlignment(TextAlignment.JUSTIFY);
vbox.getChildren().add(title);
Label subtitle = new Label();
subtitle.textProperty().bind(skinnable.subtitleProperty());
subtitle.setMaxWidth(90);
subtitle.setStyle("-fx-font-size: 10;");
subtitle.setTextAlignment(TextAlignment.JUSTIFY);
vbox.getChildren().add(subtitle);
FXUtils.onChangeAndOperate(skinnable.subtitleProperty(), subtitleString -> {
if (subtitleString == null) vbox.getChildren().setAll(title);
else vbox.getChildren().setAll(title, subtitle);
});
left.getChildren().setAll(imageViewContainer, vbox);
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
left.getChildren().clear();
} else {
left.getChildren().setAll(newGraphic);
}
});
root.setLeft(left);
HBox right = new HBox();
right.setAlignment(Pos.CENTER);
right.setMouseTransparent(true);
right.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(right, 40);
FXUtils.onChangeAndOperate(skinnable.rightGraphicProperty(),
@@ -100,16 +71,10 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
right.getChildren().setAll(newGraphic);
}
});
root.setRight(right);
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().add("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
getChildren().setAll(container);
}
}
@@ -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");
}
}
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
@@ -29,10 +27,16 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.function.Consumer;
@@ -105,6 +109,7 @@ public class ComponentList extends Control {
public void onExpand() {
if (!expanded && lazyInitializer != null) {
lazyInitializer.accept(this);
setNeedsLayout(true);
}
expanded = true;
@@ -129,6 +134,12 @@ public class ComponentList extends Control {
list = MappedObservableList.create(control.getContent(), node -> {
ComponentListCell cell = new ComponentListCell(node);
cell.getStyleClass().add("options-list-item");
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;
});
@@ -153,8 +164,24 @@ public class ComponentList extends Control {
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
VBox vbox = new VBox();
vbox.setFillWidth(true);
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
}
}
public static Node createComponentListTitle(String title) {
HBox node = new HBox();
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(8, 0, 0, 0));
{
Label advanced = new Label(title);
node.getChildren().setAll(advanced);
}
return node;
}
public static void setVgrow(Node node, Priority priority) {
node.getProperties().put("ComponentList.vgrow", priority);
}
}
@@ -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,14 +76,17 @@ class ComponentListCell extends StackPane {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
BorderPane groupNode = new BorderPane();
getStyleClass().add("no-padding");
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
VBox groupNode = new VBox();
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
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);
}
}
@@ -17,11 +17,13 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.effects.JFXDepthManager;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.ListCell;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -30,7 +32,7 @@ public abstract class FloatListCell<T> extends ListCell<T> {
protected final StackPane pane = new StackPane();
{
public FloatListCell(JFXListView<T> listView) {
setText(null);
setGraphic(null);
@@ -42,6 +44,14 @@ public abstract class FloatListCell<T> extends ListCell<T> {
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
pane.pseudoClassStateChanged(SELECTED, selected);
});
Region clippedContainer = (Region) listView.lookup(".clipped-container");
setPrefWidth(0);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
}
@Override
@@ -99,9 +99,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
// we're getting dragged without getting a mouse press
dragStart = thumb.localToParent(me.getX(), me.getY());
}
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY() : cur.getX() - dragStart.getX();
double position = preDragThumbPos + dragPos / (trackLength() - thumbLength());
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus();
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable())
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
@@ -0,0 +1,96 @@
/*
* 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.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
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;
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().addAll("hint", type.name().toLowerCase());
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_LEFT);
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));
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
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,114 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.StringUtils;
public class IconedTwoLineListItem extends HBox {
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ImageView imageView = new ImageView();
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
private final Lazy<JFXButton> externalLinkButton = new Lazy<>(() -> {
JFXButton button = new JFXButton();
button.getStyleClass().add("toggle-icon4");
button.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
button.setOnAction(e -> FXUtils.openLink(externalLink.get()));
return button;
});
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener observer;
public IconedTwoLineListItem() {
setAlignment(Pos.CENTER);
setSpacing(16);
imageView.imageProperty().bind(image);
twoLineListItem.titleProperty().bind(title);
twoLineListItem.subtitleProperty().bind(subtitle);
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
Bindings.bindContent(twoLineListItem.getTags(), tags);
observer = FXUtils.observeWeak(() -> {
getChildren().clear();
if (image.get() != null) getChildren().add(imageView);
getChildren().add(twoLineListItem);
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(externalLinkButton.get());
}, image, externalLink);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public ObservableList<String> getTags() {
return tags;
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public String getExternalLink() {
return externalLink.get();
}
public StringProperty externalLinkProperty() {
return externalLink;
}
public void setExternalLink(String externalLink) {
this.externalLink.set(externalLink);
}
public Image getImage() {
return image.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
}
public void setImage(Image image) {
this.image.set(image);
}
public ImageView getImageView() {
return imageView;
}
}
@@ -54,12 +54,12 @@ public final class ImagePickerItem extends BorderPane {
imageView.setPreserveRatio(true);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 20, 20));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 20, 20));
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
@@ -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());
}
}
@@ -0,0 +1,38 @@
/*
* 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.Hyperlink;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
public class JFXHyperlink extends Hyperlink {
public JFXHyperlink() {
super();
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
}
public JFXHyperlink(String text) {
super(text);
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
}
}
Loaded 100 of 509 files, more files were not shown because too many files have changed in this diff. Show more