Compare commits

..
301 Commits
Author SHA1 Message Date
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
319 changed files with 20574 additions and 5123 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) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
+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
+16 -4
View File
@@ -1,6 +1,6 @@
name: Java CI
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -8,11 +8,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.8
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
- name: Build with Gradle
run: ./gradlew build
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: HMCL
path: HMCL/build/libs
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
run: ./gradlew checkstyleTest
+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
+9 -2
View File
@@ -11,7 +11,6 @@ hs_err_pid*
*.log
.mine*
NVIDIA
*.json
# gradle build
/build/
@@ -38,4 +37,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
+24 -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,24 +21,30 @@ 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") ?: ""
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation project(":JSTUN")
implementation rootProject.files("lib/JFoenix.jar")
}
@@ -96,8 +104,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
@@ -113,10 +123,13 @@ shadowJar {
classifier = null
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,
'Class-Path': 'pack200.jar',
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
@@ -161,8 +174,9 @@ 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 {
main = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
@@ -207,4 +221,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'
@@ -36,6 +36,7 @@ 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;
@@ -160,10 +161,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;
@@ -43,10 +43,18 @@ 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();
@@ -60,12 +68,19 @@ public final class Main {
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("!")) {
@@ -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
@@ -29,10 +29,13 @@ 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";
@@ -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
@@ -37,6 +37,7 @@ 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 java.io.File;
import java.io.IOException;
@@ -106,6 +107,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
} catch (IOException ex) {
Logging.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) {
@@ -129,7 +133,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
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"));
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
}
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -235,10 +241,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
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())))
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
@@ -281,10 +284,6 @@ 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 {
VersionSetting vs = getVersionSetting(version);
@@ -292,12 +291,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.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())
@@ -306,7 +308,12 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
.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 +371,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;
}
}
@@ -31,15 +31,17 @@ 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);
@@ -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,6 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
@@ -47,27 +48,36 @@ import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.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.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Queue;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -129,7 +139,7 @@ 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);
@@ -172,7 +182,7 @@ public final class LauncherHelper {
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
return new HMCLGameLauncher(
repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
version,
authInfo,
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE
@@ -307,7 +317,7 @@ public final class LauncherHelper {
// Without onAccept called, the launching operation will be terminated.
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept);
@@ -325,14 +335,61 @@ public final class LauncherHelper {
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} 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());
MessageDialogPane dialog = new MessageDialogPane(
i18n("launch.advice.require_newer_java_version",
gameVersion.toString(),
version.getJavaVersion().getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION);
JFXButton linkButton = new JFXButton(i18n("download.external_link"));
linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/"));
linkButton.getStyleClass().add("dialog-accept");
dialog.addButton(linkButton);
JFXButton yesButton = new JFXButton(i18n("button.ok"));
yesButton.setOnAction(event -> {
downloadJava(version.getJavaVersion(), profile)
.thenAcceptAsync(x -> {
try {
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
newAcceptableJava.ifPresent(setting::setJavaVersion);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Cannot list javas", e);
}
}, Platform::runLater).thenAccept(x -> onAccept.run());
});
yesButton.getStyleClass().add("dialog-accept");
dialog.addButton(yesButton);
JFXButton noButton = new JFXButton(i18n("button.cancel"));
noButton.getStyleClass().add("dialog-cancel");
dialog.addButton(noButton);
dialog.setCancelButton(noButton);
Controllers.dialog(dialog);
flag = true;
}
}
}
// Game later than 1.17 requires Java 16.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_16 && gameVersion.compareTo(VersionNumber.asVersion("1.17")) >= 0) {
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_16)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} else {
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
FXUtils.openLink("https://adoptopenjdk.net/");
}, null);
}
flag = true;
}
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
@@ -393,7 +450,7 @@ public final class LauncherHelper {
// 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.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.getPlatform())
.filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
@@ -402,7 +459,7 @@ public final class LauncherHelper {
// 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 -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.getPlatform())
.filter(javaVersion -> {
if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8;
if (newJavaRequiredFinal) return javaVersion.getParsedVersion() >= JavaVersion.JAVA_8;
@@ -477,7 +534,7 @@ public final class LauncherHelper {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(executor.getException());
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
} else {
future.complete(null);
}
@@ -545,9 +602,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<>();
@@ -566,7 +621,7 @@ public final class LauncherHelper {
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
logWindow.showNormal();
logWindowLatch.countDown();
});
}
@@ -653,21 +708,37 @@ public final class LauncherHelper {
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();
switch (exitType) {
case JVM_ERROR:
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
break;
}
logWindow.showGameCrashReport(logs -> {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
LogExporter.exportLogs(logFile, repository, version, logs, new CommandBuilder().addAll(process.getCommands()).toString())
.thenRunAsync(() -> {
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
}, Schedulers.javafx())
.exceptionally(e -> {
LOG.log(Level.WARNING, "Failed to export game crash info", e);
return null;
});
});
});
}
@@ -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,129 @@
/*
* 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.microsoft.MicrosoftService;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Logging;
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.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 MicrosoftAuthenticationServer extends NanoHTTPD implements MicrosoftService.OAuthSession {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
public static String lastlyOpenedURL;
private MicrosoftAuthenticationServer(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 Response serve(IHTTPSession session) {
if (session.getMethod() != Method.GET || !"/auth-response".equals(session.getUri())) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
Map<String, String> query = mapOf(NetworkUtils.parseQuery(session.getQueryParameterString()));
if (query.containsKey("code")) {
future.complete(query.get("code"));
} else {
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(MicrosoftAuthenticationServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} 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 MicrosoftService.OAuthCallback {
@Override
public MicrosoftService.OAuthSession startServer() throws IOException {
IOException exception = null;
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
try {
MicrosoftAuthenticationServer server = new MicrosoftAuthenticationServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
return server;
} catch (IOException e) {
exception = e;
}
}
throw exception;
}
@Override
public void openBrowser(String url) throws IOException {
lastlyOpenedURL = url;
FXUtils.openLink(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(""));
}
}
}
@@ -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
@@ -27,22 +27,22 @@ 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.CharacterDeletedException;
import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
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.MicrosoftAuthenticationServer;
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;
@@ -84,8 +84,8 @@ public final class Accounts {
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
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(new MicrosoftAuthenticationServer.Factory()));
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 +328,53 @@ 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;
}
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.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
}
@@ -122,8 +122,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<>());
@@ -383,6 +395,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 +431,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;
}
@@ -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,99 @@
*/
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.net.SocketTimeoutException;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
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 +123,34 @@ 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(Exception 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 {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
}
return StringUtils.getStackTrace(exception);
}
}
@@ -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) {
@@ -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;
@@ -41,7 +41,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
@@ -87,6 +87,9 @@ public class Theme {
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()))
@@ -22,6 +22,8 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
@@ -43,8 +45,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 +66,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() {
@@ -119,6 +119,39 @@ public final class VersionSetting implements Cloneable {
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 +237,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() {
@@ -336,7 +383,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 +420,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 +442,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 +490,48 @@ 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);
}
// launcher settings
/**
@@ -478,7 +567,7 @@ public final class VersionSetting implements Cloneable {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.PLATFORM);
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform());
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
@@ -511,6 +600,7 @@ public final class VersionSetting implements Cloneable {
permSizeProperty.addListener(listener);
maxMemoryProperty.addListener(listener);
minMemoryProperty.addListener(listener);
autoMemory.addListener(listener);
preLaunchCommandProperty.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
@@ -524,8 +614,13 @@ 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);
}
@Override
@@ -539,6 +634,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setAutoMemory(isAutoMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
@@ -552,7 +648,11 @@ 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());
return versionSetting;
}
@@ -567,6 +667,7 @@ 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());
@@ -582,8 +683,13 @@ 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());
return obj;
}
@@ -604,6 +710,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 +720,19 @@ 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)]);
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
@@ -21,6 +21,7 @@ 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.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
@@ -28,11 +29,13 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -40,13 +43,22 @@ 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.DownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
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;
@@ -61,10 +73,39 @@ 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<DownloadListPage> modDownloadListPage = new Lazy<>(() -> {
return new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
{
state.set(State.fromTitle(i18n("modpack.download")));
}
};
});
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
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 +117,42 @@ 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 DownloadListPage getModpackDownloadListPage() {
return modDownloadListPage.get();
}
// FXThread
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 +163,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,14 +191,14 @@ 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());
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE);
stage.setTitle(Metadata.FULL_TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
}
@@ -159,12 +220,24 @@ public final class Controllers {
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(MessageDialogPane.ok(text, title, type, ok));
}
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(MessageDialogPane.yesOrNo(text, title, type, yes, no));
}
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton) {
dialog(MessageDialogPane.actionOrCancel(text, title, type, actionButton, null));
}
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
dialog(MessageDialogPane.actionOrCancel(text, title, type, actionButton, cancel));
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
@@ -199,6 +272,10 @@ public final class Controllers {
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
}
public static void showToast(String content) {
decorator.showToast(content);
}
public static boolean isStopped() {
return decorator == null;
}
@@ -206,7 +283,9 @@ public final class Controllers {
public static void shutdown() {
rootPage = null;
versionPage = null;
serversPage = null;
gameListPage = null;
settingsPage = null;
modDownloadListPage = null;
decorator = null;
stage = null;
scene = null;
@@ -31,6 +31,8 @@ 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) {
@@ -38,8 +40,8 @@ public final class DialogController {
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
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,19 @@ 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.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -51,13 +54,15 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
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 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.List;
import java.util.Objects;
import java.util.function.BooleanSupplier;
@@ -68,6 +73,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() {
@@ -112,6 +119,16 @@ public final class FXUtils {
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();
@@ -307,16 +324,19 @@ public final class FXUtils {
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 +369,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 +428,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 +514,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 +524,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 +535,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 +551,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
@@ -528,4 +639,37 @@ 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"));
}
}
@@ -50,6 +50,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 +70,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 +104,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 +127,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};
}
}
@@ -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);
@@ -30,12 +30,13 @@ 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;
@@ -83,6 +85,8 @@ public final class LogWindow extends Stage {
private final LogWindowImpl impl = new LogWindowImpl();
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
private boolean stopCheckLogCount = false;
public LogWindow() {
@@ -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);
}
}
}
+325 -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,71 @@ 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 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,51 +123,93 @@ 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) {
@@ -151,16 +220,28 @@ public final class SVG {
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 +253,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 +263,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 +284,206 @@ 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);
}
}
@@ -70,7 +70,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
root.setCenter(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
FXUtils.onChangeAndOperate(skinnable.loadingProperty(), loading -> {
if (loading) {
spinnerPane.showSpinner();
} else {
spinnerPane.hideSpinner();
}
});
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
@@ -93,6 +99,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
@@ -25,8 +25,12 @@ import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
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 {
@@ -36,17 +40,23 @@ public class UpgradeDialog extends JFXDialogLayout {
}
{
String url = CHANGELOG_URL + config().getUpdateChannel().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 + config().getUpdateChannel().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);
}
}
}
@@ -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
@@ -176,7 +176,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,203 @@
/*
* 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.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
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.ListPageBase;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
import java.net.URI;
public class AccountListPage extends ListPageBase<AccountListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage"), -1));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
private final ListProperty<AuthlibInjectorServer> authServers = new SimpleListProperty<>(this, "authServers", FXCollections.observableArrayList());
private final ObjectProperty<Account> selectedAccount;
public AccountListPage() {
setItems(MappedObservableList.create(accounts, AccountListItem::new));
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
}
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 SkinBase<AccountListPage> {
private final ObservableList<AdvancedListItem> authServerItems;
public AccountListPageSkin(AccountListPage skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
{
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
{
VBox boxItemList = new VBox();
boxItemList.getStyleClass().add("advanced-list-box-content");
boxItemList.getChildren().add(new ClassTitle(i18n("account.create")));
{
VBox boxMethods = new VBox();
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(Theme.blackFillBinding(), 24, 24)));
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(Theme.blackFillBinding(), 24, 24)));
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(Theme.blackFillBinding(), 24, 24)));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG.server(Theme.blackFillBinding(), 24, 24)));
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
item.setRightGraphic(btnRemove);
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
FXUtils.installFastTooltip(item, tooltip);
return item;
});
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
boxMethods.getChildren().add(boxAuthServers);
boxItemList.getChildren().add(new ScrollPane(boxMethods));
}
left.setCenter(boxItemList);
}
{
AdvancedListItem addAuthServerItem = new AdvancedListItem();
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
addAuthServerItem.setTitle(i18n("account.injector.add"));
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
addAuthServerItem.setActionButtonVisible(false);
addAuthServerItem.setLeftGraphic(wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
BorderPane.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
left.setBottom(addAuthServerItem);
}
root.setLeft(left);
}
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.itemsProperty());
scrollPane.setContent(list);
JFXScrollPane.smoothScrolling(scrollPane);
root.setCenter(scrollPane);
}
getChildren().setAll(root);
}
}
}
@@ -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
@@ -25,6 +25,7 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -33,6 +34,7 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AccountLoginPane extends StackPane {
@@ -54,6 +56,8 @@ public class AccountLoginPane extends StackPane {
lblUsername.setText(oldAccount.getUsername());
txtPassword.setOnAction(e -> onAccept());
onEscPressed(this, this::onCancel);
}
@FXML
@@ -71,7 +75,7 @@ public class AccountLoginPane extends StackPane {
if (e instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
lblCreationWarning.setText(AddAccountPane.accountException(e));
lblCreationWarning.setText(Accounts.localizeErrorMessage(e));
}
progressBar.setVisible(false);
}).start();
@@ -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,642 @@
/*
* 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.SimpleBooleanProperty;
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.MicrosoftAuthenticationServer;
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.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.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 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);
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.INFORMATION);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
logging
? i18n("account.methods.microsoft.manual")
: i18n("account.methods.microsoft.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
}
});
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
box.getChildren().setAll(profileLink, birthLink);
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();
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/en-us/articles/360050865492-JAVA-Account-Migration-FAQ"));
GridPane.setColumnSpan(box, 2);
box.getChildren().setAll(migrationLink);
add(box, 0, rowIndex);
rowIndex++;
}
if (factory instanceof OfflineAccountFactory) {
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) {
return txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
} 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;
}
}
@@ -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;
@@ -25,6 +25,8 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
public class AdvancedListBox extends ScrollPane {
private final VBox container = new VBox();
@@ -37,7 +39,6 @@ public class AdvancedListBox extends ScrollPane {
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.setSpacing(5);
container.getStyleClass().add("advanced-list-box-content");
}
@@ -53,6 +54,14 @@ public class AdvancedListBox extends ScrollPane {
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);
@@ -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,38 @@ 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.setAlignment(Pos.CENTER_LEFT);
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 +72,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);
}
}
@@ -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;
@@ -129,6 +133,9 @@ 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"));
}
return cell;
});
@@ -153,8 +160,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);
}
}
@@ -80,7 +80,7 @@ class ComponentListCell extends StackPane {
BorderPane groupNode = new BorderPane();
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
@@ -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,69 @@
/*
* 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.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class HintPane extends VBox {
private final Text label = new Text();
private final StringProperty text = new SimpleStringProperty(this, "text");
public HintPane(MessageDialogPane.MessageType type) {
setFillWidth(true);
getStyleClass().add("hint");
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.getChildren().setAll(
SVG.informationOutline(Theme.blackFillBinding(), 16, 16),
new Label(i18n("message.info"))
);
TextFlow flow = new TextFlow();
flow.getChildren().setAll(label);
getChildren().setAll(hbox, flow);
label.textProperty().bind(text);
VBox.setMargin(flow, new Insets(2, 2, 0, 2));
label.fillProperty().bind(BindingMapping.of(disabledProperty()).map(disabled -> disabled ? new Color(0, 0, 0, 0.5) : Color.BLACK));
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
}
@@ -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,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));
}
}
@@ -0,0 +1,72 @@
/*
* 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.JFXListView;
import javafx.css.PseudoClass;
import javafx.scene.control.ListCell;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
public abstract class MDListCell<T> extends ListCell<T> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final StackPane container = new StackPane();
private final StackPane root = new StackPane();
public MDListCell(JFXListView<T> listView) {
setText(null);
setGraphic(null);
root.getStyleClass().add("md-list-cell");
RipplerContainer ripplerContainer = new RipplerContainer(container);
root.getChildren().setAll(ripplerContainer);
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
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateControl(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(root);
}
}
protected StackPane getContainer() {
return container;
}
protected void setSelectable() {
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
root.pseudoClassStateChanged(SELECTED, selected);
});
}
protected abstract void updateControl(T item, boolean empty);
}
@@ -0,0 +1,104 @@
/*
* 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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
public class MenuUpDownButton extends Control {
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
private final StringProperty text = new SimpleStringProperty(this, "text");
public MenuUpDownButton() {
}
@Override
protected Skin<?> createDefaultSkin() {
return new MenuUpDownButtonSkin(this);
}
public boolean isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
private static class MenuUpDownButtonSkin extends SkinBase<MenuUpDownButton> {
protected MenuUpDownButtonSkin(MenuUpDownButton control) {
super(control);
HBox content = new HBox(8);
content.setAlignment(Pos.CENTER);
Label label = new Label();
label.setStyle("-fx-text-fill: black;");
label.textProperty().bind(control.text);
Node up = SVG.menuUp(Theme.blackFillBinding(), 16, 16);
Node down = SVG.menuDown(Theme.blackFillBinding(), 16, 16);
JFXButton button = new JFXButton();
button.setGraphic(content);
button.setOnAction(e -> {
control.selected.set(!control.isSelected());
});
FXUtils.onChangeAndOperate(control.selected, selected -> {
if (selected) {
content.getChildren().setAll(label, up);
} else {
content.getChildren().setAll(label, down);
}
});
getChildren().setAll(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
@@ -18,16 +18,20 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
@@ -40,10 +44,6 @@ public final class MessageDialogPane extends StackPane {
FINE,
}
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private Label content;
@FXML
@@ -53,20 +53,16 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept) {
private @Nullable ButtonBase cancelButton;
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
content.setText(text);
if (title != null)
this.title.setText(title);
content.setText(text);
acceptButton.setOnMouseClicked(e -> {
fireEvent(new DialogCloseEvent());
Optional.ofNullable(onAccept).ifPresent(Runnable::run);
});
actions.getChildren().remove(cancelButton);
switch (type) {
case INFORMATION:
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
@@ -86,20 +82,70 @@ public final class MessageDialogPane extends StackPane {
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
onEscPressed(this, () -> {
if (cancelButton != null) {
cancelButton.fire();
}
});
}
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageType.QUESTION, onAccept);
public void addButton(ButtonBase btn) {
btn.addEventHandler(ActionEvent.ACTION, e -> fireEvent(new DialogCloseEvent()));
actions.getChildren().add(btn);
}
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
fireEvent(new DialogCloseEvent());
Optional.ofNullable(onCancel).ifPresent(Runnable::run);
});
public void setCancelButton(@Nullable ButtonBase btn) {
cancelButton = btn;
}
acceptButton.setText(i18n("button.yes"));
cancelButton.setText(i18n("button.no"));
public static MessageDialogPane ok(String text, String title, MessageType type, Runnable ok) {
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
actions.getChildren().add(cancelButton);
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
if (ok != null) {
btnOk.setOnAction(e -> ok.run());
}
dialog.addButton(btnOk);
dialog.setCancelButton(btnOk);
return dialog;
}
public static MessageDialogPane yesOrNo(String text, String title, MessageType type, Runnable yes, Runnable no) {
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
JFXButton btnYes = new JFXButton(i18n("button.yes"));
btnYes.getStyleClass().add("dialog-accept");
if (yes != null) {
btnYes.setOnAction(e -> yes.run());
}
dialog.addButton(btnYes);
JFXButton btnNo = new JFXButton(i18n("button.no"));
btnNo.getStyleClass().add("dialog-cancel");
if (no != null) {
btnNo.setOnAction(e -> no.run());
}
dialog.addButton(btnNo);
dialog.setCancelButton(btnNo);
return dialog;
}
public static MessageDialogPane actionOrCancel(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
dialog.addButton(actionButton);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
if (cancel != null) {
btnCancel.setOnAction(e -> cancel.run());
}
dialog.addButton(btnCancel);
dialog.setCancelButton(btnCancel);
return dialog;
}
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
@@ -48,7 +49,7 @@ public class Navigator extends TransitionPane {
backable.set(canGoBack());
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
initialized = true;
}
@@ -68,14 +69,14 @@ public class Navigator extends TransitionPane {
stack.push(node);
backable.set(canGoBack());
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
NavigationEvent navigating = new NavigationEvent(this, from, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
node.getProperties().put("hmcl.navigator.animation", animationProducer);
setContent(node, animationProducer);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
EventHandler<PageCloseEvent> handler = event -> close(node);
@@ -111,7 +112,7 @@ public class Navigator extends TransitionPane {
backable.set(canGoBack());
Node node = stack.peek();
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
NavigationEvent navigating = new NavigationEvent(this, from, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
@@ -122,7 +123,7 @@ public class Navigator extends TransitionPane {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
}
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
@@ -206,12 +207,14 @@ public class Navigator extends TransitionPane {
private final Navigator source;
private final Node node;
private final Navigation.NavigationDirection direction;
public NavigationEvent(Navigator source, Node target, EventType<? extends Event> eventType) {
public NavigationEvent(Navigator source, Node target, Navigation.NavigationDirection direction, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.source = source;
this.node = target;
this.direction = direction;
}
@Override
@@ -222,5 +225,9 @@ public class Navigator extends TransitionPane {
public Node getNode() {
return node;
}
public Navigation.NavigationDirection getDirection() {
return direction;
}
}
}
@@ -83,7 +83,7 @@ public class PopupMenu extends Control {
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
VBox content = new VBox();
content.getStyleClass().add("content");
content.getStyleClass().add("popup-menu-content");
Bindings.bindContent(content.getChildren(), PopupMenu.this.getContent());
scrollPane.setContent(content);
@@ -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,6 +34,8 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -115,6 +117,8 @@ public class PromptDialogPane extends StackPane {
lblCreationWarning.setText(msg);
});
});
onEscPressed(this, cancelButton::fire);
}
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {
@@ -1,13 +1,34 @@
/*
* 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.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RequiredValidator extends ValidatorBase {
public RequiredValidator() {
this(i18n("input.not_empty"));
}
public RequiredValidator(@NamedArg("message") String message) {
@@ -18,35 +18,35 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content")
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
public SpinnerPane() {
getStyleClass().add("spinner-pane");
}
public void showSpinner() {
setLoading(true);
setLoading(true);
}
public void hideSpinner() {
setLoading(false);
setFailedReason(null);
setLoading(false);
}
public Node getContent() {
@@ -62,15 +62,27 @@ public class SpinnerPane extends Control {
}
public boolean isLoading() {
return loading.get();
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
this.loading.set(loading);
}
public String getFailedReason() {
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
}
@Override
@@ -82,62 +94,56 @@ public class SpinnerPane extends Control {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final StackPane root = new StackPane();
private Timeline animation;
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
protected Skin(SpinnerPane control) {
super(control);
root.getStyleClass().add("spinner-pane");
topPane.getChildren().setAll(spinner);
root.getChildren().setAll(contentPane, topPane);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> contentPane.getChildren().setAll(newValue));
topPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> {
Timeline prev = animation;
if (prev != null) prev.stop();
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
root.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
}
AnimationProducer transition;
topPane.setMouseTransparent(true);
topPane.setVisible(true);
topPane.getStyleClass().add("gray-background");
if (newValue)
transition = ContainerAnimations.FADE_IN.getAnimationProducer();
else
transition = ContainerAnimations.FADE_OUT.getAnimationProducer();
public interface State {}
AnimationHandler handler = new AnimationHandler() {
@Override
public Duration getDuration() {
return Duration.millis(160);
}
public static class LoadedState implements State {}
@Override
public Pane getCurrentRoot() {
return root;
}
public static class LoadingState implements State {}
@Override
public Node getPreviousNode() {
return null;
}
public static class FailedState implements State {
private final String reason;
@Override
public Node getCurrentNode() {
return topPane;
}
};
public FailedState(String reason) {
this.reason = reason;
}
Timeline now = new Timeline();
now.getKeyFrames().addAll(transition.animate(handler));
now.getKeyFrames().add(new KeyFrame(handler.getDuration(), e -> {
topPane.setMouseTransparent(!newValue);
topPane.setVisible(newValue);
}));
now.play();
animation = now;
});
public String getReason() {
return reason;
}
}
}
@@ -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,11 +21,20 @@ import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
import java.util.Stack;
public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
public Optional<Node> peek() {
if (stack.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(stack.peek());
}
}
public void push(Node node) {
stack.push(node);
getChildren().setAll(node);
@@ -27,9 +27,9 @@ import javafx.scene.control.SingleSelectionModel;
import java.util.function.Supplier;
public interface TabControl {
ObservableList<Tab> getTabs();
ObservableList<Tab<?>> getTabs();
class TabControlSelectionModel extends SingleSelectionModel<Tab> {
class TabControlSelectionModel extends SingleSelectionModel<Tab<?>> {
private final TabControl tabHeader;
public TabControlSelectionModel(final TabControl t) {
@@ -39,9 +39,9 @@ public interface TabControl {
this.tabHeader = t;
// watching for changes to the items list content
final ListChangeListener<Tab> itemsContentObserver = c -> {
final ListChangeListener<Tab<?>> itemsContentObserver = c -> {
while (c.next()) {
for (Tab tab : c.getRemoved()) {
for (Tab<?> tab : c.getRemoved()) {
if (tab != null && !tabHeader.getTabs().contains(tab)) {
if (tab.isSelected()) {
tab.setSelected(false);
@@ -117,29 +117,29 @@ public interface TabControl {
}
}
@Override protected Tab getModelItem(int index) {
final ObservableList<Tab> items = tabHeader.getTabs();
@Override protected Tab<?> getModelItem(int index) {
final ObservableList<Tab<?>> items = tabHeader.getTabs();
if (items == null) return null;
if (index < 0 || index >= items.size()) return null;
return items.get(index);
}
@Override protected int getItemCount() {
final ObservableList<Tab> items = tabHeader.getTabs();
final ObservableList<Tab<?>> items = tabHeader.getTabs();
return items == null ? 0 : items.size();
}
private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) {
private Tab<?> findNearestAvailableTab(int tabIndex, boolean doSelect) {
// we always try to select the nearest, non-disabled
// tab from the position of the closed tab.
final int tabCount = getItemCount();
int i = 1;
Tab bestTab = null;
Tab<?> bestTab = null;
while (true) {
// look leftwards
int downPos = tabIndex - i;
if (downPos >= 0) {
Tab _tab = getModelItem(downPos);
Tab<?> _tab = getModelItem(downPos);
if (_tab != null) {
bestTab = _tab;
break;
@@ -153,7 +153,7 @@ public interface TabControl {
// the removed tabs position).
int upPos = tabIndex + i - 1;
if (upPos < tabCount) {
Tab _tab = getModelItem(upPos);
Tab<?> _tab = getModelItem(upPos);
if (_tab != null) {
bestTab = _tab;
break;
@@ -174,12 +174,13 @@ public interface TabControl {
}
}
public static class Tab {
class Tab<T extends Node> {
private final StringProperty id = new SimpleStringProperty(this, "id");
private final StringProperty text = new SimpleStringProperty(this, "text");
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
private final ObjectProperty<Node> node = new SimpleObjectProperty<>(this, "node");
private Supplier<Node> nodeSupplier;
private final ObjectProperty<T> node = new SimpleObjectProperty<>(this, "node");
private final ObjectProperty<Object> userData = new SimpleObjectProperty<>(this, "userData");
private Supplier<? extends T> nodeSupplier;
public Tab(String id) {
setId(id);
@@ -190,11 +191,11 @@ public interface TabControl {
setText(text);
}
public Supplier<Node> getNodeSupplier() {
public Supplier<? extends T> getNodeSupplier() {
return nodeSupplier;
}
public void setNodeSupplier(Supplier<Node> nodeSupplier) {
public void setNodeSupplier(Supplier<? extends T> nodeSupplier) {
this.nodeSupplier = nodeSupplier;
}
@@ -234,16 +235,43 @@ public interface TabControl {
this.selected.set(selected);
}
public Node getNode() {
public T getNode() {
return node.get();
}
public ObjectProperty<Node> nodeProperty() {
public ObjectProperty<T> nodeProperty() {
return node;
}
public void setNode(Node node) {
public void setNode(T node) {
this.node.set(node);
}
public Object getUserData() {
return userData.get();
}
public ObjectProperty<?> userDataProperty() {
return userData;
}
public void setUserData(Object userData) {
this.userData.set(userData);
}
public boolean isInitialized() {
return getNode() != null;
}
public boolean initializeIfNeeded() {
if (getNode() == null) {
if (getNodeSupplier() == null) {
return false;
}
setNode(getNodeSupplier().get());
return true;
}
return false;
}
}
}
@@ -41,34 +41,56 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TabHeader extends Control implements TabControl {
public TabHeader(Tab... tabs) {
public TabHeader(Tab<?>... tabs) {
getStyleClass().setAll("tab-header");
if (tabs != null) {
getTabs().addAll(tabs);
}
}
private ObservableList<Tab> tabs = FXCollections.observableArrayList();
private ObservableList<Tab<?>> tabs = FXCollections.observableArrayList();
private ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP);
@Override
public ObservableList<Tab> getTabs() {
public ObservableList<Tab<?>> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControlSelectionModel(this));
private final ObjectProperty<SingleSelectionModel<Tab<?>>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControlSelectionModel(this));
public SingleSelectionModel<Tab> getSelectionModel() {
public SingleSelectionModel<Tab<?>> getSelectionModel() {
return selectionModel.get();
}
public ObjectProperty<SingleSelectionModel<Tab>> selectionModelProperty() {
public ObjectProperty<SingleSelectionModel<Tab<?>>> selectionModelProperty() {
return selectionModel;
}
public void setSelectionModel(SingleSelectionModel<Tab> selectionModel) {
public void setSelectionModel(SingleSelectionModel<Tab<?>> selectionModel) {
this.selectionModel.set(selectionModel);
}
/**
* The position to place the tabs.
*/
public Side getSide() {
return side.get();
}
/**
* The position of the tabs.
*/
public ObjectProperty<Side> sideProperty() {
return side;
}
/**
* The position the place the tabs in this TabHeader.
*/
public void setSide(Side side) {
this.side.set(side);
}
@Override
protected Skin<?> createDefaultSkin() {
return new TabHeaderSkin(this);
@@ -82,7 +104,7 @@ public class TabHeader extends Control implements TabControl {
private final HeaderContainer header;
private boolean isSelectingTab = false;
private Tab selectedTab;
private Tab<?> selectedTab;
protected TabHeaderSkin(TabHeader control) {
super(control);
@@ -115,7 +137,7 @@ public class TabHeader extends Control implements TabControl {
protected class HeaderContainer extends StackPane {
private Timeline timeline;
private StackPane selectedTabLine;
private StackPane headersRegion;
private HeadersRegion headersRegion;
private Scale scale = new Scale(1, 1, 0, 0);
private Rotate rotate = new Rotate(0, 0, 1);
private double selectedTabLineOffset;
@@ -125,9 +147,114 @@ public class TabHeader extends Control implements TabControl {
getStyleClass().add("tab-header-area");
setPickOnBounds(false);
headersRegion = new StackPane() {
headersRegion = new HeadersRegion();
headersRegion.sideProperty().bind(getSkinnable().sideProperty());
selectedTabLine = new StackPane();
selectedTabLine.setManaged(false);
selectedTabLine.getTransforms().addAll(scale, rotate);
selectedTabLine.setCache(true);
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(2);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
rotate.pivotXProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 0.0 : 1, getSkinnable().sideProperty()));
rotate.pivotYProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 1.0 : 0, getSkinnable().sideProperty()));
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
TabHeaderContainer container = new TabHeaderContainer(tab);
container.setVisible(true);
return container;
}));
}
public void setNeedsLayout2(boolean value) {
setNeedsLayout(value);
}
private boolean isAnimating() {
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelectingTab) {
headersRegion.animateSelectionLine();
isSelectingTab = false;
}
}
private class HeadersRegion extends StackPane {
private SideAction action;
private final ObjectProperty<Side> side = new SimpleObjectProperty<Side>() {
@Override
protected double computePrefWidth(double height) {
protected void invalidated() {
super.invalidated();
switch (get()) {
case TOP: action = new Top(); break;
case BOTTOM: action = new Bottom(); break;
case LEFT: action = new Left(); break;
case RIGHT: action = new Right(); break;
default: throw new InternalError();
}
}
};
public Side getSide() {
return side.get();
}
public ObjectProperty<Side> sideProperty() {
return side;
}
public void setSide(Side side) {
this.side.set(side);
}
@Override
protected double computePrefWidth(double height) {
return action.computePrefWidth(height);
}
@Override
protected double computePrefHeight(double width) {
return action.computePrefHeight(width);
}
@Override
protected void layoutChildren() {
action.layoutChildren();
}
protected void animateSelectionLine() {
action.animateSelectionLine();
}
private abstract class SideAction {
abstract double computePrefWidth(double height);
abstract double computePrefHeight(double width);
void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
}
abstract void animateSelectionLine();
}
private abstract class Horizontal extends SideAction {
@Override
public double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
@@ -137,7 +264,7 @@ public class TabHeader extends Control implements TabControl {
}
@Override
protected double computePrefHeight(double width) {
public double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
@@ -146,13 +273,81 @@ public class TabHeader extends Control implements TabControl {
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
@Override
protected void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
private void runTimeline(double newTransX, double newWidth) {
double lineWidth = selectedTabLine.prefWidth(-1.0D);
if (isAnimating()) {
timeline.stop();
double tempScaleX = scale.getX();
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
double tempWidth = tempScaleX * lineWidth;
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - tempWidth);
}
}
double oldScaleX = scale.getX();
double oldWidth = lineWidth * oldScaleX;
double oldTransX = selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth;
selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX;
if (transDiff < 0.0D) {
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() + oldWidth);
newTransX += newWidth;
rotate.setAngle(180.0D);
}
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(selectedTabLine.translateXProperty(), selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)
),
new KeyFrame(
Duration.seconds(0.24D),
new KeyValue(scale.xProperty(), newScaleX, Interpolator.EASE_BOTH),
new KeyValue(selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)
)
);
timeline.setOnFinished((finish) -> {
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - newWidth);
}
});
timeline.play();
}
@Override
public void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabWidth = 0.0D;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer) node;
double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = offset;
selectedTabWidth = tabHeaderPrefWidth;
break;
}
offset += tabHeaderPrefWidth;
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
private class Top extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerHeight = snapSize(prefHeight(-1));
double tabStartX = 0;
for (Node node : getChildren()) {
@@ -171,117 +366,182 @@ public class TabHeader extends Control implements TabControl {
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
};
selectedTabLine = new StackPane();
selectedTabLine.setManaged(false);
selectedTabLine.getTransforms().addAll(scale, rotate);
selectedTabLine.setCache(true);
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(1);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
prefWidthProperty().bind(headersRegion.widthProperty());
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
TabHeaderContainer container = new TabHeaderContainer(tab);
container.setVisible(true);
return container;
}));
}
public void setNeedsLayout2(boolean value) {
setNeedsLayout(value);
}
private void runTimeline(double newTransX, double newWidth) {
double tempScaleX = 0.0D;
double tempWidth = 0.0D;
double lineWidth = this.selectedTabLine.prefWidth(-1.0D);
if (this.isAnimating()) {
this.timeline.stop();
tempScaleX = this.scale.getX();
if (this.rotate.getAngle() != 0.0D) {
this.rotate.setAngle(0.0D);
tempWidth = tempScaleX * lineWidth;
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - tempWidth);
}
}
double oldScaleX = this.scale.getX();
double oldWidth = lineWidth * oldScaleX;
double oldTransX = this.selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth;
this.selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX;
double midScaleX = tempScaleX != 0.0D ? tempScaleX : (Math.abs(transDiff) / 1.3D + oldWidth) * oldScaleX / oldWidth;
if (transDiff < 0.0D) {
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() + oldWidth);
newTransX += newWidth;
this.rotate.setAngle(180.0D);
}
private class Bottom extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
this.timeline = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.12D), new KeyValue(this.scale.xProperty(), midScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.24D), new KeyValue(this.scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)));
this.timeline.setOnFinished((finish) -> {
if (this.rotate.getAngle() != 0.0D) {
this.rotate.setAngle(0.0D);
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - newWidth);
}
double headerHeight = snapSize(prefHeight(-1));
double tabStartX = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
});
this.timeline.play();
}
private boolean isAnimating() {
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
}
private void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabWidth = 0.0D;
Side side = Side.TOP;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer)node;
double tabHeaderPrefWidth = this.snapSize(tabHeader.prefWidth(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = side != Side.LEFT && side != Side.BOTTOM ? offset : -offset - tabHeaderPrefWidth;
selectedTabWidth = tabHeaderPrefWidth;
break;
child.relocate(tabStartX, snappedTopInset());
tabStartX += w;
}
offset += tabHeaderPrefWidth;
selectedTabLine.resizeRelocate(0, 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
private abstract class Vertical extends SideAction {
@Override
public double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
width = Math.max(width, child.prefWidth(height));
}
return snapSize(width) + snappedLeftInset() + snappedRightInset();
}
@Override
public double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
height += child.prefHeight(width);
}
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
private void runTimeline(double newTransY, double newHeight) {
double lineHeight = selectedTabLine.prefHeight(-1.0D);
if (isAnimating()) {
timeline.stop();
double tempScaleY = scale.getY();
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
double tempHeight = tempScaleY * lineHeight;
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - tempHeight);
}
}
double oldScaleY = scale.getY();
double oldHeight = lineHeight * oldScaleY;
double oldTransY = selectedTabLine.getTranslateY();
double newScaleY = newHeight * oldScaleY / oldHeight;
selectedTabLineOffset = newTransY;
// newTransY += offsetStart * (double)this.direction;
double transDiff = newTransY - oldTransY;
if (transDiff < 0.0D) {
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() + oldHeight);
newTransY += newHeight;
rotate.setAngle(180.0D);
}
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(selectedTabLine.translateYProperty(), selectedTabLine.getTranslateY(), Interpolator.EASE_BOTH)
),
new KeyFrame(
Duration.seconds(1.24D),
new KeyValue(scale.yProperty(), newScaleY, Interpolator.EASE_BOTH),
new KeyValue(selectedTabLine.translateYProperty(), newTransY, Interpolator.EASE_BOTH)
)
);
timeline.setOnFinished((finish) -> {
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - newHeight);
}
});
timeline.play();
}
@Override
public void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabHeight = 0.0D;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer) node;
double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = offset;
selectedTabHeight = tabHeaderPrefHeight;
break;
}
offset += tabHeaderPrefHeight;
}
}
this.runTimeline(selectedTabOffset, selectedTabHeight);
}
}
private class Left extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerWidth = snapSize(prefWidth(-1));
double tabStartY = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
child.relocate(headerWidth - w - snappedRightInset(), tabStartY);
tabStartY += h;
}
selectedTabLine.resizeRelocate(headerWidth - selectedTabLine.prefWidth(-1), 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
private class Right extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerWidth = snapSize(prefWidth(-1));
double tabStartY = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
child.relocate(snappedLeftInset(), tabStartY);
tabStartY += h;
}
selectedTabLine.resizeRelocate(0, 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
protected class TabHeaderContainer extends StackPane {
private final Tab tab;
private final Tab<?> tab;
private final Label tabText;
private final BorderPane inner;
private final JFXRippler rippler;
public TabHeaderContainer(Tab tab) {
public TabHeaderContainer(Tab<?> tab) {
this.tab = tab;
tabText = new Label();
@@ -291,6 +551,7 @@ public class TabHeader extends Control implements TabControl {
inner.setCenter(tabText);
inner.getStyleClass().add("tab-container");
rippler = new JFXRippler(inner, JFXRippler.RipplerPos.FRONT);
rippler.getStyleClass().add("tab-rippler");
rippler.setRipplerFill(ripplerColor);
getChildren().setAll(rippler);
@@ -305,4 +566,5 @@ public class TabHeader extends Control implements TabControl {
}
}
}
}
@@ -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
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
@@ -53,7 +54,7 @@ public class TaskExecutorDialogPane extends StackPane {
setCancel(cancel);
btnCancel.setOnMouseClicked(e -> {
btnCancel.setOnAction(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
onCancel.accept(this);
});
@@ -76,6 +77,8 @@ public class TaskExecutorDialogPane extends StackPane {
});
};
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
onEscPressed(this, btnCancel::fire);
}
public void setExecutor(TaskExecutor executor) {
@@ -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
@@ -28,8 +28,10 @@ import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
@@ -110,7 +112,7 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("assets.download_all"));
} else if (task instanceof GameInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof ForgeInstallTask) {
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
@@ -118,6 +120,8 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
} else if (task instanceof FabricInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
} else if (task instanceof FabricAPIInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
} else if (task instanceof CurseCompletionTask) {
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
@@ -211,6 +215,7 @@ public final class TaskListPane extends StackPane {
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
@@ -20,18 +20,28 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.AggregatedObservableList;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty tag = new SimpleStringProperty(this, "tag");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final ObservableList<Node> tagLabels;
private final AggregatedObservableList<Node> firstLineChildren;
public TwoLineListItem(String titleString, String subtitleString) {
this();
@@ -42,27 +52,39 @@ public class TwoLineListItem extends VBox {
public TwoLineListItem() {
setMouseTransparent(true);
HBox firstLine = new HBox();
FlowPane firstLine = new FlowPane();
firstLine.setMaxWidth(Double.MAX_VALUE);
Label lblTitle = new Label();
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
Label lblTag = new Label();
lblTag.getStyleClass().add("tag");
lblTag.textProperty().bind(tag);
lblTag.visibleProperty().bind(Bindings.createBooleanBinding(
() -> StringUtils.isNotBlank(tag.getValue()),
tag));
firstLine.getChildren().addAll(lblTitle, lblTag);
tagLabels = MappedObservableList.create(tags, tag -> {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
FlowPane.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
});
firstLineChildren = new AggregatedObservableList<>();
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
firstLineChildren.appendList(tagLabels);
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
Label lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");
lblSubtitle.textProperty().bind(subtitle);
getChildren().setAll(firstLine, lblSubtitle);
HBox secondLine = new HBox();
secondLine.getChildren().setAll(lblSubtitle);
getChildren().setAll(firstLine, secondLine);
FXUtils.onChangeAndOperate(subtitle, subtitleString -> {
if (subtitleString == null) getChildren().setAll(firstLine);
else getChildren().setAll(firstLine, secondLine);
});
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@@ -90,16 +112,8 @@ public class TwoLineListItem extends VBox {
this.subtitle.set(subtitle);
}
public String getTag() {
return tag.get();
}
public StringProperty tagProperty() {
return tag;
}
public void setTag(String tag) {
this.tag.set(tag);
public ObservableList<String> getTags() {
return tags;
}
@Override
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXSnackbar;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -34,6 +35,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public class Decorator extends Control {
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
@@ -51,7 +53,9 @@ public class Decorator extends Control {
private final BooleanProperty canClose = new SimpleBooleanProperty(false);
private final BooleanProperty showCloseAsHome = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private Navigation.NavigationDirection navigationDirection = Navigation.NavigationDirection.START;
private StackPane drawerWrapper;
private final JFXSnackbar snackbar = new JFXSnackbar();
private final ReadOnlyBooleanWrapper allowMove = new ReadOnlyBooleanWrapper();
private final ReadOnlyBooleanWrapper dragging = new ReadOnlyBooleanWrapper();
@@ -212,6 +216,10 @@ public class Decorator extends Control {
return onRefreshNavButtonAction;
}
public JFXSnackbar getSnackbar() {
return snackbar;
}
@Override
protected Skin<?> createDefaultSkin() {
return new DecoratorSkin(this);
@@ -238,4 +246,13 @@ public class Decorator extends Control {
e.consume();
});
}
// TODO: Dirty implementation.
public Navigation.NavigationDirection getNavigationDirection() {
return navigationDirection;
}
public void setNavigationDirection(Navigation.NavigationDirection navigationDirection) {
this.navigationDirection = navigationDirection;
}
}
@@ -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,26 +18,20 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import com.jfoenix.controls.JFXSnackbar;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -55,22 +49,23 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.logging.Level;
import java.util.stream.Stream;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
private final Decorator decorator;
private final ImageView welcomeView;
private final Navigator navigator;
private JFXDialog dialog;
@@ -89,47 +84,49 @@ public class DecoratorController {
decorator.onBackNavButtonActionProperty().set(e -> back());
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(newImage("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> decorator.getContainer().remove(welcomeView))
);
nowAnimation.play();
});
if (switchedToNewUI()) {
if (config().getLocalization().getLocale() == Locale.CHINA) {
// currently, user guide is only available in Chinese
decorator.getContainer().setAll(welcomeView);
}
}
setupBackground();
setupAuthlibInjectorDnD();
// pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> {
if (!(e.getTarget() instanceof Node)) {
return; // event source can't be determined
}
Node newTarget;
if (dialogPane != null && dialogPane.peek().isPresent()) {
newTarget = dialogPane.peek().get(); // current dialog
} else {
newTarget = navigator.getCurrentPage(); // current page
}
boolean needsRedirect = true;
Node t = (Node) e.getTarget();
while (t != null) {
if (t == newTarget) {
// current event target is in newTarget
needsRedirect = false;
break;
}
t = t.getParent();
}
if (!needsRedirect) {
return;
}
e.consume();
newTarget.fireEvent(e.copyFor(e.getSource(), newTarget));
});
// press ESC to go back
onEscPressed(navigator, this::back);
}
public Decorator getDecorator() {
return decorator;
}
/**
* @return true if the user is seeing the current version of UI for the first time.
*/
private boolean switchedToNewUI() {
if (config().getUiVersion() < Config.CURRENT_UI_VERSION) {
config().setUiVersion(Config.CURRENT_UI_VERSION);
return true;
}
return false;
}
// ==== Background ====
private void setupBackground() {
@@ -174,10 +171,10 @@ public class DecoratorController {
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(Files::isRegularFile)
.filter(Files::isReadable)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
String ext = getExtension(it).toLowerCase();
return ext.equals("png") || ext.equals("jpg");
})
.collect(toList());
} catch (IOException e) {
@@ -199,13 +196,23 @@ public class DecoratorController {
}
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
if (!Files.isReadable(path))
return Optional.empty();
Image img;
try {
img = new Image(path.toAbsolutePath().toUri().toString());
} catch (IllegalArgumentException e) {
LOG.log(WARNING, "Couldn't load background image", e);
return Optional.empty();
}
return Optional.empty();
if (img.getException() != null) {
LOG.log(WARNING, "Couldn't load background image", img.getException());
return Optional.empty();
}
return Optional.of(img);
}
// ==== Navigation ====
@@ -230,10 +237,15 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
if (page.back())
navigator.close();
if (page.back()) {
if (navigator.canGoBack()) {
navigator.close();
}
}
} else {
navigator.close();
if (navigator.canGoBack()) {
navigator.close();
}
}
}
@@ -246,14 +258,6 @@ public class DecoratorController {
}
}
private void onNavigating(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node from = event.getNode();
if (from instanceof DecoratorPage)
((DecoratorPage) from).back();
}
private void onNavigated(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node to = event.getNode();
@@ -273,6 +277,8 @@ public class DecoratorController {
decorator.showCloseAsHomeProperty().set(true);
}
decorator.setNavigationDirection(event.getDirection());
// state property should be updated at last.
if (to instanceof DecoratorPage) {
decorator.stateProperty().bind(((DecoratorPage) to).stateProperty());
@@ -311,6 +317,8 @@ public class DecoratorController {
dialog.setDialogContainer(decorator.getDrawerWrapper());
dialog.setOverlayClose(false);
dialog.show();
navigator.setDisable(true);
}
dialogPane.push(node);
@@ -350,10 +358,18 @@ public class DecoratorController {
dialog.close();
dialog = null;
dialogPane = null;
navigator.setDisable(false);
}
}
}
// ==== Toast ====
public void showToast(String content) {
decorator.getSnackbar().fireEvent(new JFXSnackbar.SnackbarEvent(content, null, 2000L, false, null));
}
// ==== Wizard ====
public void startWizard(WizardProvider wizardProvider) {
@@ -50,20 +50,18 @@ public interface DecoratorPage extends Refreshable {
private final boolean backable;
private final boolean refreshable;
private final boolean animate;
private final boolean titleBarTransparent;
private final double leftPaneWidth;
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate) {
this(title, titleNode, backable, refreshable, animate, false, 0);
this(title, titleNode, backable, refreshable, animate, 0);
}
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, boolean titleBarTransparent, double leftPaneWidth) {
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
this.title = title;
this.titleNode = titleNode;
this.backable = backable;
this.refreshable = refreshable;
this.animate = animate;
this.titleBarTransparent = titleBarTransparent;
this.leftPaneWidth = leftPaneWidth;
}
@@ -71,6 +69,10 @@ public interface DecoratorPage extends Refreshable {
return new State(title, null, true, false, true);
}
public static State fromTitle(String title, double leftPaneWidth) {
return new State(title, null, true, false, true, leftPaneWidth);
}
public static State fromTitleNode(Node titleNode) {
return new State(null, titleNode, true, false, true);
}
@@ -95,10 +97,6 @@ public interface DecoratorPage extends Refreshable {
return animate;
}
public boolean isTitleBarTransparent() {
return titleBarTransparent;
}
public double getLeftPaneWidth() {
return leftPaneWidth;
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Animation;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
@@ -41,8 +42,10 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Lang;
public class DecoratorSkin extends SkinBase<Decorator> {
@@ -75,27 +78,25 @@ public class DecoratorSkin extends SkinBase<Decorator> {
root = new StackPane();
root.getStyleClass().add("window");
StackPane shadowContainer = new StackPane();
shadowContainer.getStyleClass().add("body");
parent = new StackPane();
parent.getStyleClass().add("body");
parent.backgroundProperty().bind(skinnable.contentBackgroundProperty());
parent.setPickOnBounds(false);
parent.prefHeightProperty().bind(control.prefHeightProperty());
parent.prefWidthProperty().bind(control.prefWidthProperty());
Rectangle clip = new Rectangle();
clip.widthProperty().bind(parent.widthProperty());
clip.heightProperty().bind(parent.heightProperty());
clip.setArcWidth(8);
clip.setArcHeight(8);
parent.setClip(clip);
skinnable.getSnackbar().registerSnackbarContainer(parent);
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
root.getChildren().setAll(parent);
// animation layer at bottom
{
HBox layer = new HBox();
leftPane = new StackPane();
leftPane.setPrefWidth(0);
leftPane.getStyleClass().add("jfx-decorator-drawer");
layer.getChildren().setAll(leftPane);
parent.getChildren().add(layer);
}
shadowContainer.getChildren().setAll(parent);
root.getChildren().setAll(shadowContainer);
StackPane wrapper = new StackPane();
BorderPane frame = new BorderPane();
@@ -107,8 +108,19 @@ public class DecoratorSkin extends SkinBase<Decorator> {
// center node with a animation layer at bottom, a container layer at middle and a "welcome" layer at top.
StackPane container = new StackPane();
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
FXUtils.setOverflowHidden(container);
// animation layer at bottom
{
HBox layer = new HBox();
leftPane = new StackPane();
leftPane.setPrefWidth(0);
leftPane.getStyleClass().add("jfx-decorator-drawer");
layer.getChildren().setAll(leftPane);
container.getChildren().add(layer);
}
// content layer at middle
{
StackPane contentPlaceHolder = new StackPane();
@@ -141,40 +153,48 @@ public class DecoratorSkin extends SkinBase<Decorator> {
titleContainer = new StackPane();
titleContainer.setPickOnBounds(false);
titleContainer.getStyleClass().addAll("jfx-tool-bar");
titleContainer.getStyleClass().addAll("jfx-tool-bar", "background");
control.capableDraggingWindow(titleContainer);
StackPane titleBarBackground = new StackPane();
titleBarBackground.getStyleClass().add("background");
titleContainer.getChildren().add(titleBarBackground);
BorderPane titleBar = new BorderPane();
titleBar.setPickOnBounds(false);
titleContainer.getChildren().add(titleBar);
Rectangle rectangle = new Rectangle(0, 0, 0, 0);
rectangle.widthProperty().bind(titleContainer.widthProperty());
rectangle.heightProperty().bind(titleContainer.heightProperty().add(100));
titleContainer.setClip(rectangle);
Rectangle buttonsContainerPlaceHolder = new Rectangle();
{
navBarPane = new TransitionPane();
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
if (s == null) return;
Node node = createNavBar(skinnable, s.isTitleBarTransparent(), s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
double targetOpacity = s.isTitleBarTransparent() ? 0 : 1;
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
if (s.isAnimate()) {
navBarPane.setContent(node, ContainerAnimations.FADE.getAnimationProducer());
AnimationProducer animation;
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) {
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT.getAnimationProducer();
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) {
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT.getAnimationProducer();
} else {
animation = ContainerAnimations.FADE.getAnimationProducer();
}
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
navBarPane.setContent(node, animation);
} else {
navBarPane.getChildren().setAll(node);
}
FXUtils.playAnimation(titleBarBackground, "animation",
s.isAnimate() ? Duration.millis(160) : null, titleBarBackground.opacityProperty(), null, targetOpacity, FXUtils.SINE);
titleBarTransparent = s.isTitleBarTransparent();
FXUtils.playAnimation(leftPane, "animation",
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
leftPane.prefWidthProperty().unbind();
if (s.getLeftPaneWidth() >= 0) {
FXUtils.playAnimation(leftPane, "animation",
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
} else {
Animation animation = FXUtils.playAnimation(leftPane, "animation1",
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, container.getWidth(), FXUtils.SINE);
if (animation != null) {
animation.setOnFinished(action -> {
if (animation.getStatus() != Animation.Status.STOPPED) {
leftPane.prefWidthProperty().bind(container.widthProperty());
}
});
}
}
});
titleBar.setCenter(navBarPane);
titleBar.setRight(buttonsContainerPlaceHolder);
@@ -212,51 +232,52 @@ public class DecoratorSkin extends SkinBase<Decorator> {
getChildren().add(root);
}
private Node createNavBar(Decorator skinnable, boolean titleBarTransparent, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) {
private Node createNavBar(Decorator skinnable, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) {
BorderPane navBar = new BorderPane();
{
HBox navLeft = new HBox();
navLeft.setAlignment(Pos.CENTER_LEFT);
navLeft.setPadding(new Insets(0, 5, 0, 5));
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().set(canBack);
if (canBack) {
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().set(canBack);
JFXButton closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
navLeft.getChildren().add(backNavButton);
}
if (canBack) navLeft.getChildren().add(backNavButton);
if (canClose) navLeft.getChildren().add(closeNavButton);
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
else
if (canClose) {
JFXButton closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
navBar.setLeft(navLeft);
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
else
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
navLeft.getChildren().add(closeNavButton);
}
if (canBack || canClose) {
navBar.setLeft(navLeft);
}
BorderPane center = new BorderPane();
if (title != null) {
Label titleLabel = new Label();
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.getStyleClass().add("jfx-decorator-title");
if (titleBarTransparent) titleLabel.pseudoClassStateChanged(TRANSPARENT, true);
if (!canClose && !canBack) {
titleLabel.setAlignment(Pos.CENTER);
// 20: in this case width of navLeft is 10, so to make the text center aligned,
// we should have width 2 * 10 reduced
titleLabel.setPrefWidth(leftPaneWidth - 20);
}
if (titleNode != null) {
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
// 18 = 10 (horizontal padding of navLeft) + 8 (margin-left)
return leftPaneWidth - 18 - backNavButton.getWidth() - closeNavButton.getWidth();
}, backNavButton.widthProperty(), closeNavButton.widthProperty()));
// 8 (margin-left)
return leftPaneWidth - 8 - navLeft.getWidth();
}, navLeft.widthProperty()));
}
titleLabel.setText(title);
center.setLeft(titleLabel);
@@ -26,49 +26,48 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public abstract class DecoratorTabPage extends DecoratorTransitionPage implements TabControl {
public DecoratorTabPage() {
getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
if (newValue.getNode() == null && newValue.getNodeSupplier() != null) {
newValue.setNode(newValue.getNodeSupplier().get());
}
newValue.initializeIfNeeded();
if (newValue.getNode() != null) {
onNavigating(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATING));
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING));
navigate(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
onNavigated(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATED));
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED));
}
});
}
public DecoratorTabPage(TabHeader.Tab... tabs) {
public DecoratorTabPage(TabHeader.Tab<?>... tabs) {
this();
if (tabs != null) {
getTabs().addAll(tabs);
}
}
private ObservableList<TabHeader.Tab> tabs = FXCollections.observableArrayList();
private ObservableList<TabHeader.Tab<?>> tabs = FXCollections.observableArrayList();
@Override
public ObservableList<TabHeader.Tab> getTabs() {
public ObservableList<TabHeader.Tab<?>> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<TabHeader.Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControl.TabControlSelectionModel(this));
private final ObjectProperty<SingleSelectionModel<TabHeader.Tab<?>>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControl.TabControlSelectionModel(this));
public SingleSelectionModel<TabHeader.Tab> getSelectionModel() {
public SingleSelectionModel<TabHeader.Tab<?>> getSelectionModel() {
return selectionModel.get();
}
public ObjectProperty<SingleSelectionModel<TabHeader.Tab>> selectionModelProperty() {
public ObjectProperty<SingleSelectionModel<TabHeader.Tab<?>>> selectionModelProperty() {
return selectionModel;
}
public void setSelectionModel(SingleSelectionModel<TabHeader.Tab> selectionModel) {
public void setSelectionModel(SingleSelectionModel<TabHeader.Tab<?>> selectionModel) {
this.selectionModel.set(selectionModel);
}
}
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.ui.wizard.Refreshable;
public abstract class DecoratorTransitionPage extends Control implements DecoratorPage {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(""));
private final DoubleProperty leftPaneWidth = new SimpleDoubleProperty();
private final BooleanProperty titleBarTransparent = new SimpleBooleanProperty(false);
private final BooleanProperty backable = new SimpleBooleanProperty(false);
private final BooleanProperty refreshable = new SimpleBooleanProperty(false);
private Node currentPage;
@@ -57,11 +56,11 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
if (to instanceof DecoratorPage) {
state.bind(Bindings.createObjectBinding(() -> {
State state = ((DecoratorPage) to).stateProperty().get();
return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true, titleBarTransparent.get(), leftPaneWidth.get());
return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true, leftPaneWidth.get());
}, ((DecoratorPage) to).stateProperty()));
} else {
state.unbind();
state.set(new State("", null, backable.get(), false, true, titleBarTransparent.get(), leftPaneWidth.get()));
state.set(new State("", null, backable.get(), false, true, leftPaneWidth.get()));
}
if (to instanceof Region) {
@@ -121,16 +120,4 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
public void setLeftPaneWidth(double leftPaneWidth) {
this.leftPaneWidth.set(leftPaneWidth);
}
public boolean isTitleBarTransparent() {
return titleBarTransparent.get();
}
public BooleanProperty titleBarTransparentProperty() {
return titleBarTransparent;
}
public void setTitleBarTransparent(boolean titleBarTransparent) {
this.titleBarTransparent.set(titleBarTransparent);
}
}
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.wizard.*;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
@@ -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,9 +20,11 @@ package org.jackhuang.hmcl.ui.download;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -40,7 +42,7 @@ class AdditionalInstallersPage extends InstallersPage {
protected final String gameVersion;
protected final Version version;
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, GameRepository repository, InstallerWizardDownloadProvider downloadProvider) {
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) {
super(controller, repository, gameVersion, downloadProvider);
this.gameVersion = gameVersion;
this.version = version;
@@ -84,13 +86,14 @@ class AdditionalInstallersPage extends InstallersPage {
protected void reload() {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version.resolvePreservingPatches(repository));
String game = analyzer.getVersion(MINECRAFT).orElse(null);
String fabric = analyzer.getVersion(FABRIC).orElse(null);
String forge = analyzer.getVersion(FORGE).orElse(null);
String liteLoader = analyzer.getVersion(LITELOADER).orElse(null);
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
String fabric = analyzer.getVersion(FABRIC).orElse(null);
String fabricApi = analyzer.getVersion(FABRIC_API).orElse(null);
InstallerItem[] libraries = group.getLibraries();
String[] versions = new String[]{game, fabric, forge, liteLoader, optiFine};
String[] versions = new String[]{game, forge, liteLoader, optiFine, fabric, fabricApi};
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
@@ -0,0 +1,323 @@
/*
* 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.download;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.Node;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.mod.DownloadManager;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
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.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DownloadPage extends BorderPane implements DecoratorPage {
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
private final TabHeader tab;
private final TabHeader.Tab<VersionsPage> newGameTab = new TabHeader.Tab<>("newGameTab");
private final TabHeader.Tab<DownloadListPage> modTab = new TabHeader.Tab<>("modTab");
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<DownloadListPage> customizationTab = new TabHeader.Tab<>("customizationTab");
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
private final TransitionPane transitionPane = new TransitionPane();
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
private WeakListenerHolder listenerHolder;
public DownloadPage() {
newGameTab.setNodeSupplier(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected));
modpackTab.setNodeSupplier(() -> {
DownloadListPage page = new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl);
JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack"));
installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED);
installLocalModpackButton.getStyleClass().add("jfx-button-raised");
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
page.getActions().add(installLocalModpackButton);
return page;
});
modTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MOD, (profile, version, file) -> download(profile, version, file, "mods"), true));
resourcePackTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_RESOURCE_PACK, (profile, version, file) -> download(profile, version, file, "resourcepacks")));
// customizationTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_CUSTOMIZATION, this::download));
worldTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_WORLD));
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
Profiles.registerVersionsListener(this::loadVersions);
tab.getSelectionModel().select(newGameTab);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
if (newValue.initializeIfNeeded()) {
if (newValue.getNode() instanceof VersionPage.VersionLoadable) {
((VersionPage.VersionLoadable) newValue.getNode()).loadVersion(Profiles.getSelectedProfile(), null);
}
}
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
{
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("game"));
item.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 24, 24)));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(newGameTab));
item.setOnAction(e -> tab.getSelectionModel().select(newGameTab));
})
.startCategory(i18n("download"))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("mods"));
item.setLeftGraphic(wrap(SVG.puzzle(Theme.blackFillBinding(), 24, 24)));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab));
item.setOnAction(e -> tab.getSelectionModel().select(modTab));
})
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("modpack"));
settingsItem.setLeftGraphic(wrap(SVG.pack(Theme.blackFillBinding(), 24, 24)));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modpackTab));
settingsItem.setOnAction(e -> tab.getSelectionModel().select(modpackTab));
})
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("resourcepack"));
item.setLeftGraphic(wrap(SVG.textureBox(Theme.blackFillBinding(), 24, 24)));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
item.setOnAction(e -> tab.getSelectionModel().select(resourcePackTab));
})
// .addNavigationDrawerItem(item -> {
// item.setTitle(i18n("download.curseforge.customization"));
// item.setLeftGraphic(wrap(SVG.script(Theme.blackFillBinding(), 24, 24)));
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
// item.setOnAction(e -> tab.getSelectionModel().select(customizationTab));
// })
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("world"));
item.setLeftGraphic(wrap(SVG.earth(Theme.blackFillBinding(), 24, 24)));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
item.setOnAction(e -> tab.getSelectionModel().select(worldTab));
});
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
}
setCenter(transitionPane);
}
private void download(Profile profile, @Nullable String version, DownloadManager.Version file, String subdirectoryName) {
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
Path dest = runDirectory.resolve(subdirectoryName).resolve(file.getFile().getFilename());
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(exception -> {
if (exception != null) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
} else {
Controllers.showToast(i18n("install.success"));
}
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}
private void loadVersions(Profile profile) {
WeakListenerHolder listenerHolder = new WeakListenerHolder();
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
if (modTab.isInitialized()) {
modTab.getNode().loadVersion(profile, null);
}
if (modpackTab.isInitialized()) {
modpackTab.getNode().loadVersion(profile, null);
}
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
}
if (customizationTab.isInitialized()) {
customizationTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
}
});
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private class DownloadNavigator implements Navigation {
private final Map<String, Object> settings = new HashMap<>();
@Override
public void onStart() {
}
@Override
public void onNext() {
}
@Override
public void onPrev(boolean cleanUp) {
}
@Override
public boolean canPrev() {
return false;
}
@Override
public void onFinish() {
}
@Override
public void onEnd() {
}
@Override
public void onCancel() {
}
@Override
public Map<String, Object> getSettings() {
return settings;
}
public void onGameSelected() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile, (GameRemoteVersion) settings.get("game")), i18n("install.new_game"));
}
}
}
private static class VanillaInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final DefaultDependencyManager dependencyManager;
private final DownloadProvider downloadProvider;
private final GameRemoteVersion gameVersion;
public VanillaInstallWizardProvider(Profile profile, GameRemoteVersion gameVersion) {
this.profile = profile;
this.gameVersion = gameVersion;
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = profile.getDependency(downloadProvider);
}
@Override
public void start(Map<String, Object> settings) {
settings.put(PROFILE, profile);
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion);
}
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
GameBuilder builder = dependencyManager.gameBuilder();
String name = (String) settings.get("name");
builder.name(name);
builder.gameVersion(((RemoteVersion) settings.get(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())).getGameVersion());
for (Map.Entry<String, Object> entry : settings.entrySet())
if (!LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(entry.getKey()) && entry.getValue() instanceof RemoteVersion)
builder.version((RemoteVersion) entry.getValue());
return builder.buildAsync().whenComplete(any -> profile.getRepository().refreshVersions())
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
}
@Override
public Object finish(Map<String, Object> settings) {
settings.put("title", i18n("install.new_game"));
settings.put("success_message", i18n("install.success"));
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
return finishVersionDownloadingAsync(settings);
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
switch (step) {
case 0:
return new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@Override
public boolean cancel() {
return true;
}
public static final String PROFILE = "PROFILE";
}
}
@@ -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
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -31,17 +31,20 @@ import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.util.Map;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InstallersPage extends Control implements WizardPage {
@@ -51,16 +54,14 @@ public class InstallersPage extends Control implements WizardPage {
protected JFXTextField txtName = new JFXTextField();
protected BooleanProperty installable = new SimpleBooleanProperty();
public InstallersPage(WizardController controller, GameRepository repository, String gameVersion, InstallerWizardDownloadProvider downloadProvider) {
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
this.controller = controller;
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(i18n("install.new_game.already_exists"));
Validator nameValidator = new Validator(OperatingSystem::isNameValid);
nameValidator.setMessage(i18n("install.new_game.malformed"));
txtName.getValidators().addAll(hasVersion, nameValidator);
installable.bind(Bindings.createBooleanBinding(() -> txtName.validate(),
txtName.textProperty()));
txtName.getValidators().addAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
txtName.setText(gameVersion);
group.game.installable.setValue(false);
@@ -69,6 +70,10 @@ public class InstallersPage extends Control implements WizardPage {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
library.action.set(e -> {
if ("fabric-api".equals(libraryId)) {
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
}
if (library.incompatibleLibraryName.get() == null)
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(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
@@ -20,12 +20,13 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile;
@@ -33,19 +34,19 @@ 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.WebStage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.util.Map;
import java.util.Optional;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -93,15 +94,14 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
txtModpackName.setDisable(true);
} else {
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
btnInstall.disableProperty().bind(
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
.not());
}
btnInstall.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtModpackName.validate(),
txtModpackName.textProperty()));
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
selectedFile = filePath.get();
@@ -131,7 +131,8 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
txtModpackName.setText(manifest.getName());
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
@@ -154,10 +155,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
@FXML
private void onDescribe() {
if (manifest != null) {
WebStage stage = new WebStage();
stage.getWebView().getEngine().loadContent(manifest.getDescription());
stage.setTitle(i18n("modpack.description"));
stage.showAndWait();
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
}
@@ -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,21 +24,23 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebStage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -101,12 +103,15 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
lblAuthor.setText(manifest.getAuthor());
if (!name.isPresent()) {
txtModpackName.setText(manifest.getName());
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
btnInstall.disableProperty().bind(
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
.not());
}
}
@@ -124,10 +129,7 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
@FXML
private void onDescribe() {
WebStage stage = new WebStage();
stage.getWebView().getEngine().loadContent(manifest.getDescription());
stage.setTitle(i18n("modpack.description"));
stage.showAndWait();
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
@Override
@@ -18,10 +18,7 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.ArtifactMalformedException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
@@ -57,7 +54,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
private final Version version;
private final String libraryId;
private final String oldLibraryVersion;
private final InstallerWizardDownloadProvider downloadProvider;
private final DownloadProvider downloadProvider;
public UpdateInstallerWizardProvider(@NotNull Profile profile, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
this.profile = profile;
@@ -65,7 +62,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
this.version = version;
this.libraryId = libraryId;
this.oldLibraryVersion = oldLibraryVersion;
this.downloadProvider = new InstallerWizardDownloadProvider(DownloadProviders.getDownloadProvider());
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = profile.getDependency(downloadProvider);
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.setting.DownloadProviders;
@@ -35,11 +36,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VanillaInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final DefaultDependencyManager dependencyManager;
private final InstallerWizardDownloadProvider downloadProvider;
private final DownloadProvider downloadProvider;
public VanillaInstallWizardProvider(Profile profile) {
this.profile = profile;
this.downloadProvider = new InstallerWizardDownloadProvider(DownloadProviders.getDownloadProvider());
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = profile.getDependency(downloadProvider);
}
@@ -18,44 +18,45 @@
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
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.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion;
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
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;
@@ -63,7 +64,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private final String gameVersion;
private final String libraryId;
private final String title;
private final WizardController controller;
private final Navigation navigation;
@FXML
private JFXListView<RemoteVersion> list;
@@ -84,87 +85,111 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@FXML
private HBox checkPane;
@FXML
private VBox centrePane;
private ComponentList centrePane;
@FXML
private JFXComboBox<String> downloadSourceComboBox;
private StackPane center;
private VersionList<?> versionList;
private TaskExecutor executor;
private CompletableFuture<?> executor;
public VersionsPage(WizardController controller, String title, String gameVersion, InstallerWizardDownloadProvider downloadProvider, String libraryId, Runnable callback) {
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.title = title;
this.gameVersion = gameVersion;
this.libraryId = libraryId;
this.controller = controller;
this.navigation = navigation;
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
downloadSourceComboBox.getItems().setAll(DownloadProviders.providersById.keySet());
downloadSourceComboBox.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourceComboBox.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
controller.getSettings().put("downloadProvider", newValue);
downloadProvider.setDownloadProvider(DownloadProviders.getDownloadProviderByPrimaryId(newValue));
versionList = downloadProvider.getVersionListById(libraryId);
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else {
centrePane.getChildren().setAll(list);
}
refresh();
});
downloadSourceComboBox.getSelectionModel().select((String)controller.getSettings().getOrDefault("downloadProvider", DownloadProviders.getPrimaryDownloadProviderId()));
versionList = downloadProvider.getVersionListById(libraryId);
if (versionList.hasType()) {
centrePane.getContent().setAll(checkPane, list);
} else {
centrePane.getContent().setAll(list);
}
ComponentList.setVgrow(list, Priority.ALWAYS);
InvalidationListener listener = o -> list.getItems().setAll(loadVersions());
chkRelease.selectedProperty().addListener(listener);
chkSnapshot.selectedProperty().addListener(listener);
chkOld.selectedProperty().addListener(listener);
list.setCellFactory(listView -> new FloatListCell<RemoteVersion>() {
ImageView imageView = new ImageView();
TwoLineListItem content = new TwoLineListItem();
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
IconedTwoLineListItem content = new IconedTwoLineListItem();
RipplerContainer ripplerContainer = new RipplerContainer(content);
StackPane pane = new StackPane();
{
HBox container = new HBox(12);
container.setPadding(new Insets(0, 0, 0, 6));
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
container.getChildren().setAll(imageView, content);
pane.getStyleClass().add("md-list-cell");
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
pane.getChildren().setAll(ripplerContainer);
}
@Override
protected void updateControl(RemoteVersion remoteVersion, boolean empty) {
if (empty) return;
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
super.updateItem(remoteVersion, empty);
if (empty) {
setGraphic(null);
return;
}
setGraphic(pane);
content.setTitle(remoteVersion.getSelfVersion());
content.setSubtitle(remoteVersion.getGameVersion());
if (remoteVersion.getReleaseDate() != null) {
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate()));
} else {
content.setSubtitle("");
}
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
content.setSubtitle(i18n("version.game.release"));
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
content.getTags().setAll(i18n("version.game.release"));
content.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
case SNAPSHOT:
content.setSubtitle(i18n("version.game.snapshot"));
imageView.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
break;
default:
content.setSubtitle(i18n("version.game.old"));
imageView.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
content.getTags().setAll(i18n("version.game.old"));
content.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
break;
}
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
imageView.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
content.setSubtitle(remoteVersion.getGameVersion());
content.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
imageView.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
content.setSubtitle(remoteVersion.getGameVersion());
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof ForgeRemoteVersion) {
imageView.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
content.setSubtitle(remoteVersion.getGameVersion());
content.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof FabricRemoteVersion) {
imageView.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
content.setSubtitle(remoteVersion.getGameVersion());
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof FabricAPIRemoteVersion) {
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
}
}
});
@@ -172,9 +197,10 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
return;
controller.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
callback.run();
});
refresh();
}
@@ -199,7 +225,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(exception -> {
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
@@ -215,7 +241,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
} else {
list.getItems().setAll(items);
}
root.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(center, ContainerAnimations.FADE.getAnimationProducer());
}
});
} else {
@@ -225,7 +251,10 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
});
}
}).executor().start();
// https://github.com/huanghongxun/HMCL/issues/938
System.gc();
});
}
@Override
@@ -237,7 +266,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId);
if (executor != null)
executor.cancel();
executor.cancel(true);
}
@FXML
@@ -246,7 +275,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
}
@FXML
private void onBack() { controller.onPrev(true); }
private void onBack() { navigation.onPrev(true); }
@FXML
private void onSponsor() {
@@ -87,21 +87,24 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null)
if (files != null) {
for (File it : files) {
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + it.getName());
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected())
node.setIndeterminate(true);
node.getChildren().add(subNode);
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected()) {
node.setIndeterminate(true);
}
node.getChildren().add(subNode);
}
}
}
if (!node.isSelected()) node.setIndeterminate(false);
// Empty folder need not to be displayed.
if (node.getChildren().isEmpty())
if (node.getChildren().isEmpty()) {
return null;
}
}
HBox graphic = new HBox();
@@ -126,12 +129,13 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
private void getFilesNeeded(CheckBoxTreeItem<String> node, String basePath, List<String> list) {
if (node == null) return;
if (node.isSelected()) {
if (node.isSelected() || node.isIndeterminate()) {
if (basePath.length() > "minecraft/".length())
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
for (TreeItem<String> child : node.getChildren()) {
if (child instanceof CheckBoxTreeItem)
if (child instanceof CheckBoxTreeItem) {
getFilesNeeded(((CheckBoxTreeItem<String>) child), basePath + "/" + child.getValue(), list);
}
}
}
}
@@ -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
@@ -78,13 +78,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private final SimpleStringProperty url = new SimpleStringProperty("");
private final SimpleBooleanProperty forceUpdate = new SimpleBooleanProperty();
private final SimpleBooleanProperty packWithLauncher = new SimpleBooleanProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty("");
private final SimpleIntegerProperty minMemory = new SimpleIntegerProperty(0);
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller;
@@ -215,7 +215,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
txtModpackName.textProperty().bindBidirectional(skinnable.name);
txtModpackName.setLabelFloat(true);
txtModpackName.setPromptText(i18n("modpack.name"));
txtModpackName.getValidators().add(new RequiredValidator(i18n("modpack.not_a_valid_name")));
txtModpackName.getValidators().add(new RequiredValidator());
StackPane.setMargin(txtModpackName, insets);
list.getContent().add(txtModpackName);
@@ -0,0 +1,177 @@
/*
* 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.main;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AboutPage extends StackPane {
public AboutPage() {
ComponentList about = new ComponentList();
{
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
launcher.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
launcher.setTitle("Hello Minecraft! Launcher");
launcher.setSubtitle(Metadata.VERSION);
launcher.setExternalLink("https://hmcl.huangyuhui.net");
IconedTwoLineListItem author = new IconedTwoLineListItem();
author.setImage(new Image("/assets/img/yellow_fish.jpg", 32, 32, false, true));
author.setTitle("huanghongxun");
author.setSubtitle("https://www.huangyuhui.net");
author.setExternalLink("https://www.huangyuhui.net");
about.getContent().setAll(launcher, author);
}
ComponentList thanks = new ComponentList();
{
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
yushijinhun.setImage(new Image("/assets/img/yushijinhun.jpg", 32, 32, false, true));
yushijinhun.setTitle("yushijinhun");
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
yushijinhun.setExternalLink("https://yushi.moe/");
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
bangbang93.setImage(new Image("/assets/img/bangbang93.jpg", 32, 32, false, true));
bangbang93.setTitle("bangbang93");
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
redLnn.setTitle("Red_lnn");
redLnn.setImage(new Image("/assets/img/red_lnn.jpg", 32, 32, false, true));
redLnn.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
mcbbs.setImage(new Image("/assets/img/chest.png", 32, 32, false, true));
mcbbs.setTitle(i18n("about.thanks_to.mcbbs"));
mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement"));
mcbbs.setExternalLink("https://www.mcbbs.net/");
IconedTwoLineListItem mcmod = new IconedTwoLineListItem();
mcmod.setImage(new Image("/assets/img/mcmod.png", 32, 32, false, true));
mcmod.setTitle(i18n("about.thanks_to.mcmod"));
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(new Image("/assets/img/github.png", 32, 32, false, true));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
users.setTitle(i18n("about.thanks_to.users"));
users.setSubtitle(i18n("about.thanks_to.users.statement"));
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
thanks.getContent().setAll(yushijinhun, bangbang93, mcbbs, mcmod, redLnn, users, contributors);
}
ComponentList dep = new ComponentList();
{
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
javafx.setTitle("JavaFX");
javafx.setSubtitle("Copyright (c) 2013, 2021, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setExternalLink("https://openjfx.io/");
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
jfoenix.setTitle("JFoenix");
jfoenix.setSubtitle("Copyright (c) 2016 JFoenix.\nLicensed under the MIT License.");
jfoenix.setExternalLink("http://www.jfoenix.com/");
IconedTwoLineListItem gson = new IconedTwoLineListItem();
gson.setTitle("Gson");
gson.setSubtitle("Copyright 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
gson.setExternalLink("https://github.com/google/gson");
IconedTwoLineListItem xz = new IconedTwoLineListItem();
xz.setTitle("XZ for Java");
xz.setSubtitle("Lasse Collin, Igor Pavlov, and/or Brett Okken.\nPublic Domain.");
xz.setExternalLink("https://tukaani.org/xz/java.html");
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
fxgson.setTitle("fx-gson");
fxgson.setSubtitle("Copyright (c) 2016 Joffrey Bion.\nLicensed under the MIT License.");
fxgson.setExternalLink("https://github.com/joffrey-bion/fx-gson");
IconedTwoLineListItem constantPoolScanner = new IconedTwoLineListItem();
constantPoolScanner.setTitle("Constant Pool Scanner");
constantPoolScanner.setSubtitle("Copyright 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
constantPoolScanner.setExternalLink("https://github.com/jenkinsci/constant-pool-scanner");
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
openNBT.setTitle("OpenNBT");
openNBT.setSubtitle("Copyright (C) 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT);
}
ComponentList legal = new ComponentList();
{
TwoLineListItem copyright = new TwoLineListItem();
copyright.setTitle(i18n("about.copyright"));
copyright.setSubtitle(i18n("about.copyright.statement"));
TwoLineListItem claim = new TwoLineListItem();
claim.setTitle(i18n("about.claim"));
claim.setSubtitle(i18n("about.claim.statement"));
TwoLineListItem openSource = new TwoLineListItem();
openSource.setTitle(i18n("about.open_source"));
openSource.setSubtitle(i18n("about.open_source.statement"));
legal.getContent().setAll(copyright, claim, openSource);
}
VBox content = new VBox(16);
content.setPadding(new Insets(10));
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("about")),
about,
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
dep,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(content);
scrollPane.setFitToWidth(true);
getChildren().setAll(scrollPane);
}
}
@@ -0,0 +1,319 @@
/*
* 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.main;
import com.jfoenix.controls.*;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.net.Proxy;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reversedSelectedPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
public class DownloadSettingsPage extends StackPane {
public DownloadSettingsPage() {
VBox content = new VBox(10);
content.setPadding(new Insets(10));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
getChildren().setAll(scrollPane);
{
ComponentList downloadSource = new ComponentList();
{
VBox pane = new VBox(8);
VBox chooseWrapper = new VBox();
chooseWrapper.setPadding(new Insets(8, 0, 8, 0));
JFXCheckBox chkAutoChooseDownloadSource = new JFXCheckBox(i18n("settings.launcher.download_source.auto"));
chkAutoChooseDownloadSource.selectedProperty().bindBidirectional(config().autoChooseDownloadTypeProperty());
chooseWrapper.getChildren().setAll(chkAutoChooseDownloadSource);
BorderPane versionListSourcePane = new BorderPane();
versionListSourcePane.setPadding(new Insets(0, 0, 8, 30));
versionListSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty().not());
{
Label label = new Label(i18n("settings.launcher.version_list_source"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
versionListSourcePane.setLeft(label);
JFXComboBox<String> cboVersionListSource = new JFXComboBox<>();
cboVersionListSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
versionListSourcePane.setRight(cboVersionListSource);
FXUtils.setLimitWidth(cboVersionListSource, 420);
cboVersionListSource.getItems().setAll(DownloadProviders.providersById.keySet());
selectedItemPropertyFor(cboVersionListSource).bindBidirectional(config().versionListSourceProperty());
}
BorderPane downloadSourcePane = new BorderPane();
downloadSourcePane.setPadding(new Insets(0, 0, 8, 30));
downloadSourcePane.disableProperty().bind(chkAutoChooseDownloadSource.selectedProperty());
{
Label label = new Label(i18n("settings.launcher.download_source"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
downloadSourcePane.setLeft(label);
JFXComboBox<String> cboDownloadSource = new JFXComboBox<>();
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourcePane.setRight(cboDownloadSource);
FXUtils.setLimitWidth(cboDownloadSource, 420);
cboDownloadSource.getItems().setAll(DownloadProviders.rawProviders.keySet());
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
}
pane.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
downloadSource.getContent().add(pane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.version_list_source")), downloadSource);
}
{
ComponentList downloadThreads = new ComponentList();
{
VBox pane = new VBox(16);
pane.setPadding(new Insets(8, 0, 8, 0));
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(config().autoDownloadThreadsProperty());
pane.getChildren().add(chkAutoDownloadThreads);
chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
config().downloadThreadsProperty().set(FetchTask.DEFAULT_CONCURRENCY);
}
});
}
{
HBox hbox = new HBox(8);
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(config().autoDownloadThreadsProperty());
Label label = new Label(i18n("settings.launcher.download.threads"));
JFXSlider slider = new JFXSlider(1, 256, 64);
HBox.setHgrow(slider, Priority.ALWAYS);
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
threadsField.textProperty().bindBidirectional(config().downloadThreadsProperty(), SafeStringConverter.fromInteger());
AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(config().downloadThreadsProperty(), value -> {
changedByTextField.set(true);
slider.setValue(value.intValue());
changedByTextField.set(false);
});
slider.valueProperty().addListener((value, oldVal, newVal) -> {
if (changedByTextField.get()) return;
config().downloadThreadsProperty().set(value.getValue().intValue());
});
hbox.getChildren().setAll(label, slider, threadsField);
pane.getChildren().add(hbox);
}
{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(config().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
pane.getChildren().add(hintPane);
}
downloadThreads.getContent().add(pane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadThreads);
}
{
ComponentList proxyList = new ComponentList();
VBox proxyWrapper = new VBox();
proxyWrapper.setPadding(new Insets(8, 0, 8, 0));
proxyWrapper.setSpacing(10);
VBox proxyPane = new VBox();
{
JFXCheckBox chkDisableProxy = new JFXCheckBox(i18n("settings.launcher.proxy.disable"));
proxyWrapper.getChildren().add(chkDisableProxy);
reversedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
}
{
proxyPane.setPadding(new Insets(0, 0, 0, 30));
ColumnConstraints colHgrow = new ColumnConstraints();
colHgrow.setHgrow(Priority.ALWAYS);
JFXRadioButton chkProxyNone;
JFXRadioButton chkProxyHttp;
JFXRadioButton chkProxySocks;
{
HBox hBox = new HBox();
chkProxyNone = new JFXRadioButton(i18n("settings.launcher.proxy.none"));
chkProxyHttp = new JFXRadioButton(i18n("settings.launcher.proxy.http"));
chkProxySocks = new JFXRadioButton(i18n("settings.launcher.proxy.socks"));
hBox.getChildren().setAll(chkProxyNone, chkProxyHttp, chkProxySocks);
proxyPane.getChildren().add(hBox);
}
{
GridPane gridPane = new GridPane();
gridPane.setHgap(20);
gridPane.setVgap(10);
gridPane.setStyle("-fx-padding: 0 0 0 15;");
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), colHgrow);
gridPane.getRowConstraints().setAll(new RowConstraints(), new RowConstraints());
{
Label host = new Label(i18n("settings.launcher.proxy.host"));
GridPane.setRowIndex(host, 1);
GridPane.setColumnIndex(host, 0);
GridPane.setHalignment(host, HPos.RIGHT);
gridPane.getChildren().add(host);
}
{
JFXTextField txtProxyHost = new JFXTextField();
GridPane.setRowIndex(txtProxyHost, 1);
GridPane.setColumnIndex(txtProxyHost, 1);
gridPane.getChildren().add(txtProxyHost);
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
txtProxyHost.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
}
{
Label port = new Label(i18n("settings.launcher.proxy.port"));
GridPane.setRowIndex(port, 2);
GridPane.setColumnIndex(port, 0);
GridPane.setHalignment(port, HPos.RIGHT);
gridPane.getChildren().add(port);
}
{
JFXTextField txtProxyPort = new JFXTextField();
GridPane.setRowIndex(txtProxyPort, 2);
GridPane.setColumnIndex(txtProxyPort, 1);
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
gridPane.getChildren().add(txtProxyPort);
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty(),
SafeStringConverter.fromInteger()
.restrict(it -> it >= 0 && it <= 0xFFFF)
.fallbackTo(0)
.asPredicate(Validator.addTo(txtProxyPort)));
}
proxyPane.getChildren().add(gridPane);
}
GridPane authPane = new GridPane();
{
VBox vBox = new VBox();
vBox.setStyle("-fx-padding: 20 0 20 5;");
JFXCheckBox chkProxyAuthentication = new JFXCheckBox(i18n("settings.launcher.proxy.authentication"));
vBox.getChildren().setAll(chkProxyAuthentication);
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
proxyPane.getChildren().add(vBox);
}
{
authPane.setHgap(20);
authPane.setVgap(10);
authPane.setStyle("-fx-padding: 0 0 0 15;");
authPane.getColumnConstraints().setAll(new ColumnConstraints(), colHgrow);
authPane.getRowConstraints().setAll(new RowConstraints(), new RowConstraints());
{
Label username = new Label(i18n("settings.launcher.proxy.username"));
GridPane.setRowIndex(username, 0);
GridPane.setColumnIndex(username, 0);
authPane.getChildren().add(username);
}
{
JFXTextField txtProxyUsername = new JFXTextField();
GridPane.setRowIndex(txtProxyUsername, 0);
GridPane.setColumnIndex(txtProxyUsername, 1);
authPane.getChildren().add(txtProxyUsername);
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
}
{
Label password = new Label(i18n("settings.launcher.proxy.password"));
GridPane.setRowIndex(password, 1);
GridPane.setColumnIndex(password, 0);
authPane.getChildren().add(password);
}
{
JFXPasswordField txtProxyPassword = new JFXPasswordField();
GridPane.setRowIndex(txtProxyPassword, 1);
GridPane.setColumnIndex(txtProxyPassword, 1);
authPane.getChildren().add(txtProxyPassword);
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
}
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyNone.setUserData(Proxy.Type.DIRECT);
chkProxyNone.setToggleGroup(proxyConfigurationGroup);
chkProxyHttp.setUserData(Proxy.Type.HTTP);
chkProxyHttp.setToggleGroup(proxyConfigurationGroup);
chkProxySocks.setUserData(Proxy.Type.SOCKS);
chkProxySocks.setToggleGroup(proxyConfigurationGroup);
selectedItemPropertyFor(proxyConfigurationGroup, Proxy.Type.class).bindBidirectional(config().proxyTypeProperty());
proxyPane.getChildren().add(authPane);
}
proxyWrapper.getChildren().add(proxyPane);
}
proxyList.getContent().add(proxyWrapper);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.proxy")), proxyList);
}
}
}
@@ -0,0 +1,452 @@
/*
* 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.main;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.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.*;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.List;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FeedbackPage extends VBox {
private final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList();
private final SpinnerPane spinnerPane = new SpinnerPane();
public FeedbackPage() {
setSpacing(10);
setPadding(new Insets(10));
{
HBox loginPane = new HBox(16);
loginPane.setAlignment(Pos.CENTER_LEFT);
loginPane.getStyleClass().add("card");
TwoLineListItem accountInfo = new TwoLineListItem();
HBox.setHgrow(accountInfo, Priority.ALWAYS);
accountInfo.titleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
accountInfo.subtitleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
JFXButton logButton = new JFXButton();
logButton.textProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.login") : i18n("account.logout")));
logButton.setOnAction(e -> log());
loginPane.getChildren().setAll(accountInfo, logButton);
getChildren().add(loginPane);
}
{
HBox searchPane = new HBox(8);
searchPane.getStyleClass().add("card");
getChildren().add(searchPane);
JFXTextField searchField = new JFXTextField();
searchField.setOnAction(e -> search(searchField.getText()));
HBox.setHgrow(searchField, Priority.ALWAYS);
searchField.setPromptText(i18n("search"));
JFXButton searchButton = new JFXButton();
searchButton.getStyleClass().add("toggle-icon4");
searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1));
searchButton.setOnAction(e -> addFeedback());
searchPane.getChildren().setAll(searchField, searchButton);
}
{
spinnerPane.getStyleClass().add("card");
VBox.setVgrow(spinnerPane, Priority.ALWAYS);
JFXListView<FeedbackResponse> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), feedbacks);
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView) {
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton likeButton = new JFXButton();
private final JFXButton unlikeButton = new JFXButton();
{
HBox container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(content, Priority.ALWAYS);
content.setMouseTransparent(true);
setSelectable();
likeButton.getStyleClass().add("toggle-icon4");
likeButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
unlikeButton.getStyleClass().add("toggle-icon4");
unlikeButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
container.getChildren().setAll(content, likeButton, unlikeButton);
StackPane.setMargin(container, new Insets(10, 16, 10, 16));
getContainer().getChildren().setAll(container);
}
@Override
protected void updateControl(FeedbackResponse feedback, boolean empty) {
content.setTitle(feedback.getTitle());
content.setSubtitle(feedback.getContent());
content.getTags().add("#" + feedback.getId());
content.getTags().add(feedback.getAuthor());
content.getTags().add(feedback.getLauncherVersion());
content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
}
});
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
FeedbackResponse selectedItem = listView.getSelectionModel().getSelectedItem();
Controllers.dialog(new ViewFeedbackDialog(selectedItem));
});
getChildren().add(spinnerPane);
}
}
private void search(String keyword) {
Task.supplyAsync(() -> {
return HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback", pair("s", keyword)).<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
}).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
spinnerPane.hideSpinner();
if (exception != null) {
spinnerPane.setFailedReason(i18n("feedback.failed"));
} else {
feedbacks.setAll(result);
}
}).start();
}
private void log() {
if (account.get() == null) {
// login
Controllers.dialog(new LoginDialog());
} else {
// logout
account.set(null);
}
}
private void addFeedback() {
if (account.get() == null) {
Controllers.dialog(i18n("feedback.add.login"));
return;
}
Controllers.dialog(new AddFeedbackDialog());
}
private static class HMCLAccount {
private final String nickname;
private final String email;
private final String accessToken;
public HMCLAccount(String nickname, String email, String accessToken) {
this.nickname = nickname;
this.email = email;
this.accessToken = accessToken;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getAccessToken() {
return accessToken;
}
}
private static class HMCLLoginResponse {
private final int err;
private final String nickname;
private final String email;
private final String accessToken;
public HMCLLoginResponse(int err, String nickname, String email, String accessToken) {
this.err = err;
this.nickname = nickname;
this.email = email;
this.accessToken = accessToken;
}
public int getErr() {
return err;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getAccessToken() {
return accessToken;
}
public static final int ERR_OK = 0;
public static final int ERR_WRONG = 400;
}
private class LoginDialog extends JFXDialogLayout {
private final SpinnerPane spinnerPane = new SpinnerPane();
private final Label errorLabel = new Label();
public LoginDialog() {
setHeading(new Label(i18n("feedback.login")));
GridPane body = new GridPane();
ColumnConstraints fieldColumn = new ColumnConstraints();
fieldColumn.setFillWidth(true);
body.getColumnConstraints().setAll(new ColumnConstraints(), fieldColumn);
body.setVgap(8);
body.setHgap(8);
setBody(body);
JFXTextField usernameField = new JFXTextField();
usernameField.setValidators(new RequiredValidator());
body.addRow(0, new Label(i18n("account.username")), usernameField);
JFXPasswordField passwordField = new JFXPasswordField();
passwordField.setValidators(new RequiredValidator());
body.addRow(1, new Label(i18n("account.password")), passwordField);
JFXButton registerButton = new JFXButton();
registerButton.setText(i18n("account.register"));
registerButton.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/user/login"));
JFXButton loginButton = new JFXButton();
spinnerPane.setContent(loginButton);
loginButton.setText(i18n("account.login"));
loginButton.setOnAction(e -> login(usernameField.getText(), passwordField.getText()));
JFXButton cancelButton = new JFXButton();
cancelButton.setText(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
setActions(errorLabel, registerButton, spinnerPane, cancelButton);
}
private void login(String username, String password) {
spinnerPane.showSpinner();
errorLabel.setText("");
Task.supplyAsync(() -> {
return HttpRequest.POST("https://hmcl.huangyuhui.net/api/user/login")
.json(mapOf(
pair("username", username),
pair("password", password)
)).getJson(HMCLLoginResponse.class);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
if (exception instanceof IOException) {
if (exception instanceof ResponseCodeException && ((ResponseCodeException) exception).getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) {
errorLabel.setText(i18n("account.failed.invalid_password"));
} else {
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
}
} else if (exception instanceof JsonParseException) {
errorLabel.setText(i18n("account.failed.server_response_malformed"));
} else {
errorLabel.setText(exception.getClass().getName() + ": " + exception.getLocalizedMessage());
}
} else {
if (result.err == HMCLLoginResponse.ERR_OK) {
account.setValue(new HMCLAccount(result.getNickname(), result.getEmail(), result.getAccessToken()));
fireEvent(new DialogCloseEvent());
} else if (result.err == HMCLLoginResponse.ERR_WRONG) {
errorLabel.setText(i18n("account.failed.invalid_password"));
} else {
errorLabel.setText(i18n("account.failed", result.err));
}
}
}).start();
}
}
private static class AddFeedbackDialog extends JFXDialogLayout {
public AddFeedbackDialog() {
setHeading(new Label(i18n("feedback.add")));
GridPane body = new GridPane();
body.setVgap(8);
body.setHgap(8);
HintPane searchHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
GridPane.setColumnSpan(searchHintPane, 2);
searchHintPane.setText(i18n("feedback.add.hint.search_before_add"));
body.addRow(0, searchHintPane);
HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
GridPane.setColumnSpan(titleHintPane, 2);
titleHintPane.setText(i18n("feedback.add.hint.title"));
body.addRow(1, titleHintPane);
JFXTextField titleField = new JFXTextField();
titleField.setValidators(new RequiredValidator());
body.addRow(2, new Label(i18n("feedback.title")), titleField);
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
comboBox.setMaxWidth(-1);
comboBox.getItems().setAll(FeedbackType.values());
comboBox.getSelectionModel().select(0);
comboBox.setConverter(stringConverter(e -> i18n("feedback.type." + e.name().toLowerCase())));
body.addRow(3, new Label(i18n("feedback.type")), comboBox);
Label contentLabel = new Label(i18n("feedback.content"));
GridPane.setColumnSpan(contentLabel, 2);
body.addRow(4, contentLabel);
JFXTextArea contentArea = new JFXTextArea();
contentArea.setValidators(new RequiredValidator());
contentArea.setPromptText(i18n("feedback.add.hint.content"));
GridPane.setColumnSpan(contentArea, 2);
body.addRow(5, contentArea);
setBody(body);
JFXButton okButton = new JFXButton();
okButton.setText(i18n("button.ok"));
okButton.setOnAction(e -> addFeedback(titleField.getText(), comboBox.getSelectionModel().getSelectedItem(), contentArea.getText()));
JFXButton cancelButton = new JFXButton();
cancelButton.setText(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
setActions(okButton, cancelButton);
}
private void addFeedback(String title, FeedbackType feedbackType, String content) {
fireEvent(new DialogCloseEvent());
// TODO
}
}
private static class ViewFeedbackDialog extends JFXDialogLayout {
public ViewFeedbackDialog(FeedbackResponse feedback) {
BorderPane heading = new BorderPane();
TwoLineListItem left = new TwoLineListItem();
left.setTitle(feedback.getTitle());
left.setSubtitle(feedback.getAuthor());
left.getTags().add("#" + feedback.getId());
left.getTags().add(feedback.getLauncherVersion());
left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
setHeading(heading);
Label content = new Label(feedback.getContent());
content.setWrapText(true);
setBody(content);
JFXButton okButton = new JFXButton();
okButton.setText(i18n("button.ok"));
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
setActions(okButton);
}
}
private static class FeedbackResponse {
private final int id;
private final String title;
private final String content;
private final String author;
private final String launcherVersion;
private final String gameVersion;
private final FeedbackType type;
public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, String gameVersion, FeedbackType type) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
this.launcherVersion = launcherVersion;
this.gameVersion = gameVersion;
this.type = type;
}
public int getId() {
return id;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public String getAuthor() {
return author;
}
public String getLauncherVersion() {
return launcherVersion;
}
public String getGameVersion() {
return gameVersion;
}
public FeedbackType getType() {
return type;
}
}
private enum FeedbackType {
FEATURE_REQUEST,
BUG_REPORT
}
}
@@ -0,0 +1,140 @@
/*
* 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.main;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import javafx.geometry.Insets;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class HelpPage extends SpinnerPane {
private final VBox content;
public HelpPage() {
content = new VBox();
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
setContent(content);
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
docPane.setTitle(i18n("help.doc"));
docPane.setSubtitle(i18n("help.detail"));
docPane.setExternalLink(Metadata.HELP_URL);
ComponentList doc = new ComponentList();
doc.getContent().setAll(docPane);
content.getChildren().add(doc);
loadHelp();
}
private void loadHelp() {
showSpinner();
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/help/").getJson(new TypeToken<List<HelpCategory>>() {
}.getType()))
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList();
for (Help help : category.getItems()) {
IconedTwoLineListItem item = new IconedTwoLineListItem();
item.setTitle(help.getTitle());
item.setSubtitle(help.getSubtitle());
item.setExternalLink(help.getUrl());
categoryPane.getContent().add(item);
}
content.getChildren().add(ComponentList.createComponentListTitle(category.title));
content.getChildren().add(categoryPane);
}
hideSpinner();
}).start();
}
private static class HelpCategory {
@SerializedName("title")
private final String title;
@SerializedName("items")
private final List<Help> items;
public HelpCategory() {
this("", Collections.emptyList());
}
public HelpCategory(String title, List<Help> items) {
this.title = title;
this.items = items;
}
public String getTitle() {
return title;
}
public List<Help> getItems() {
return items;
}
}
private static class Help {
@SerializedName("title")
private final String title;
@SerializedName("subtitle")
private final String subtitle;
@SerializedName("url")
private final String url;
public Help() {
this("", "", "");
}
public Help(String title, String subtitle, String url) {
this.title = title;
this.subtitle = subtitle;
this.url = url;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public String getUrl() {
return url;
}
}
}
@@ -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/>.
*/
package org.jackhuang.hmcl.ui.main;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
private final TabHeader.Tab<SettingsPage> settingsTab = new TabHeader.Tab<>("settingsPage");
private final TabHeader.Tab<PersonalizationPage> personalizationTab = new TabHeader.Tab<>("personalizationPage");
private final TabHeader.Tab<DownloadSettingsPage> downloadTab = new TabHeader.Tab<>("downloadSettingsPage");
private final TabHeader.Tab<HelpPage> helpTab = new TabHeader.Tab<>("helpPage");
private final TabHeader.Tab<AboutPage> aboutTab = new TabHeader.Tab<>("aboutPage");
private final TabHeader.Tab<FeedbackPage> feedbackTab = new TabHeader.Tab<>("feedbackPage");
private final TabHeader.Tab<SponsorPage> sponsorTab = new TabHeader.Tab<>("sponsorPage");
private final TransitionPane transitionPane = new TransitionPane();
public LauncherSettingsPage() {
gameTab.setNodeSupplier(VersionSettingsPage::new);
settingsTab.setNodeSupplier(SettingsPage::new);
personalizationTab.setNodeSupplier(PersonalizationPage::new);
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
helpTab.setNodeSupplier(HelpPage::new);
feedbackTab.setNodeSupplier(FeedbackPage::new);
sponsorTab.setNodeSupplier(SponsorPage::new);
aboutTab.setNodeSupplier(AboutPage::new);
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, sponsorTab, aboutTab);
tab.getSelectionModel().select(gameTab);
gameTab.initializeIfNeeded();
gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
newValue.initializeIfNeeded();
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
{
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.type.global.manage"));
settingsItem.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 24, 24)));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
settingsItem.setOnAction(e -> tab.getSelectionModel().select(gameTab));
})
.startCategory(i18n("launcher"))
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.launcher.general"));
settingsItem.setLeftGraphic(wrap(SVG.applicationOutline(Theme.blackFillBinding(), 24, 24)));
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab));
settingsItem.setOnAction(e -> tab.getSelectionModel().select(settingsTab));
})
.addNavigationDrawerItem(personalizationItem -> {
personalizationItem.setTitle(i18n("settings.launcher.appearance"));
personalizationItem.setLeftGraphic(wrap(SVG.styleOutline(Theme.blackFillBinding(), 24, 24)));
personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab));
personalizationItem.setOnAction(e -> tab.getSelectionModel().select(personalizationTab));
})
.addNavigationDrawerItem(downloadItem -> {
downloadItem.setTitle(i18n("download"));
downloadItem.setLeftGraphic(wrap(SVG.downloadOutline(Theme.blackFillBinding(), 24, 24)));
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
downloadItem.setOnAction(e -> tab.getSelectionModel().select(downloadTab));
})
.startCategory(i18n("help"))
.addNavigationDrawerItem(helpItem -> {
helpItem.setTitle(i18n("help"));
helpItem.setLeftGraphic(wrap(SVG.helpCircleOutline(Theme.blackFillBinding(), 24, 24)));
helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab));
helpItem.setOnAction(e -> tab.getSelectionModel().select(helpTab));
})
.addNavigationDrawerItem(feedbackItem -> {
feedbackItem.setTitle(i18n("feedback"));
feedbackItem.setLeftGraphic(wrap(SVG.messageAlertOutline(Theme.blackFillBinding(), 24, 24)));
feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab));
feedbackItem.setOnAction(e -> tab.getSelectionModel().select(feedbackTab));
})
.addNavigationDrawerItem(sponsorItem -> {
sponsorItem.setTitle(i18n("sponsor"));
sponsorItem.setLeftGraphic(wrap(SVG.handHearOutline(Theme.blackFillBinding(), 24, 24)));
sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab));
sponsorItem.setOnAction(e -> tab.getSelectionModel().select(sponsorTab));
})
.addNavigationDrawerItem(aboutItem -> {
aboutItem.setTitle(i18n("about"));
aboutItem.setLeftGraphic(wrap(SVG.informationOutline(Theme.blackFillBinding(), 24, 24)));
aboutItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(aboutTab));
aboutItem.setOnAction(e -> tab.getSelectionModel().select(aboutTab));
});
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
}
setCenter(transitionPane);
}
public void showGameSettings(Profile profile) {
gameTab.getNode().loadVersion(profile, null);
tab.getSelectionModel().select(gameTab);
}
@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
@@ -30,6 +30,8 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -59,7 +61,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle("HMCL " + Metadata.VERSION));
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final PopupMenu menu = new PopupMenu();
private final JFXPopup popup = new JFXPopup(menu);
@@ -75,6 +77,19 @@ public final class MainPage extends StackPane implements DecoratorPage {
private JFXButton menuButton;
{
HBox titleNode = new HBox(8);
titleNode.setPadding(new Insets(0, 0, 0, 2));
titleNode.setAlignment(Pos.CENTER_LEFT);
ImageView titleIcon = new ImageView();
titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false));
Label titleLabel = new Label(Metadata.FULL_TITLE);
titleLabel.getStyleClass().add("jfx-decorator-title");
titleNode.getChildren().setAll(titleIcon, titleLabel);
state.setValue(State.fromTitleNode(titleNode));
setPadding(new Insets(20));
updatePane = new StackPane();
Loaded 100 of 319 files, more files were not shown because too many files have changed in this diff. Show more