Compare commits

...
444 Commits
Author SHA1 Message Date
huanghongxun 61597cb83d feat: EXE launcher no longer reject Java 11 and later. 2021-05-24 21:39:16 +08:00
huanghongxun 2f17552b2c feat: ask user to download required jvm. 2021-05-24 20:57:07 +08:00
huanghongxun f2d3d24338 feat: load downloaded java installations. 2021-05-24 20:46:31 +08:00
huanghongxun e8a64f52f0 fix: downloading java runtime from mojang 2021-05-23 14:24:53 +08:00
huanghongxun f55fe0fc45 fix: Compilation Error 2021-05-19 00:45:21 +08:00
Glavo 1e3a5f9515 fix: --illegal-access will be removed in Java 17 2021-05-18 15:42:46 +08:00
huanghongxun a9c7eb0954 feat: download specified jre from mojang 2021-05-18 00:53:26 +08:00
huanghongxun 289472d8ba feat: more accurate system architecture normalization 2021-05-18 00:53:09 +08:00
Glavo f191e80100 Fix #874: Remove the dependency on javafx.swing 2021-05-17 12:02:31 +08:00
Glavo 607248e8b3 Fix #873: On 64 bit x86 machines, os.arch may be x86_64 2021-05-17 12:01:36 +08:00
Glavo cecabe5bc4 Fix #873: On 64 bit x86 machines, os.arch may be x64 or x86-64 2021-05-17 12:01:36 +08:00
Glavo 947a2e3d91 fix typo 2021-05-17 01:10:05 +08:00
Glavo 382e0f3b1d fix #871: update lang.h 2021-05-17 01:10:05 +08:00
Glavo 6e440783dc fix #871: update i18n properties 2021-05-17 01:10:05 +08:00
Glavo fcd7d8ba83 fix: Build failure at ClassicVersion::ClassicVersion() 2021-05-17 01:10:05 +08:00
Glavo a53d2a1370 fix: IllegalAccessError on Java 16 2021-05-16 22:31:32 +08:00
yuhuihuang 20148d0b02 fix: G1 not proposed in Java7? 2021-05-13 16:46:23 +08:00
huanghongxun 3114e33067 feat: Version.javaVersion 2021-05-13 13:04:12 +08:00
Haowei Wen e137d72b76 feat: use Gradle 7.0 2021-04-29 15:19:27 +08:00
Haowei Wen cc087dee81 fix: not compatible with Gradle 7.0
* use api & implementation scope, instead of compile
* add shadow plugin back
  * related: https://github.com/huanghongxun/HMCL/pull/863
  * the duplicate entry bug is fixed
* move dependencies from root project to HMCLCore module
2021-04-29 15:19:27 +08:00
huanghongxun eed475cb28 Add comments to build.gradle 2021-04-26 11:49:05 +08:00
huanghongxun d07c45f0b5 fix: NPE when version.json:mainClass is null. 2021-04-26 11:47:56 +08:00
huanghongxun 0aa0fa1b78 chore: try to figure out why NPE thrown calling builder.start() 2021-04-26 11:47:56 +08:00
ZekerZhayard e91ec13dbd Remove shadowJar. 2021-04-24 20:02:46 +08:00
huanghongxun e36105d937 Reland: create modpack with launcher packed in 2021-04-22 08:40:11 +08:00
ZekerZhayard 07d4df312d Add JavaFX to module path. 2021-04-16 12:18:05 +08:00
ZekerZhayard b521c5b76a Compatible with Java 16. 2021-04-16 12:18:05 +08:00
huanghongxun c1596443f5 fix: crash 2021-04-11 22:50:09 +08:00
huanghongxun cadeb1f59c Fix: macOS app icon 2021-04-11 21:55:09 +08:00
huanghongxun 2d4223f7ed fix: Suppress IOException when caching downloaded library. Closes #834. 2021-04-11 00:18:04 +08:00
huanghongxun c453bbc7cd chore: Change description of yggdrasil and microsoft account login. Closes #839. 2021-04-10 22:12:29 +08:00
huanghongxun 2184961e3c Add: download and patch JavaFX before start. Closes #800. 2021-04-10 17:13:25 +08:00
huanghongxun 8afd8bde8b Fix: failed to parse datapack pack.mcmeta in 6th format. Closes #840. 2021-04-10 17:13:25 +08:00
Haowei Wen 65952a5d81 feat: set ServerResponseMalformedException's message to response content 2021-03-07 13:24:16 +08:00
Haowei Wen ac1f2ec1ed fix: skin upload: error message from server is not displayed 2021-03-07 13:24:16 +08:00
Haowei Wen 454cc1e702 feat: show spinner while refreshing account 2021-03-07 13:24:16 +08:00
Haowei Wen e3fa7428bf feat: automatically detect skin model when uploading 2021-03-07 13:24:16 +08:00
huanghongxun 7ece35e28a fix: move .hmcl directory to $HOME/.cache/hmcl to fulfill XDG standard. Closes #849. 2021-03-07 13:21:57 +08:00
huanghongxun d22dae9834 feat: Add progress gui for downloading JavaFX 2021-03-07 13:15:44 +08:00
寒兮 63bd707741 fix: not compatible with JVM environment without Javafx
Copy from Recaf
2021-03-06 15:13:17 +08:00
Haowei Wen 734c1e14f0 fix: not compatible with ely.by
Ely.by's Yggdrasil response does not include 'user' field.
This commit makes the 'user' field optional.
2021-02-15 16:32:18 +08:00
huanghongxun 46869ad5f7 fix: not update game/addons for server modpack 2021-02-02 12:13:37 +08:00
huanghongxun 13fa713d58 fix: mcbbs modpack completion 2021-01-26 12:25:06 +08:00
huanghongxun 94cd33af5c fix: not validating modpack name at first 2021-01-26 12:23:26 +08:00
huanghongxun 7efd9607e5 fix: modpack export wizard next step button is disabled. 2021-01-26 12:09:09 +08:00
huanghongxun 94539ad77b feat: progress bar indicating Microsoft login page loading progress 2021-01-26 12:07:34 +08:00
huanghongxun f2a86857bc feat: MCBBS modpack 2021-01-16 16:17:54 +08:00
Haowei Wen 3f2be63d08 build: remove license-gradle-plugin 2021-01-09 22:22:06 +08:00
huanghongxun 2d7c1393d6 fix: #813 2020-12-26 21:31:10 +08:00
huanghongxun 3d33230e25 fix: RejectedExecutionException 2020-12-26 21:28:59 +08:00
yuhuihuang dd4683e693 fix: Microsoft Account login 2020-12-24 20:32:47 +08:00
huanghongxun 8bc5d2112f fix: #801 2020-12-21 01:56:46 +08:00
huanghongxun a828896989 fix: executor service 2020-12-19 20:19:46 +08:00
yuhuihuang 5e8734aa95 fix: compilation 2020-12-10 22:09:19 +08:00
Haowei Wen 1ac844f398 do not occupy space when upload skin button is invisible 2020-12-10 21:56:31 +08:00
Haowei Wen 3f85ac208c impl yushijinhun/authlib-injector#90 2020-12-10 21:56:31 +08:00
yuhuihuang c191186023 feat: Microsoft Account authentication 2020-12-10 21:47:58 +08:00
yuhuihuang 4715a95a54 feat: initial multithreaded downloading 2020-12-10 21:47:58 +08:00
huanghongxun 8108f11c45 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2020-09-04 00:49:03 +08:00
huanghongxun f4f1850271 fix: try to fix blocking game running when launcher visibility was set to close after game started. 2020-09-04 00:38:38 +08:00
Haowei Wen 5a81972f68 impl https://github.com/yushijinhun/authlib-injector/issues/85 2020-08-31 19:16:22 +08:00
huanghongxun 674dfca34d fix: #784 2020-08-27 11:20:07 +08:00
yuhuihuang cafcb74e22 fix: #774 2020-08-16 14:52:17 +08:00
yushijinhun 88c595dab2 Remove 'ForbiddenOperationException' from auth error message 2020-08-15 17:20:51 +08:00
yushijinhun 48e2fcc601 Set Locale.default to selected language
e6d11fe9a3 is broken because
Locale.default is not set by HMCL at all. This commit fixes the bug.

Note that now if we want to obtain the system language, we must use
Locales.DEFAULT rather than Locale.getDefault().
2020-08-15 17:20:51 +08:00
yushijinhun 2f10948239 implement yushijinhun/authlib-injector#78 2020-08-14 17:26:05 +08:00
xtex 7f674f237b fix: #769 2020-08-11 21:38:03 +08:00
BBleae c3f19d2c4a perf: cache Java version from executable. 2020-08-11 15:58:46 +08:00
BBleae 94b2cafa7a fix: override options.txt 2020-08-10 18:05:06 +08:00
yuhuihuang 1d66ebef58 add: #757. 2020-08-09 19:44:48 +08:00
yuhuihuang 66459a124c add: readme for bmclapi overrides 2020-08-09 19:28:15 +08:00
yuhuihuang 5bbb565bd4 add: #762 2020-08-09 19:24:49 +08:00
yuhuihuang 58819b6361 fix: test 2020-08-09 19:20:52 +08:00
yuhuihuang 2de6a45ca1 fix: #759 2020-08-09 19:16:09 +08:00
yuhuihuang c60261d917 fix: #755 2020-08-09 18:29:01 +08:00
yushijinhun 94b6d7596e Redesign authlib-injector updater
The process of searching for authlib-injector.jar has been changed to:
1. use ~/.hmcl/authlib-injector.jar if present
2. use ./authlib-injector.jar if present
3. download if none of above is present

Instead of checking for updates when logging in, we now do it at startup.
We only check for updates if there is at least one authlib-injector
server configured.

This changed is expected to imporve launching speed.
2020-08-09 17:59:36 +08:00
yushijinhun ae63b9712e Revert "add: accept authlib-injector artifact in current working directory"
This reverts commit 02cb2a3fb1.
2020-08-09 17:59:36 +08:00
yuhuihuang 00b945f6e3 fix: #746 2020-08-01 15:08:12 +08:00
yuhuihuang 37c47fc2dc fix: #751 2020-08-01 14:48:39 +08:00
huanghongxun 51325d2423 chore: recompile JFoenix.jar 2020-07-20 22:34:28 +08:00
Yuhui Huang 469a7df4d4 Merge pull request #750 from Glavo/javafx
Fix #717
2020-07-20 22:32:39 +08:00
Glavo ec56e24b7e fix #717 2020-07-20 16:17:31 +08:00
yuhuihuang 00c350cd82 fix: #737 2020-07-19 16:36:24 +08:00
yuhuihuang 55d78aeba7 fix: #742 2020-07-19 15:41:11 +08:00
yuhuihuang 85c01cde00 fix: #747 2020-07-19 15:30:47 +08:00
yuhuihuang 4ce0a83845 feat: display new version of updated modpack. Closes #748. 2020-07-19 15:19:53 +08:00
yuhuihuang d6859d1956 fix: not stop waiting for MC 1.0 launch 2020-07-19 14:21:11 +08:00
yuhuihuang 61447f0964 fix: #741 2020-07-19 14:20:42 +08:00
Yuhui Huang 22a02faced Merge pull request #745 from jihuayu/javafx
fix #744
2020-07-14 23:33:33 +08:00
jihuayu a60c288335 fix:fix #744 (2) 2020-07-14 23:31:25 +08:00
jihuayu 3c224198e7 fix:fix #744 2020-07-14 23:13:33 +08:00
huanghongxun dc03f28d56 refactor: WizardDisplayer 2020-06-26 21:29:47 +08:00
huanghongxun bd18355a1a fix: NPE 2020-06-26 21:25:26 +08:00
huanghongxun 1208d6dabc feat: decorator shadow 2020-06-21 18:18:20 +08:00
huanghongxun 63082f5a5e fix: #731 2020-06-21 17:13:23 +08:00
Yuhui Huang c24ef2a200 Merge pull request #730 from haarlemmer/haarlemmer-patch-1
Modify some Chinese (Simplified) translations.
2020-06-20 00:22:52 +08:00
haarlemmer 891b64ee0f Modify some Chinese (Simplified) translations. 2020-06-19 19:20:32 +08:00
huanghongxun 82763f3c32 feat: font family override 2020-06-16 14:35:21 +08:00
huanghongxun 3119b4d88d fix: #728 2020-06-16 14:30:11 +08:00
huanghongxun 6532dc72db fix: proxy settings not applied to game 2020-06-16 13:24:43 +08:00
huanghongxun fd24fcc0f2 fix: map_to_resources in asset_index.json 2020-06-16 13:12:41 +08:00
huanghongxun 0d3e204f0d fix: #724 2020-06-14 20:06:19 +08:00
huanghongxun 64d9fa7c6b fix: #729 2020-06-14 19:51:46 +08:00
huanghongxun 53ff657106 fix: #722 2020-06-01 16:58:06 +08:00
huanghongxun 6eb0c16fcc fix: retry url in LibraryDownloadTask 2020-06-01 13:26:28 +08:00
huanghongxun 0984c23253 alt: skin icon 2020-06-01 13:05:55 +08:00
huanghongxun 5805914ebb fix: upload skin 2020-06-01 11:59:21 +08:00
huanghongxun bfb55a3813 add: upload skin 2020-05-31 23:00:44 +08:00
huanghongxun 476c41b29b fix: sometimes cannot edit modpack versiojn settings 2020-05-31 23:00:44 +08:00
huanghongxun a2ef477ae0 add: forbids installation buttons for incompatible libraries. Closes #720. 2020-05-31 22:24:03 +08:00
Yuhui Huang 42d42e9750 fix: #619 2020-05-26 17:19:55 +08:00
Yuhui Huang 182e8ba2e8 Merge pull request #715 from Asnxthaony/patch-1
Send Accept-Language header
2020-05-24 14:58:20 +08:00
Asnxthaony e6d11fe9a3 feat: send accept language header 2020-05-24 14:54:07 +08:00
huanghongxun 7c510f9fb8 add: allow installing OptiFine 1.14.4 and Forge in same game version 2020-05-03 19:21:54 +08:00
huanghongxun b53a3ebcc2 alt: add logs to figure out the reason why app crashes. 2020-04-29 22:58:25 +08:00
huanghongxun 8e9f706d37 fix: VersionNotFoundException 2020-04-29 21:56:05 +08:00
huanghongxun 8997895a82 fix: IllegalArgumentException 2020-04-29 21:48:21 +08:00
huanghongxun 94db91b7c1 fix: f**king ZipFileSystem 2020-04-29 21:44:51 +08:00
huanghongxun 4f70465395 fix: crash when JavaSoft registries are malformed. 2020-04-29 21:41:58 +08:00
huanghongxun b2693cd3f9 fix: show crashreport instead of a warning of corruption 2020-04-29 20:25:35 +08:00
huanghongxun 3ae1c3467b fix: IllegalArgumentException 2020-04-29 20:18:43 +08:00
huanghongxun 1bba1b427c fix: support downloading files from urls whose protocols are not http. 2020-04-29 20:17:13 +08:00
huanghongxun 63e2fdf56d alt: show authlib injector server name in main screen. 2020-04-25 19:55:42 +08:00
huanghongxun cf1b91ec55 alt: change selected version in VersionPage 2020-04-25 19:45:28 +08:00
huanghongxun 4bac9380d1 fix: game crash when Forge, OptiFine are both installed. 2020-04-25 16:46:53 +08:00
huanghongxun fec5c9a5ce fix: AccessDeniedException when updating server modpack 2020-04-25 01:14:31 +08:00
huanghongxun 810ea9ace9 add: remember window size 2020-04-24 23:12:58 +08:00
huanghongxun b933938f1b alt: show better prompt in game launching. 2020-04-24 14:13:46 +08:00
huanghongxun c408cccd35 alt: buttons in game list now also available in version page 2020-04-24 14:13:24 +08:00
huanghongxun bc842437fa fix: breaking versions that inherit from a version being renamed 2020-04-23 12:28:11 +08:00
huanghongxun 9bda50f66e alt: color of scrollbar in popup menu 2020-04-23 12:10:25 +08:00
huanghongxun 886ebe6885 alt: alignment 2020-04-23 11:26:40 +08:00
huanghongxun d574d8d78b fix: InvalidPathException when custom run directory is not valid 2020-04-23 11:13:03 +08:00
huanghongxun a3b74e0c75 fix: RejectedExecutionException 2020-04-23 09:50:43 +08:00
huanghongxun eb9d123176 fix: IllegalArgumentException 2020-04-23 09:50:22 +08:00
huanghongxun 383de6cb2e fix: crash when renaming game version 2020-04-23 08:12:19 +08:00
huanghongxun 43de254a98 fix: ConcurrentModificationException 2020-04-23 08:02:55 +08:00
huanghongxun 02cb2a3fb1 add: accept authlib-injector artifact in current working directory 2020-04-22 23:52:51 +08:00
huanghongxun 62b6c07401 fix: npe 2020-04-22 23:52:25 +08:00
huanghongxun e630c6dff7 fix: downloading libraries of optifine 2020-04-18 18:57:01 +08:00
huanghongxun 0639d82914 add: launch button in gamelist 2020-04-18 10:45:39 +08:00
huanghongxun 2701a84b94 fix: jfxdialog 2020-04-17 23:46:20 +08:00
huanghongxun 859da37d71 fix: not generating modpack.cfg when CurseCompletionTask fail 2020-04-17 23:46:08 +08:00
huanghongxun 5893eae87f fix: cannot rename game version sometimes 2020-04-17 23:45:15 +08:00
huanghongxun 99f42db370 fix: fail to update 2020-04-17 14:17:10 +08:00
huanghongxun 32e9076198 fix: not compile 2020-04-17 00:49:22 +08:00
Yuhui Huang e1cacad6b8 Merge pull request #700 from vanja-san/patch-2
Update I18N_ru.properties
2020-04-17 00:40:36 +08:00
huanghongxun 02ce950020 alt: allow dragging window when a dialog is shown 2020-04-17 00:16:17 +08:00
huanghongxun 1eeba344f7 fix: game is blocked for launching 2020-04-17 00:15:34 +08:00
huanghongxun f84c4310f9 alt: do not retry when server respond with 404 2020-04-15 13:46:44 +08:00
huanghongxun 97e8ff91ea alt: print the reason why renaming version failed 2020-04-15 13:31:58 +08:00
huanghongxun 1c865e3d8d alt: make scrollbar in logwindow more obvous 2020-04-15 13:30:17 +08:00
huanghongxun 6ca7a86189 fix: not checking corruption of forge installer 2020-04-15 13:14:16 +08:00
huanghongxun 3c716ea170 alt: ask user whether continue launching or not if error 2020-04-15 00:20:22 +08:00
huanghongxun 3a7800db61 alt: align titlebar buttons. Mentioned in #701 2020-04-15 00:12:07 +08:00
The_BadUser b04430528e Update I18N_ru.properties 2020-04-14 20:23:53 +07:00
huanghongxun 4382326dec fix: not closing JarFile 2020-04-14 15:07:21 +08:00
huanghongxun 5adbac3cc9 fix: #699 2020-04-14 15:02:52 +08:00
huanghongxun d8b84a2574 fix: clear in LogWindow broken 2020-04-12 17:53:13 +08:00
huanghongxun 0d59e52ff9 fix: prelaunch command not tokenized 2020-04-12 16:52:06 +08:00
huanghongxun feb30a7349 fix: wrongly considering Forge 1.12.2 not compatible with OptiFine 2020-04-12 14:16:09 +08:00
huanghongxun 3082e4cd16 fix: not downloading assets 2020-04-12 14:15:27 +08:00
huanghongxun c28dfd0ae0 add: support change download provider in install wizard 2020-04-12 00:41:12 +08:00
huanghongxun 986a430e92 refactor: ensure that VersionLists fetch original download URL 2020-04-12 00:41:12 +08:00
huanghongxun 80f27f5ea4 alt: navigate to game version list when no versions exist 2020-04-12 00:41:03 +08:00
huanghongxun 4c44a3c410 alt: change description of uninstalled libraries 2020-04-12 00:40:53 +08:00
huanghongxun 14cfd6abeb add: support variables in prelaunch command which is supported by MultiMC. Closes #698 2020-04-08 21:18:25 +08:00
huanghongxun b957ca1931 fix: cannot download library when .pack.xz responded not 404 2020-04-08 21:17:21 +08:00
huanghongxun 9372d818a5 alt: no smooth scrolling any longer 2020-04-07 16:32:29 +08:00
huanghongxun aee9c9bb32 alt: dialog covers title bar now 2020-04-07 16:32:29 +08:00
huanghongxun 5ed42d1654 alt: remove background of some pages 2020-04-07 16:32:29 +08:00
huanghongxun dd94d2254f alt: adjust window border 2020-04-07 16:32:29 +08:00
huanghongxun bf4c831047 fix: forge 2852 2020-04-07 16:32:29 +08:00
Yuhui Huang 33df069803 Merge pull request #694 from Rowisi/Rowisi-patch-1
add up-to-date download link
2020-04-06 17:43:02 +08:00
huanghongxun c360a4abb0 alt: transparent title bar 2020-04-06 17:33:57 +08:00
huanghongxun d29a198f7f alt: change UI black to #292929 2020-04-06 17:31:47 +08:00
huanghongxun 8df651c5a8 add: scroll to game crash report automatically 2020-04-06 13:21:17 +08:00
Rowisi 191b88ab2a add up-to-date download link 2020-04-05 02:15:13 +01:00
huanghongxun d00b269786 fix: not shutting down executor service after game launched. 2020-03-31 21:54:00 +08:00
huanghongxun cbc85dd99e add: export game logs, use ListView instead of WebView in LogWindow, 2020-03-30 21:47:23 +08:00
huanghongxun cf25e6c27b fix: invisible scroll bar in webview 2020-03-30 16:30:10 +08:00
huanghongxun 76807e1cd2 alt: more friendly corrupt file error prompt. Closes #692 2020-03-30 16:19:54 +08:00
huanghongxun d84877dfcc refactor: Move EnumGameDirectory into HMCLCore 2020-03-30 16:16:41 +08:00
huanghongxun f38d3b0026 alt: some UI details 2020-03-30 16:15:27 +08:00
huanghongxun 511b555255 fix: #690 2020-03-26 18:54:26 +08:00
huanghongxun e79206bcb6 alt: no longer show default JVM args 2020-03-26 18:41:37 +08:00
huanghongxun d8fa97e8c7 alt: update Gradle 2020-03-26 18:41:17 +08:00
huanghongxun be2b55cc67 fix: change selected version after altering installed libraries of one version 2020-03-22 13:22:11 +08:00
huanghongxun e68a3c7cd4 add: retry other download source in auto installation 2020-03-22 13:19:43 +08:00
huanghongxun b3bf9d5c94 alt: ensure text aligned 2020-03-22 13:19:16 +08:00
huanghongxun e166651fe7 fix: #688 2020-03-22 13:12:39 +08:00
huanghongxun 516c73ee15 alt: smooth scrolling in VersionPage 2020-03-22 10:54:09 +08:00
huanghongxun ac532c9400 alt: mod list now sorted by filename ignoring case 2020-03-20 21:08:28 +08:00
huanghongxun b2f6ef72c3 add: copy instance. Closes #687 2020-03-19 13:12:51 +08:00
huanghongxun 3e2bb9678d Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2020-03-16 23:23:13 +08:00
huanghongxun 0074f12a14 fix: severe log 2020-03-16 23:20:05 +08:00
huanghongxun 6a982b2a5c add: allow removing libraries in vanilla installation 2020-03-16 22:49:15 +08:00
huanghongxun bde76707f3 alt: actions in game list now also in version page 2020-03-16 22:48:42 +08:00
huanghongxun 2bc7bdba8a fix: lets encrypt, no longer require 8u101 2020-03-16 22:44:58 +08:00
Yuhui Huang 2e4f6ce076 Merge pull request #684 from vanja-san/patch-1
Update I18N_ru.properties
2020-03-12 12:08:10 +08:00
The_BadUser 5d3a382d26 Update I18N_ru.properties
Full update RU translation
2020-03-12 09:08:15 +07:00
huanghongxun ee8be3166f fix: changing game version without alerting incompatibility 2020-03-11 00:27:29 +08:00
huanghongxun 8fd500e931 fix: forbids OptiFine and Fabric coexistence 2020-03-11 00:18:29 +08:00
huanghongxun 2b9f070279 fix: #683 2020-03-10 17:50:25 +08:00
huanghongxun a53603dbbf fix: #582 2020-03-09 12:07:09 +08:00
huanghongxun 4332bc124e fix: unable to launch 2020-03-08 20:49:00 +08:00
huanghongxun 20d0a2ae62 fix: fail to launch 2020-03-08 11:14:11 +08:00
huanghongxun 646bfa5be7 alt: reduce call count of VersionPage.loadVersion 2020-03-06 14:12:03 +08:00
huanghongxun 16d92454f4 add: support resizing window 2020-03-06 12:58:40 +08:00
huanghongxun 129ce9a545 fix: buggy game version switching 2020-03-06 12:35:33 +08:00
huanghongxun 7b6a96b4b6 fix: compatibility issue with other launchers using auto-installation 2020-03-06 12:23:34 +08:00
huanghongxun 7260ae9b5e alt: no longer show failure installation of curse modpack 2020-03-03 12:47:10 +08:00
huanghongxun d333f54430 alt: installer list page look 2020-03-03 12:42:54 +08:00
huanghongxun 3efec78732 add: show asset and mod download progress 2020-03-03 00:59:15 +08:00
huanghongxun 4f51ab00a1 alt: make modpack selection page more readable 2020-03-02 20:38:43 +08:00
huanghongxun 6857200d75 alt: version page look 2020-03-02 20:07:29 +08:00
huanghongxun 1c3b3bf050 alt: root page tab 2020-03-02 18:06:53 +08:00
huanghongxun e11a5389c6 alt: relocate launch button 2020-03-02 16:23:15 +08:00
huanghongxun 120238b234 Merge branch 'gui' into javafx 2020-03-02 16:21:22 +08:00
huanghongxun c093c6e23d alt: root page look 2020-03-02 16:21:16 +08:00
huanghongxun 11d47bf9b1 alt: version page look 2020-03-02 14:27:14 +08:00
huanghongxun fa13b1fc0d add: animation at title bar 2020-03-02 00:31:41 +08:00
huanghongxun 334e93196b add: change look of scroll bar 2020-03-01 20:49:26 +08:00
huanghongxun de6cdeab52 add: switch game by scrolling on the launch button 2020-03-01 16:01:11 +08:00
huanghongxun 24a6c2d3ba fix: wrong forge installer failure prompt 2020-02-28 16:09:09 +08:00
huanghongxun 58cdc68b6e fix: wrong failure URL 2020-02-28 16:06:02 +08:00
huanghongxun 7389e210a7 alt: change default download provider 2020-02-25 13:15:43 +08:00
huanghongxun b117769711 alt: initial UI refactor 2020-02-25 09:36:18 +08:00
huanghongxun 1ca4045a2f alt: rename natives directory 2020-02-22 23:13:52 +08:00
huanghongxun fb4a499fe9 fix: #678 2020-02-22 23:13:09 +08:00
huanghongxun 4aac45f3b4 fix: not stop waiting for game when terminated 2020-02-22 23:12:45 +08:00
huanghongxun 506786e932 refactor: Decorator 2020-02-22 23:12:27 +08:00
huanghongxun 1726cc7ec7 alt: some UI details 2020-02-21 12:35:40 +08:00
huanghongxun 5b1c72ad5c alt: some UI details 2020-02-21 11:02:13 +08:00
huanghongxun 0fa324452f fix: cannot change init window dimension 2020-02-21 00:23:21 +08:00
huanghongxun 6ad5ba3736 alt: change UI of game installation wizard. Also UI performance improved. 2020-02-21 00:10:16 +08:00
huanghongxun 5707e1d57e fix: not showing friendly prompt of GameAssetIndexMalformedException 2020-02-21 00:09:02 +08:00
huanghongxun f3424e0796 alt: title style 2020-02-21 00:08:07 +08:00
huanghongxun 0b0118446f fix: download empty file from MCBBS 2020-02-20 14:35:10 +08:00
huanghongxun 5c19dcd208 fix: game crashes when asset index file is empty 2020-02-20 14:22:55 +08:00
huanghongxun 0a68e52fca fix: tasks with default name will not be displayed 2020-02-20 14:06:03 +08:00
huanghongxun 5c25ab3eda add: stages of modpack installation 2020-02-20 14:03:11 +08:00
huanghongxun 91682aeafa add: redownload corrupt files when game crashed 2020-02-20 00:02:56 +08:00
huanghongxun cd9180c69a fix: removing modpack with incomplete installation 2020-02-19 22:50:48 +08:00
huanghongxun 3269841207 alt: merge dependency resolving and modpack launching stage 2020-02-19 21:51:06 +08:00
huanghongxun b4ec71d65f alt: redownload asset index if it is empty 2020-02-19 21:49:58 +08:00
huanghongxun 1a0be11ae3 fix: incorrect download speed 2020-02-19 21:49:27 +08:00
huanghongxun b72d42a09a fix: task accidental disappearance 2020-02-19 21:48:59 +08:00
huanghongxun ed5e225cdc fix: incorrect progess list node size 2020-02-19 21:47:49 +08:00
huanghongxun c26579bebd add: show stages of game installation 2020-02-19 16:51:05 +08:00
huanghongxun 1ea9170b2e add: more brief launching steps 2020-02-19 13:34:40 +08:00
huanghongxun a144a5eee0 alt: update BMCLAPI sponsor info 2020-02-19 00:40:37 +08:00
huanghongxun 1b1f532988 add: retry other download providers for libraries 2020-02-19 00:16:12 +08:00
huanghongxun ca577475fc add: retry other download providers when one failed. 2020-02-19 00:10:39 +08:00
huanghongxun bc4b41e8cd add: clear button for cache directory 2020-02-17 00:46:11 +08:00
huanghongxun a1226012ff add: increase download concurrency. According to https://www.bilibili.com/video/av89055581. 2020-02-17 00:20:17 +08:00
huanghongxun cfd51915d3 fix: potential misunderstanding of BMCLAPI sponsor 2020-02-16 23:22:10 +08:00
huanghongxun 030d54bc5c fix: not on FX thread. Mentioned in #673. 2020-02-16 22:54:59 +08:00
huanghongxun 97a5d7e71a add: confirmation of removing mods or datapacks. Closes #670. 2020-02-16 12:21:01 +08:00
huanghongxun 8b16e8be4d fix: incorrect log in GetTask 2020-02-13 14:42:42 +08:00
huanghongxun 89dbc1be4a add: more friendly http code prompt 2020-02-13 14:42:42 +08:00
huanghongxun 25bc8f5cb3 add: tooltip and switching account by scrolling on account item 2020-02-13 14:42:42 +08:00
huanghongxun 37d62b665e fix: fabric support from BMCLAPI and mcbbs 2020-02-13 14:34:48 +08:00
huanghongxun aa2838d219 add: print game command for debugging 2020-02-13 12:31:14 +08:00
huanghongxun 979e58edf1 alt: not removing game if curse mod cannot be downloaded 2020-02-13 00:11:45 +08:00
Yuhui Huang 0f72d98067 Merge pull request #668 from pan93412/patch-1
i18n: zh: fixup
2020-02-09 15:02:08 +08:00
pan93412 eea401f99d i18n: zh: fixup 2020-02-09 14:48:43 +08:00
huanghongxun b1301dcdec add: BMCLAPI support for fabric download 2020-02-09 00:55:51 +08:00
huanghongxun bdb6784a9b fix: broken error handling 2020-02-06 12:47:25 +08:00
huanghongxun 0d906e0adf fix: #664 2020-02-06 12:46:38 +08:00
huanghongxun 4923370897 fix: #661 2020-02-05 15:25:28 +08:00
huanghongxun bdee33456b fix: #662 2020-02-05 14:36:44 +08:00
huanghongxun 5a8446524a update copyright 2020-02-05 13:13:10 +08:00
Yuhui Huang 7c75e3d4a0 Merge pull request #659 from pan93412/patch-1
i18n: zh: update chinese (traditional) translation
2020-02-05 12:40:49 +08:00
Yuhui Huang e67a1d16f5 Merge branch 'javafx' into patch-1 2020-02-05 12:40:33 +08:00
huanghongxun b864ea89d7 add: restore old sync task executor 2020-02-05 11:58:50 +08:00
huanghongxun 04aa257b48 refactor: extract superclass of TaskExecutor 2020-02-05 11:58:28 +08:00
huanghongxun 1b466eb33f fix: recognizing CancellationException as crash 2020-02-04 22:49:49 +08:00
huanghongxun a162828f0a fix: recongnizing vanilla as patched 2020-02-04 22:49:23 +08:00
huanghongxun b154871207 add: friendly prompt for 404 2020-02-04 22:45:54 +08:00
huanghongxun c7e3639155 fix: download missing library more than one time 2020-02-04 22:38:59 +08:00
huanghongxun 9c1905bb51 no longer download file from BMCLAPI when the official download provider is activated 2020-02-04 15:26:37 +08:00
huanghongxun 31e39900b6 fix: unable to launch when asset index not downloaded 2020-02-04 15:20:07 +08:00
huanghongxun 2b604bfd93 add: friendly prompt for corrupt authlib-injector 2020-02-04 15:19:31 +08:00
pan93412 29f138ab94 i18n: zh: fix: 升級程序 2020-02-03 16:35:54 +08:00
pan93412 8325021f9d i18n: zh: update chinese (traditional) translation 2020-02-03 16:33:41 +08:00
huanghongxun 25e4fa3834 add: friendly prompt of corrupt forge installer 2020-02-03 12:09:06 +08:00
huanghongxun 9849b5659b fix: #654 2020-02-03 00:33:59 +08:00
huanghongxun 43428c054a add: auto fix malformed asset index file 2020-02-02 23:35:47 +08:00
huanghongxun 1d579c52eb fix: unable to cancel tasks 2020-02-02 23:34:42 +08:00
huanghongxun 214ff9de94 add: MCBBS download provider 2020-02-02 23:32:01 +08:00
huanghongxun bb2e74009e fix: #596 2020-01-14 11:51:01 +08:00
huanghongxun 53366ee284 fix: mess quote escaping in Windows 2020-01-14 11:50:16 +08:00
huanghongxun 8ec9a7a218 add: now BMCLAPI is sponsored by MCBBS 2020-01-14 11:05:47 +08:00
huanghongxun f3434fe2a8 Fix #644 2020-01-14 10:58:15 +08:00
huanghongxun acda1952ca Fix #641 2020-01-14 10:17:46 +08:00
huanghongxun 084b15e113 fix: delete installed curse when some mods failed 2020-01-14 10:11:30 +08:00
huanghongxun d6d8c60e5b fix: encoding of lang 2020-01-14 01:43:35 +08:00
huanghongxun d3f24f0147 fix: encoding of lang 2020-01-14 01:35:54 +08:00
huanghongxun 30499aa623 fix: unable to launch vivecraft 2020-01-14 01:20:59 +08:00
huanghongxun 20d5957331 Update license headers of source files. Closes #640 2020-01-07 00:11:53 +08:00
huanghongxun d859eeeb19 fix: not stop waiting for launching game 1.15 2019-12-18 17:27:28 +08:00
Yuhui Huang 076733e6ce Update gradle.yml 2019-12-17 20:09:42 +08:00
Yuhui Huang 8fab4349bf Create gradle.yml 2019-12-17 20:03:31 +08:00
huanghongxun 829d92c8bb Exclude logs and CustomSkinLoader cache 2019-12-14 12:51:16 +08:00
huanghongxun 71131dadb7 Fix #632 2019-12-14 12:50:22 +08:00
huanghongxun 61abed04f3 Fix crash 2019-12-14 12:14:08 +08:00
huanghongxun 6e79d287f3 Revert "Fix #632"
This reverts commit 30f86571
2019-12-14 09:20:01 +08:00
huanghongxun 30f8657174 Fix #632 2019-12-14 00:32:14 +08:00
huanghongxun f516854226 Disable JVM check when the option is enabled 2019-11-27 10:34:08 +08:00
huanghongxun 0cee859e15 always download missing files for server modpack 2019-11-22 10:57:40 +08:00
huanghongxun 7b987c9b95 Fix #628 2019-11-22 10:49:00 +08:00
huanghongxun 8cdd8b3f70 Fix not loading authlib-injector server name 2019-11-12 18:10:09 +08:00
huanghongxun 956b4e4a6a Add fileApi option in modpackInfoPage 2019-11-12 17:49:26 +08:00
huanghongxun 4e0ffc8d1e Support Server Auto-Update Task. Closes #624 2019-11-11 17:35:03 +08:00
huanghongxun 177935241f Support download modpack from direct download url 2019-11-11 17:34:59 +08:00
huanghongxun f133fcf66c Fix NPE 2019-11-11 17:34:58 +08:00
huanghongxun 869f57cbf1 avoiding crash when recorded AI server url is malformed 2019-11-11 17:34:58 +08:00
huanghongxun de7c2e4f07 Fix typo 2019-11-11 17:34:58 +08:00
Yuhui Huang 8bc2650f7b Merge pull request #625 from poopingpenis/javafx
Updated Spanish translation
2019-11-08 10:38:00 +08:00
Yuhui Huang cef9f0b2bb Merge pull request #622 from Lenni0451/javafx
Added autoscroll and fixed typo
2019-11-08 10:36:57 +08:00
poopingpenis e36b28c957 Add files via upload 2019-11-01 14:40:35 -04:00
poopingpenis a770a7ee5c Add files via upload 2019-11-01 14:38:53 -04:00
poopingpenis e3e9a19f6f Add files via upload 2019-11-01 14:37:07 -04:00
poopingpenis 4772a144ae Add files via upload 2019-10-31 20:27:36 -04:00
poopingpenis d32ba75d5f Add files via upload 2019-10-31 09:03:36 -04:00
poopingpenis 96f869278b Add files via upload 2019-10-31 09:00:23 -04:00
poopingpenis df1f9c76ad Add files via upload 2019-10-31 08:58:22 -04:00
gitfamhub 0b934bf6b5 Merge branch 'javafx' into javafx 2019-10-30 22:35:55 -04:00
gitfamhub f5319fa94c Add files via upload 2019-10-30 22:32:08 -04:00
gitfamhub 28229471f4 Add files via upload 2019-10-30 22:28:46 -04:00
gitfamhub d00c61b89f Add files via upload 2019-10-30 22:07:16 -04:00
Lenni0451 1f7d3591b9 Revert "Increased window size by 20 to prevent sideways scrollbars to show up"
This reverts commit 4e537de894.
2019-10-28 20:36:59 +01:00
Lenni0451 4e537de894 Increased window size by 20 to prevent sideways scrollbars to show up 2019-10-26 22:49:26 +02:00
Lenni0451 e130a14ebe Added autoscroll checkbox to log window 2019-10-26 22:05:45 +02:00
Lenni0451 31aa2f3a27 Fixed typo and added autoscroll translation 2019-10-26 22:05:21 +02:00
Yuhui Huang 1f9c94dbc4 Merge pull request #616 from machinesmith42/javafx
Update I18N.properties
2019-10-10 16:56:14 +08:00
huanghongxun a2540c2e9b Fix #619 2019-10-10 16:51:01 +08:00
huanghongxun 4737240c91 Add: forgesvc for Curse Mod Detection fallback 2019-10-10 16:46:32 +08:00
huanghongxun 944d88b8af Fix: keep not completed Curse modpack game version 2019-10-10 16:39:27 +08:00
huanghongxun 529c4ed420 Fix: ConcurrentModificationException 2019-10-10 16:03:25 +08:00
huanghongxun e0ebb02530 Fix: java.nio.file.InvalidPathException 2019-10-10 15:53:47 +08:00
huanghongxun be22d55cec Fix: java.lang.IllegalArgumentException: MALFORMED[1] 2019-10-10 15:24:37 +08:00
huanghongxun 993ce660aa Fix: not recognizing java 12 2019-09-29 18:46:03 +08:00
machinesmith42 79e120dd74 Update I18N.properties 2019-09-27 10:51:55 -05:00
huanghongxun d4155bc14d Update Russian and English translations 2019-09-13 13:32:20 +08:00
huanghongxun ba699c4ed5 Fix NPE 2019-09-12 16:38:28 +08:00
huanghongxun bc96afd0cd Fix #605 2019-09-12 15:51:20 +08:00
huanghongxun cf1a66e858 load authlib-injector server from external file 2019-09-09 17:39:49 +08:00
huanghongxun 5eba896e6d Fix forge installer 404 2019-09-09 16:49:36 +08:00
huanghongxun e69d149c34 Fix HMCL import 2019-09-09 15:32:55 +08:00
huanghongxun 2f9955febd No longer report JsonParseException 2019-09-05 19:06:09 +08:00
huanghongxun df39be708f Check if given game version is malformed 2019-09-05 17:49:59 +08:00
huanghongxun ce3b9e3b1f FIx: unable to download Forge installer 2019-09-05 15:49:27 +08:00
huanghongxun a9feb65fc8 Try to fix ConcurrentModificationException 2019-08-25 15:35:17 +08:00
huanghongxun 93837a28fa No longer report imageio exception 2019-08-24 12:06:54 +08:00
huanghongxun 678f3b8eb7 Try to fix NPE 2019-08-24 12:01:04 +08:00
huanghongxun 0833fa9e7d Fix crash when typing wrong path 2019-08-24 11:59:55 +08:00
huanghongxun 369b9bb6cf Fix #603 2019-08-24 11:57:43 +08:00
huanghongxun c9b6f2f62f Collect uncaught RuntimeException thrown in task execution 2019-08-24 11:49:53 +08:00
huanghongxun e2787af044 no longer force using javaw 2019-08-20 00:54:12 +08:00
huanghongxun 30c27bbb67 MultiMC fabric and local libraries 2019-08-20 00:53:59 +08:00
huanghongxun 7aee7e33d0 Making versions whose library being removed pure patched 2019-08-20 00:53:36 +08:00
huanghongxun a26061c388 alter order if game item context menu 2019-08-19 16:08:48 +08:00
huanghongxun 11e66ac13c Support changing game version 2019-08-19 16:05:40 +08:00
huanghongxun a2ac3c1f18 Hide generated resolved patches in InstallerListPage 2019-08-19 15:39:30 +08:00
huanghongxun e7142e4032 Fix displaying "null" in GameItem 2019-08-19 14:57:42 +08:00
huanghongxun 0dd9dc6c17 Now pressing enter when at main page will launch the game 2019-08-19 00:49:48 +08:00
huanghongxun a53bfed060 Add primary and secondary click action on GameListItem 2019-08-19 00:38:31 +08:00
huanghongxun c5919f2719 Fix OptiFine installation 2019-08-19 00:18:34 +08:00
huanghongxun fb24516b90 Show unrecognized patches in InstallerListPage 2019-08-19 00:17:03 +08:00
huanghongxun 60fa4eb0a1 Fix LibrariesUniqueTask changing order of classpath 2019-08-19 00:16:02 +08:00
huanghongxun 536576084d Fix forge local installation 2019-08-19 00:12:28 +08:00
huanghongxun c62cc499c5 Show HMCL version in game main menu 2019-08-18 10:54:05 +08:00
huanghongxun c11bbf8836 Fix failure of auto-installing more than one library 2019-08-18 10:27:48 +08:00
huanghongxun 71c0cede24 Fixed unrecognizing Optifine jar file 2019-08-18 10:27:12 +08:00
huanghongxun ad16bbc96d Disable failure test 2019-08-17 12:13:05 +08:00
huanghongxun 520ffe4c09 Add missing license header 2019-08-17 00:45:47 +08:00
huanghongxun e3f603c145 Repackaging 2019-08-17 00:12:02 +08:00
huanghongxun 880b8cb072 Support exporting multimc modpack 2019-08-16 21:13:54 +08:00
huanghongxun 86bf3d473a Fixed #601 2019-08-16 14:46:20 +08:00
huanghongxun eb6baa8ad1 Support fabric auto-installation 2019-08-16 12:35:56 +08:00
huanghongxun d53d56d93a Renaming json as well as jar when the folder is renamed manually 2019-08-16 00:29:52 +08:00
huanghongxun d143ff48fa Show furnace icon when the game is patched 2019-08-16 00:29:26 +08:00
huanghongxun b11acb7762 Make auto-installing more flexible 2019-08-16 00:18:52 +08:00
huanghongxun 51aa0dd851 Fix non-idea-compilable errors 2019-08-15 12:54:39 +08:00
huanghongxun 861b0a1c62 Add exceptional functional interfaces 2019-08-15 12:53:52 +08:00
huanghongxun 27a33f567f add missing setCacheDirectory directives 2019-08-15 12:45:31 +08:00
huanghongxun e862a4b46b Fix #592 2019-07-28 20:16:12 +08:00
huanghongxun 2b3e8ebfc0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2019-07-28 20:05:18 +08:00
Yuhui Huang 6118877ad6 Merge pull request #594 from asl97/patch-1
Fix #586
2019-07-27 18:47:10 +08:00
asl97 0454ff9bf7 Fix #586 2019-07-27 01:21:11 +08:00
huanghongxun 4faad50d99 Fix #567 2019-05-31 10:48:38 +08:00
Yuhui Huang 272599b448 Merge pull request #569 from gitfamhub/javafx
Spanish translation
2019-05-30 21:25:36 -05:00
huanghongxun c12d76e2fa Fixed wrong English localization translations. Mentioned in #569. 2019-05-31 10:20:11 +08:00
gitfamhub 55550f1773 Add files via upload 2019-05-30 22:13:25 -04:00
gitfamhub e8992d5ad7 Add files via upload 2019-05-30 22:12:50 -04:00
gitfamhub a8bd589b18 Add files via upload 2019-05-30 21:08:05 -04:00
gitfamhub 602cf7b110 Add files via upload 2019-05-30 21:01:30 -04:00
gitfamhub 1e7b731485 Delete I18N_es.properties 2019-05-30 20:47:23 -04:00
gitfamhub 824b8e410c Add files via upload 2019-05-30 09:23:51 -04:00
gitfamhub ba47953485 Add files via upload 2019-05-30 09:20:00 -04:00
gitfamhub c7e666261f Add files via upload 2019-05-30 07:24:29 -04:00
gitfamhub 0c4c98d2c9 Add files via upload 2019-05-30 07:21:06 -04:00
gitfamhub 0c63be37bb Add files via upload 2019-05-30 07:20:21 -04:00
gitfamhub 9623a311a0 Add files via upload 2019-05-30 07:09:23 -04:00
gitfamhub 13da61d895 Add files via upload 2019-05-30 07:06:43 -04:00
huanghongxun a7c02e2dea name task async 2019-05-13 00:05:08 +08:00
huanghongxun cfb9610afc Merge branch 'task-refactor' into javafx
# Conflicts:
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java
#	HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java
#	HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java
#	HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java
2019-05-10 10:33:06 +08:00
huanghongxun a141c6b137 Display changelog when update 2019-05-09 01:51:51 +08:00
huanghongxun 300efb99d2 Fix #540 2019-05-09 00:49:10 +08:00
yushijinhun 6fb3610bcc Refresh skins on startup
Mentioned in #539
2019-05-02 12:40:46 +08:00
yushijinhun 43fb3ca10a Fix illegal texturePayload may cause crash 2019-05-02 12:35:43 +08:00
yushijinhun adc360eaaa Fix #548 2019-05-02 00:12:33 +08:00
huanghongxun 5e659352d7 Support install Forge/OptiFine from local file 2019-04-30 20:26:31 +08:00
huanghongxun 6595e0a3cf Remove authlib-injector-servers.fxml 2019-04-30 00:29:19 +08:00
huanghongxun c43b2b3bb9 Fix potential "not on UI thread" exception 2019-04-30 00:28:22 +08:00
huanghongxun ea19e6efd8 Add Fabric installerItem in list page 2019-04-29 17:57:04 +08:00
huanghongxun 55d38f2b36 Fix importing fabric multimc instance 2019-04-28 14:18:04 +08:00
huanghongxun e0abf3e0c1 Support fabric detection 2019-04-28 14:17:28 +08:00
huanghongxun 974cba8dee Suppress NPE in LogWindow 2019-04-25 13:56:44 +08:00
huanghongxun 019cf5fc8f Suppress NPE 2019-04-25 13:50:51 +08:00
huanghongxun 1956ba7a73 Force refreshing version list when remove version from disk 2019-04-25 13:39:07 +08:00
huanghongxun c209617ee7 Try to fix ConcurrentModificationException 2019-04-25 13:38:44 +08:00
huanghongxun f4b5e061a4 Suppress NPE 2019-04-25 13:38:12 +08:00
huanghongxun 06b67a4b9a Suppress image resource not found 2019-04-25 12:56:04 +08:00
huanghongxun a385265956 Fix not on ui thread 2019-04-25 11:31:01 +08:00
huanghongxun 6ac32a40c7 Suppress UncheckedIOException thrown by loading alex and steve skin image. 2019-04-25 11:28:49 +08:00
huanghongxun 20c612c660 Fix #537 2019-04-25 11:20:58 +08:00
huanghongxun 7e8cc5fe6c Fix #547 2019-04-25 11:16:59 +08:00
huanghongxun 9ed14b227b Workaround fix for NPE when remove mods 2019-04-25 10:49:25 +08:00
huanghongxun ee466d0f4c Fix #545 2019-04-03 17:02:08 +08:00
huanghongxun b198eea9e4 Friendly prompt if response code is not OK 2019-04-03 14:05:14 +08:00
huanghongxun 8c7580488e More friendly prompt for SocketTimeoutException 2019-03-24 14:30:58 +08:00
huanghongxun 3f4364c67c Fix cannot launch a game multiple times 2019-03-24 12:14:13 +08:00
huanghongxun 23e2df5e58 Unzip natives to /versions/ reducing trashes in temp 2019-03-21 11:08:00 +08:00
huanghongxun 79136db162 Lower XZ preset 2019-03-19 11:11:59 +08:00
huanghongxun 32d0d6a7ee Add some task test 2019-02-26 15:28:40 +08:00
huanghongxun d9a336e50b Remove redundant codes 2019-02-26 13:29:09 +08:00
huanghongxun 3c958d06c8 Replace scheduler to executor 2019-02-26 11:39:38 +08:00
huanghongxun 15a75a69b3 Fix exception handling 2019-02-26 08:17:46 +08:00
huanghongxun cae32cff4c remove isDependentSucceeded in whenComplete 2019-02-25 23:59:21 +08:00
huanghongxun eeef76594f Rename task.lastException -> task.exception 2019-02-25 22:17:30 +08:00
huanghongxun 8f2a1030bc Refactor Task 2019-02-24 23:27:53 +08:00
523 changed files with 20721 additions and 7379 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
*.exe filter=lfs diff=lfs merge=lfs -text
+18
View File
@@ -0,0 +1,18 @@
name: Java CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
java-package: jdk+fx
- name: Build with Gradle
run: ./gradlew build
+5
View File
@@ -17,20 +17,25 @@ NVIDIA
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
# idea
.idea
/out/
/HMCL/out/
/HMCLCore/out/
/HMCLTransformerDiscoveryService/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/HMCLTransformerDiscoveryService/bin/
.classpath
.project
.settings
# netbeans
.nb-gradle
*.exe
+69 -19
View File
@@ -1,6 +1,15 @@
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'org.tukaani:xz:1.8'
}
}
plugins {
id 'com.github.johnrengelman.shadow' version '4.0.0'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
import java.nio.file.FileSystems
@@ -27,8 +36,8 @@ version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
compile project(":HMCLCore")
compile rootProject.files("lib/JFoenix.jar")
implementation project(":HMCLCore")
implementation rootProject.files("lib/JFoenix.jar")
}
def digest(String algorithm, byte[] bytes) {
@@ -69,35 +78,70 @@ ext.packer = Pack200.newPacker()
packer.properties()["pack.effort"] = "9"
ext.unpacker = Pack200.newUnpacker()
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
// .class files remain the same.
def repack(File file) {
def packed = new ByteArrayOutputStream()
new JarFile(file).withCloseable { packer.pack(it, packed) }
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
}
jar {
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version
sourceSets {
java11 {
java {
srcDirs = ['src/main/java11']
}
}
finalizedBy shadowJar
}
compileJava11Java {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
sourceCompatibility = 11
targetCompatibility = 11
}
jar {
enabled = false
dependsOn shadowJar
}
shadowJar {
classifier = null
exclude 'META-INF/maven/**'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
dependencies {
exclude(dependency('org.jetbrains:annotations'))
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version,
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
'javafx.graphics/javafx.css',
'javafx.base/com.sun.javafx.runtime',
'javafx.controls/com.sun.javafx.scene.control.behavior',
'javafx.controls/javafx.scene.control.skin',
'javafx.controls/com.sun.javafx.scene.control',
'javafx.base/com.sun.javafx.binding',
'javafx.base/com.sun.javafx.event',
'javafx.graphics/com.sun.javafx.stage'
].join(" "),
'Add-Exports': [
'java.base/jdk.internal.loader',
'javafx.controls/com.sun.javafx.scene.control.behavior',
'javafx.controls/javafx.scene.control.skin',
'javafx.controls/com.sun.javafx.scene.control',
'javafx.base/com.sun.javafx.binding',
'javafx.graphics/com.sun.javafx.stage',
'javafx.base/com.sun.javafx.event'
].join(" ")
}
doLast {
repack(jar.archivePath)
repack(jar.archivePath) // see repack()
attachSignature(jar.archivePath)
createChecksum(jar.archivePath)
}
@@ -126,6 +170,11 @@ processResources {
convertToBSS "assets/css/root.css"
convertToBSS "assets/css/blue.css"
into('META-INF/versions/11') {
from sourceSets.java11.output
}
dependsOn java11Classes
}
task makePack(dependsOn: jar) {
@@ -140,7 +189,8 @@ task makePack(dependsOn: jar) {
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(6)).withCloseable { it << makePack.outputPath.bytes }
// Our CI server does not have enough memory space to compress file at highest level.
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packXz)
}
@@ -157,4 +207,4 @@ task makeExecutables(dependsOn: jar) doLast {
build.dependsOn makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
build.dependsOn makeExecutables
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -22,38 +22,46 @@ import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.*;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@Override
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
CookieHandler.setDefault(COOKIE_MANAGER);
try {
try {
ConfigHolder.init();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", e);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
@@ -63,8 +71,8 @@ public final class Launcher extends Application {
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
initIcon();
UpdateChecker.init();
@@ -75,8 +83,25 @@ public final class Launcher extends Application {
}
}
@Override
public void stop() throws Exception {
super.stop();
Controllers.onApplicationStop();
}
private void initIcon() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
AwtUtils.setAppleIcon(image);
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
return;
}
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
try {
LOG.info("*** " + Metadata.TITLE + " ***");
@@ -106,7 +131,6 @@ public final class Launcher extends Application {
Schedulers.shutdown();
Controllers.shutdown();
Platform.exit();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
@@ -135,6 +159,11 @@ public final class Launcher extends Application {
} catch (URISyntaxException e) {
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);
}
if (result.isEmpty())
return null;
@@ -142,5 +171,5 @@ public final class Launcher extends Application {
return result;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
+56 -13
View File
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,18 +17,24 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.*;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -42,17 +48,20 @@ public final class Main {
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
checkJavaFX();
checkDirectoryPath();
// This environment check will take ~300ms
thread(() -> checkDSTRootCAX3(), "CA Certificate Check", true);
thread(() -> {
fixLetsEncrypt();
checkDSTRootCAX3();
}, "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
if (UpdateHandler.processArguments(args)) {
return;
}
checkJavaFX();
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
Launcher.main(args);
}
@@ -68,9 +77,13 @@ public final class Main {
private static void checkJavaFX() {
try {
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit(i18n("fatal.missing_javafx"));
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
}
}
@@ -115,4 +128,34 @@ public final class Main {
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
}
static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
try (InputStream ksStream = Files.newInputStream(ksPath)) {
defaultKeyStore.load(ksStream, "changeit".toCharArray());
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
for (String alias : Collections.list(defaultKeyStore.aliases()))
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
instance.init(merged);
SSLContext tls = SSLContext.getInstance("TLS");
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,11 +17,12 @@
*/
package org.jackhuang.hmcl;
import java.nio.file.Path;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Stores metadata about this application.
*/
@@ -35,8 +36,18 @@ public final class Metadata {
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
private static Path getHMCLDirectory() {
String home = System.getProperty("user.home", ".");
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// to fulfill XDG standard.
return Paths.get(home, ".cache", "hmcl");
}
return OperatingSystem.getWorkingDirectory("hmcl");
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -21,24 +21,32 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
/**
* @author huangyuhui
*/
public final class HMCLGameLauncher extends DefaultLauncher {
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options) {
this(repository, versionId, authInfo, options, null);
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) {
this(repository, version, authInfo, options, null);
}
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
this(repository, versionId, authInfo, options, listener, true);
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
this(repository, version, authInfo, options, listener, true);
}
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
super(repository, versionId, authInfo, options, listener, daemon);
public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
super(repository, version, authInfo, options, listener, daemon);
}
@Override
@@ -48,4 +56,29 @@ public final class HMCLGameLauncher extends DefaultLauncher {
res.put("${launcher_version}", Metadata.VERSION);
return res;
}
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
if (optionsFile.exists())
return;
try {
// TODO: Dirty implementation here
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
FileUtils.writeText(optionsFile, "lang:zh_cn\nforceUnicodeFont:true\n");
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
}
}
@Override
public ManagedProcess launch() throws IOException, InterruptedException {
generateOptionsTxt();
return super.launch();
}
@Override
public void makeLaunchScript(File scriptFile) throws IOException {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,24 +19,40 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public boolean checkedModpack = false, checkingModpack = false;
// local version settings
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
@@ -47,26 +63,41 @@ public class HMCLGameRepository extends DefaultGameRepository {
return profile;
}
@Override
public GameDirectoryType getGameDirectoryType(String id) {
if (beingModpackVersions.contains(id) || isModpack(id)) {
return GameDirectoryType.VERSION_FOLDER;
} else {
return getVersionSetting(id).getGameDirType();
}
}
@Override
public File getRunDirectory(String id) {
if (beingModpackVersions.contains(id) || isModpack(id))
return getVersionRoot(id);
else {
VersionSetting vs = profile.getVersionSetting(id);
switch (vs.getGameDirType()) {
case VERSION_FOLDER: return getVersionRoot(id);
case ROOT_FOLDER: return super.getRunDirectory(id);
case CUSTOM: return new File(vs.getGameDir());
default: throw new Error();
}
switch (getGameDirectoryType(id)) {
case VERSION_FOLDER:
return getVersionRoot(id);
case ROOT_FOLDER:
return super.getRunDirectory(id);
case CUSTOM:
File dir = new File(getVersionSetting(id).getGameDir());
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
return dir;
default:
throw new Error();
}
}
@Override
protected void refreshVersionsImpl() {
versionSettings.clear();
localVersionSettings.clear();
super.refreshVersionsImpl();
versions.keySet().forEach(this::loadVersionSetting);
versions.keySet().forEach(this::loadLocalVersionSetting);
versions.keySet().forEach(version -> {
if (isModpack(version)) {
specializeVersionSetting(version);
}
});
try {
File file = new File(getBaseDirectory(), "launcher_profiles.json");
@@ -92,19 +123,57 @@ public class HMCLGameRepository extends DefaultGameRepository {
clean(getRunDirectory(id));
}
private File getVersionSettingFile(String id) {
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
File srcDir = getVersionRoot(srcId);
File dstDir = getVersionRoot(dstId);
if (dstDir.exists()) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
oldVersionSetting.setGameDirType(GameDirectoryType.VERSION_FOLDER);
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
saveVersionSetting(dstId);
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // HMCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
}
private File getLocalVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg");
}
private void loadVersionSetting(String id) {
File file = getVersionSettingFile(id);
private void loadLocalVersionSetting(String id) {
File file = getLocalVersionSettingFile(id);
if (file.exists())
try {
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
initVersionSetting(id, versionSetting);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
initVersionSetting(id, new VersionSetting());
initLocalVersionSetting(id, new VersionSetting());
}
}
@@ -113,18 +182,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
* @param id the version id.
* @return new version setting, null if given version does not exist.
*/
public VersionSetting createVersionSetting(String id) {
public VersionSetting createLocalVersionSetting(String id) {
if (!hasVersion(id))
return null;
if (versionSettings.containsKey(id))
return getVersionSetting(id);
if (localVersionSettings.containsKey(id))
return getLocalVersionSetting(id);
else
return initVersionSetting(id, new VersionSetting());
return initLocalVersionSetting(id, new VersionSetting());
}
private VersionSetting initVersionSetting(String id, VersionSetting vs) {
private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
localVersionSettings.put(id, vs);
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
versionSettings.put(id, vs);
return vs;
}
@@ -133,46 +202,57 @@ public class HMCLGameRepository extends DefaultGameRepository {
*
* @param id version id
*
* @return may return null if the id not exists
* @return corresponding version setting, null if the version has no its own version setting.
*/
public VersionSetting getVersionSetting(String id) {
if (!versionSettings.containsKey(id))
loadVersionSetting(id);
VersionSetting setting = versionSettings.get(id);
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
VersionSetting setting = localVersionSettings.get(id);
if (setting != null && isModpack(id))
setting.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
setting.setGameDirType(GameDirectoryType.VERSION_FOLDER);
return setting;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
profile.getGlobal().setUsesGlobal(true);
return profile.getGlobal();
} else
return vs;
}
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
}
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return new Image("/assets/img/grass.png");
return newImage("/assets/img/grass.png");
Version version = getVersion(id);
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if (version.getMainClass() != null &&
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
return newImage("/assets/img/furnace.png");
else
return new Image("/assets/img/grass.png");
return newImage("/assets/img/grass.png");
}
public boolean saveVersionSetting(String id) {
if (!versionSettings.containsKey(id))
if (!localVersionSettings.containsKey(id))
return false;
File file = getVersionSettingFile(id);
File file = getLocalVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
try {
FileUtils.writeText(file, GSON.toJson(versionSettings.get(id)));
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
@@ -186,9 +266,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
* @return specialized version setting, null if given version does not exist.
*/
public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id);
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null)
vs = createVersionSetting(id);
vs = createLocalVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
@@ -196,7 +276,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public void globalizeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id);
VersionSetting vs = getLocalVersionSetting(id);
if (vs != null)
vs.setUsesGlobal(true);
}
@@ -205,6 +285,54 @@ public class HMCLGameRepository extends DefaultGameRepository {
return FORBIDDEN.contains(id);
}
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
VersionSetting vs = getVersionSetting(version);
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.getMaxMemory())
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) {
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
config.getManifest().injectLaunchOptions(builder);
}
} catch (IOException | JsonParseException e) {
e.printStackTrace();
}
}
return builder.create();
}
@Override
public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg");
@@ -218,6 +346,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
beingModpackVersions.remove(id);
}
public void markVersionLaunchedAbnormally(String id) {
try {
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
} catch (IOException ignored) {
}
}
public boolean unmarkVersionLaunchedAbnormally(String id) {
File file = new File(getVersionRoot(id), ".abnormal");
boolean result = file.isFile();
file.delete();
return result;
}
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.create();
@@ -1,82 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends Task {
private final DefaultGameRepository repository;
private final String version;
private final List<String> whitelist;
private final Modpack modpack;
private final File output;
/**
* @param output mod pack file.
* @param version to locate version.json
*/
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this.repository = repository;
this.version = version;
this.whitelist = whitelist;
this.modpack = modpack;
this.output = output;
onDone().register(event -> {
if (event.isFailed()) output.delete();
});
}
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
if (path.isEmpty())
return true;
for (String s : blackList)
if (path.equals(s))
return false;
for (String s : whitelist)
if (path.equals(s))
return true;
return false;
});
Version mv = repository.getResolvedVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
@@ -35,17 +35,20 @@ import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class HMCLModpackInstallTask extends Task {
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final String name;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final Modpack modpack;
private final List<Task> dependencies = new LinkedList<>();
private final List<Task> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
DependencyManager dependency = profile.getDependency();
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
@@ -74,24 +77,42 @@ public final class HMCLModpackInstallTask extends Task {
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
public List<Task> getDependencies() {
public List<Task<?>> getDependencies() {
return dependencies;
}
@Override
public List<Task> getDependents() {
public List<Task<?>> getDependents() {
return dependents;
}
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
dependencies.add(new VersionJsonSaveTask(repository, version));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
// reinstall libraries
// libraries of Forge and OptiFine should be obtained by installation.
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
continue;
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
}
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
@Override
public List<String> getStages() {
return Stream.concat(
dependents.stream().flatMap(task -> task.getStages().stream()),
Stream.of("hmcl.modpack")
).collect(Collectors.toList());
}
public static final String MODPACK_TYPE = "HMCL";
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -27,51 +26,12 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
/**
* @author huangyuhui
*/
public final class HMCLModpackManager {
public static final List<String> MODPACK_BLACK_LIST = Lang.immutableListOf(
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"pack.json", "launcher.jar", "hmclmc.log", "cache", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
);
public static final List<String> MODPACK_SUGGESTED_BLACK_LIST = Lang.immutableListOf(
"fonts", // BetterFonts
"saves", "servers.dat", "options.txt", // Minecraft
"blueprints" /* BuildCraft */,
"optionsof.txt" /* OptiFine */,
"journeymap" /* JourneyMap */,
"optionsshaders.txt",
"mods/VoxelMods");
public static ModAdviser.ModSuggestion suggestMod(String fileName, boolean isDirectory) {
if (match(MODPACK_BLACK_LIST, fileName, isDirectory))
return ModAdviser.ModSuggestion.HIDDEN;
if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory))
return ModAdviser.ModSuggestion.NORMAL;
else
return ModAdviser.ModSuggestion.SUGGESTED;
}
private static boolean match(List<String> l, String fileName, boolean isDirectory) {
for (String s : l)
if (isDirectory) {
if (fileName.startsWith(s + "/"))
return true;
} else if (fileName.equals(s))
return true;
return false;
}
/**
* Read the manifest in a HMCL modpack.
*
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,18 +18,29 @@
package org.jackhuang.hmcl.game;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
import org.jackhuang.hmcl.launch.ProcessCreationException;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -40,25 +51,23 @@ import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.RejectedExecutionException;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -83,6 +92,7 @@ public final class LauncherHelper {
this.setting = profile.getVersionSetting(selectedVersion);
this.launcherVisibility = setting.getLauncherVisibility();
this.showLogs = setting.isShowLogs();
this.launchingStepsPane.setTitle(i18n("version.launch"));
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
@@ -102,9 +112,9 @@ public final class LauncherHelper {
try {
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(this::launch0);
Schedulers.defaultScheduler().execute(this::launch0);
});
} catch (InterruptedException ignore) {
} catch (InterruptedException | RejectedExecutionException ignore) {
}
});
}
@@ -118,30 +128,37 @@ public final class LauncherHelper {
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
CountDownLatch launchingLatch = new CountDownLatch(1);
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
TaskExecutor executor = dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck)
.thenComposeAsync(Task.allOf(
Task.composeAsync(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version, integrityCheck);
}), Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
}))).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version)).orElse(null);
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.thenCompose(() -> Task.ofResult(i18n("account.methods"), () -> {
.thenComposeAsync(Task.supplyAsync(() -> {
try {
return account.logIn();
} catch (CredentialExpiredException e) {
@@ -151,31 +168,27 @@ public final class LauncherHelper {
LOG.warning("Authentication failed, try playing offline: " + e);
return account.playOffline().orElseThrow(() -> e);
}
}))
.thenApply(Schedulers.javafx(), authInfo -> {
emitStatus(LoadingState.LAUNCHING);
return authInfo;
})
.thenApply(authInfo -> new HMCLGameLauncher(
repository,
selectedVersion,
authInfo,
setting.toLaunchOptions(profile.getGameDir()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, setting, gameVersion.isPresent())
))
.thenCompose(launcher -> { // launcher is prev task's result
}).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
return new HMCLGameLauncher(
repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo,
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
return Task.supplyAsync(launcher::launch);
} else {
return new LaunchTask<ManagedProcess>(() -> {
return Task.supplyAsync(() -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(i18n("version.launch_script"));
});
}
})
.thenAccept(process -> { // process is LaunchTask's result
}).thenAcceptAsync(process -> { // process is LaunchTask's result
if (scriptFile == null) {
PROCESSES.add(process);
if (launcherVisibility == LauncherVisibility.CLOSE)
@@ -191,29 +204,26 @@ public final class LauncherHelper {
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}
})
}).thenRunAsync(() -> {
launchingLatch.await();
}).withStage("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
.executor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
final AtomicInteger finished = new AtomicInteger(0);
@Override
public void onFinished(Task task) {
finished.incrementAndGet();
int runningTasks = executor.getRunningTasks();
Platform.runLater(() -> launchingStepsPane.setProgress(1.0 * finished.get() / runningTasks));
}
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (!success && !Controllers.isStopped()) {
Platform.runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Exception ex = executor.getLastException();
Platform.runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
if (!success) {
Exception ex = executor.getException();
if (ex != null) {
String message;
if (ex instanceof CurseCompletionException) {
@@ -228,7 +238,46 @@ public final class LauncherHelper {
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", url);
else
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URL url = ((DownloadException) ex).getUrl();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", url);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
} else if (ex instanceof AuthlibInjectorDownloadException) {
message = i18n("account.failed.injector_download_failure");
} else if (ex instanceof CharacterDeletedException) {
message = i18n("account.failed.character_deleted");
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
URL url = rce.getUrl();
if (responseCode == 404)
message = i18n("download.code.404", url);
else
message = i18n("download.failed", url, responseCode);
} else {
message = StringUtils.getStackTrace(ex);
}
@@ -237,9 +286,9 @@ public final class LauncherHelper {
MessageType.ERROR);
}
}
});
}
launchingStepsPane.setExecutor(null);
}
launchingStepsPane.setExecutor(null);
});
}
});
@@ -268,6 +317,22 @@ public final class LauncherHelper {
flag = true;
}
if (!flag && version.getJavaVersion() != null) {
if (java.getParsedVersion() < version.getJavaVersion().getMajorVersion()) {
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} else {
Controllers.dialog(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), MessageType.WARNING, () -> {
downloadJava(version.getJavaVersion(), profile).thenAccept(x -> onAccept.run());
});
flag = true;
}
}
}
// 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()
@@ -291,7 +356,7 @@ public final class LauncherHelper {
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
&& version.getMainClass().contains("launchwrapper")
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
@@ -323,7 +388,7 @@ public final class LauncherHelper {
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
final JavaVersion java32 = java;
// First find if same java version but whose platform is 64-bit installed.
@@ -359,13 +424,13 @@ public final class LauncherHelper {
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageType.ERROR, onAccept);
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), onAccept, null);
flag = true;
}
// Cannot allocate too much memory exceeding free space.
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageType.ERROR, onAccept);
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), onAccept, null);
flag = true;
}
@@ -377,7 +442,19 @@ public final class LauncherHelper {
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageType.ERROR, onAccept);
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), onAccept, null);
flag = true;
}
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
if (!flag) {
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), onAccept, null);
flag = true;
}
}
@@ -386,19 +463,43 @@ public final class LauncherHelper {
onAccept.run();
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
}
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<Void> future = new CompletableFuture<>();
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
profile.getDependency().getDownloadProvider()).executor(false);
executor.addTaskListener(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(executor.getException());
} else {
future.complete(null);
}
});
}
});
javaDownloadingPane.setExecutor(executor, true);
Controllers.dialog(javaDownloadingPane);
executor.start();
return future;
}
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.getStage()::show);
Platform.runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
break;
case KEEP:
// No operations here
@@ -416,19 +517,6 @@ public final class LauncherHelper {
}
}
private static class LaunchTask<T> extends TaskResult<T> {
private final ExceptionalSupplier<T, Exception> supplier;
public LaunchTask(ExceptionalSupplier<T, Exception> supplier) {
this.supplier = supplier;
}
@Override
public void execute() throws Exception {
setResult(supplier.get());
}
}
/**
* The managed process listener.
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
@@ -436,17 +524,21 @@ public final class LauncherHelper {
*/
class HMCLProcessListener implements ProcessListener {
private final VersionSetting setting;
private final HMCLGameRepository repository;
private final String version;
private final Map<String, String> forbiddenTokens;
private ManagedProcess process;
private boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final LinkedList<Pair<String, Log4jLevel>> logs;
private final CountDownLatch latch = new CountDownLatch(1);
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CountDownLatch launchingLatch;
public HMCLProcessListener(AuthInfo authInfo, VersionSetting setting, boolean detectWindow) {
this.setting = setting;
public HMCLProcessListener(HMCLGameRepository repository, String version, AuthInfo authInfo, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
this.version = version;
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
if (authInfo == null)
@@ -465,11 +557,17 @@ public final class LauncherHelper {
public void setProcess(ManagedProcess process) {
this.process = process;
String command = new CommandBuilder().addAll(process.getCommands()).toString();
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
command = command.replace(entry.getKey(), entry.getValue());
LOG.info("Launched process: " + command);
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
latch.countDown();
logWindowLatch.countDown();
});
}
@@ -481,7 +579,7 @@ public final class LauncherHelper {
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
launchingLatch.countDown();
}
});
break;
@@ -489,17 +587,17 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
Platform.runLater(launchingLatch::countDown);
break;
case HIDE:
launchingLatch.countDown();
Platform.runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
Controllers.shutdown();
Schedulers.shutdown();
}
});
break;
@@ -511,29 +609,29 @@ public final class LauncherHelper {
String newLog = log;
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
newLog = newLog.replace(entry.getKey(), entry.getValue());
String filteredLog = newLog;
if (level.lessOrEqual(Log4jLevel.ERROR))
System.err.print(log);
System.err.println(filteredLog);
else
System.out.print(log);
System.out.println(filteredLog);
logs.add(pair(log, level));
logs.add(pair(filteredLog, level));
if (logs.size() > config().getLogLines())
logs.removeFirst();
if (showLogs) {
try {
latch.await();
logWindow.waitForLoaded();
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
Platform.runLater(() -> logWindow.logLine(log, level));
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
lwjgl = true;
finishLaunch();
}
@@ -541,32 +639,37 @@ public final class LauncherHelper {
@Override
public void onExit(int exitCode, ExitType exitType) {
launchingLatch.countDown();
if (exitType == ExitType.INTERRUPTED)
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (exitType != ExitType.NORMAL && logWindow == null)
if (exitType != ExitType.NORMAL) {
repository.markVersionLaunchedAbnormally(version);
Platform.runLater(() -> {
logWindow = new LogWindow();
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;
}
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.show();
logWindow.onDone.register(() -> {
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();
});
}
checkExit();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -20,7 +20,16 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
@@ -36,12 +45,20 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
ignored.printStackTrace();
// ignore it, not a valid MCBBS modpack.
}
try {
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
@@ -60,6 +77,12 @@ public final class ModpackHelper {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
}
throw new UnsupportedModpackException(file.toString());
}
@@ -86,7 +109,7 @@ public final class ModpackHelper {
throw new UnsupportedModpackException();
}
public static Task getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -95,16 +118,36 @@ public final class ModpackHelper {
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
};
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
}
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
VersionSetting vs = repository.specializeVersionSetting(name);
repository.undoMark(name);
if (vs != null)
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
} else {
HMCLGameRepository repository = profile.getRepository();
repository.removeVersionFromDisk(name);
}
};
@@ -115,13 +158,29 @@ public final class ModpackHelper {
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
}
public static Task getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name));
default:
throw new UnsupportedModpackException();
}
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
switch (configuration.getType()) {
@@ -140,6 +199,16 @@ public final class ModpackHelper {
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
case ServerModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof ServerModpackManifest))
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name));
default:
throw new UnsupportedModpackException();
}
@@ -147,7 +216,7 @@ public final class ModpackHelper {
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
vs.setUsesGlobal(false);
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
vs.setGameDirType(GameDirectoryType.VERSION_FOLDER);
if (c.isOverrideJavaLocation()) {
vs.setJavaDir(Lang.nonNull(c.getJavaPath(), ""));
@@ -182,5 +251,18 @@ public final class ModpackHelper {
}
}
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
});
}
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
});
}
}
@@ -1,50 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.Objects;
public final class MultiMCInstallVersionSettingTask extends Task {
private final Profile profile;
private final MultiMCInstanceConfiguration manifest;
private final String version;
public MultiMCInstallVersionSettingTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
this.profile = profile;
this.manifest = manifest;
this.version = version;
}
@Override
public Scheduler getScheduler() {
return Schedulers.javafx();
}
@Override
public void execute() {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,47 +17,45 @@
*/
package org.jackhuang.hmcl.game;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
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;
import java.util.logging.Level;
import javax.imageio.ImageIO;
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.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
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.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* @author yushijinhun
@@ -126,6 +124,9 @@ public final class TexturesLoader {
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
}
if (img == null)
throw new IOException("Texture is malformed");
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
@@ -136,15 +137,17 @@ public final class TexturesLoader {
// ==== Skins ====
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
static {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
}
private static void loadDefaultSkin(String path, TextureModel model) {
try (InputStream in = TexturesLoader.class.getResourceAsStream(path)) {
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
}
}
@@ -182,6 +185,30 @@ public final class TexturesLoader {
}
}, uuidFallback);
}
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
return BindingMapping.of(account.getTextures())
.map(textures -> textures
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}
// ====
// ==== Avatar ====
@@ -206,16 +233,34 @@ 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(img -> SwingFXUtils.toFXImage(img, null));
.map(TexturesLoader::toFXImage);
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount) {
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
} else {
return Bindings.createObjectBinding(
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
() -> 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) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -29,19 +29,25 @@ 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.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.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
@@ -50,6 +56,7 @@ import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -61,9 +68,24 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Accounts {
private Accounts() {}
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
private static void triggerAuthlibInjectorUpdateCheck() {
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
Schedulers.io().execute(() -> {
try {
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check update for authlib-injector", e);
}
});
}
}
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(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
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);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -72,6 +94,7 @@ public final class Accounts {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.put("microsoft", FACTORY_MICROSOFT);
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
}
@@ -94,6 +117,8 @@ public final class Accounts {
return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return FACTORY_MOJANG;
else if (account instanceof MicrosoftAccount)
return FACTORY_MICROSOFT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
@@ -197,7 +222,7 @@ public final class Accounts {
Account selected = selectedAccount.get();
if (selected != null) {
Schedulers.io().schedule(() -> {
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
@@ -206,10 +231,14 @@ public final class Accounts {
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
triggerAuthlibInjectorUpdateCheck();
}
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
Schedulers.io().schedule(() -> {
Schedulers.io().execute(() -> {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
@@ -243,7 +272,19 @@ public final class Accounts {
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(Metadata.HMCL_DIRECTORY, DownloadProviders::getDownloadProvider);
return new AuthlibInjectorDownloader(
Metadata.HMCL_DIRECTORY.resolve("authlib-injector.jar"),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
if (local.isPresent()) {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Paths.get("authlib-injector.jar"));
}
};
} else {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
@@ -279,7 +320,8 @@ public final class Accounts {
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
@@ -0,0 +1,91 @@
/*
* 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.setting;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
private final List<String> urls;
public AuthlibInjectorServers(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return urls;
}
@Override
public void validate() throws JsonParseException {
if (urls == null)
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
}
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
private static AuthlibInjectorServers configInstance;
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
}
configInstance = new AuthlibInjectorServers(Collections.emptyList());
if (Files.exists(configLocation)) {
try {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
}
}
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
for (String url : AuthlibInjectorServers.getConfigInstance().getUrls()) {
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
.thenAcceptAsync(Schedulers.javafx(), server -> config().getAuthlibInjectorServers().add(server))
.start();
}
}
}
public static AuthlibInjectorServers getConfigInstance() {
return configInstance;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -110,6 +110,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("proxyPassword")
private StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("width")
private DoubleProperty width = new SimpleDoubleProperty();
@SerializedName("height")
private DoubleProperty height = new SimpleDoubleProperty();
@SerializedName("theme")
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
@@ -329,6 +335,30 @@ public final class Config implements Cloneable, Observable {
return proxyPass;
}
public double getWidth() {
return width.get();
}
public DoubleProperty widthProperty() {
return width;
}
public void setWidth(double width) {
this.width.set(width);
}
public double getHeight() {
return height.get();
}
public DoubleProperty heightProperty() {
return height;
}
public void setHeight(double height) {
this.height.set(height);
}
public Theme getTheme() {
return theme.get();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -52,12 +52,19 @@ public final class ConfigHolder {
return configInstance;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configLocation = locateConfig();
LOG.log(Level.INFO, "Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
@@ -84,6 +91,26 @@ public final class ConfigHolder {
}
private static Path locateConfig() {
Path exePath = Paths.get("");
try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath();
if (Files.isRegularFile(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
Path config = jarPath.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(config))
return config;
Path dotConfig = jarPath.resolve(CONFIG_FILENAME_LINUX);
if (Files.isRegularFile(dotConfig))
return dotConfig;
}
} catch (Throwable ignore) {
}
Path config = Paths.get(CONFIG_FILENAME);
if (Files.isRegularFile(config))
return config;
@@ -93,7 +120,7 @@ public final class ConfigHolder {
return dotConfig;
// create new
return OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? config : dotConfig;
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
}
private static Config loadConfig() throws IOException {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,44 +17,79 @@
*/
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 org.jackhuang.hmcl.ui.FXUtils;
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.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import java.util.Map;
import java.util.Optional;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
public final class DownloadProviders {
private DownloadProviders() {}
public static final Map<String, DownloadProvider> providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider()));
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
public static final String DEFAULT_PROVIDER_ID = "bmclapi";
public static final Map<String, DownloadProvider> providersById;
private static ObjectBinding<DownloadProvider> downloadProviderProperty;
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
static {
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider(bmclapiRoot)),
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
}
static void init() {
downloadProviderProperty = Bindings.createObjectBinding(
() -> Optional.ofNullable(providersById.get(config().getDownloadType()))
.orElse(providersById.get(DEFAULT_PROVIDER_ID)),
config().downloadTypeProperty());
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(providersById.get(config().getDownloadType()))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
DOWNLOAD_PROVIDER.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
providersById.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
});
}
public static DownloadProvider getDownloadProvider() {
return downloadProviderProperty.get();
public static String getPrimaryDownloadProviderId() {
String downloadType = config().getDownloadType();
if (providersById.containsKey(downloadType))
return downloadType;
else
return DEFAULT_PROVIDER_ID;
}
public static ObservableObjectValue<DownloadProvider> downloadProviderProperty() {
return downloadProviderProperty;
public static AdaptedDownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
AdaptedDownloadProvider adaptedDownloadProvider = new AdaptedDownloadProvider();
DownloadProvider primary = 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;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -24,6 +24,7 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -162,17 +163,15 @@ public final class Profile implements Observable {
}
public DefaultDependencyManager getDependency() {
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
return getDependency(DownloadProviders.getDownloadProvider());
}
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
return new DefaultDependencyManager(repository, downloadProvider, HMCLCacheRepository.REPOSITORY);
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
getGlobal().setGlobal(true); // always keep global.isGlobal = true
getGlobal().setUsesGlobal(true);
return getGlobal();
} else
return vs;
return repository.getVersionSetting(id);
}
@Override
@@ -226,7 +225,7 @@ public final class Profile implements Observable {
@Override
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -25,6 +25,8 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.Locale;
public class Settings {
private static Settings instance;
@@ -44,10 +46,14 @@ public class Settings {
}
private Settings() {
config().localizationProperty().addListener(unused -> updateSystemLocale());
updateSystemLocale();
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
Profiles.init();
AuthlibInjectorServers.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
@@ -73,4 +79,8 @@ public class Settings {
return null;
}
}
private static void updateSystemLocale() {
Locale.setDefault(config().getLocalization().getLocale());
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -23,8 +23,8 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -39,7 +39,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@JsonAdapter(Theme.TypeAdapter.class)
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("#283593"), // dark blue
@@ -49,12 +49,14 @@ public class Theme {
Color.web("#B71C1C") // red
};
private final Color paint;
private final String color;
private final String name;
Theme(String name, String color) {
this.name = name;
this.color = color;
this.paint = Color.web(color);
}
public String getName() {
@@ -78,12 +80,17 @@ public class Theme {
}
public String[] getStylesheets() {
Color textFill = getForegroundColor();
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", Optional.ofNullable(System.getProperty("hmcl.font.override")).map(fontFamily -> "-fx-font-family: " + fontFamily + ";").orElse("")));
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
@@ -138,7 +145,7 @@ public class Theme {
}
public static ObjectBinding<Color> blackFillBinding() {
return Bindings.createObjectBinding(() -> Color.BLACK);
return Bindings.createObjectBinding(() -> BLACK);
}
public static ObjectBinding<Color> whiteFillBinding() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,39 +19,29 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting {
public final class VersionSetting implements Cloneable {
public transient String id;
@@ -422,17 +412,17 @@ public final class VersionSetting {
* 0 - .minecraft<br/>
* 1 - .minecraft/versions/&lt;version&gt;/<br/>
*/
private final ObjectProperty<EnumGameDirectory> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
private final ObjectProperty<GameDirectoryType> gameDirTypeProperty = new SimpleObjectProperty<>(this, "gameDirType", GameDirectoryType.ROOT_FOLDER);
public ObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
public ObjectProperty<GameDirectoryType> gameDirTypeProperty() {
return gameDirTypeProperty;
}
public EnumGameDirectory getGameDirType() {
public GameDirectoryType getGameDirType() {
return gameDirTypeProperty.get();
}
public void setGameDirType(EnumGameDirectory gameDirType) {
public void setGameDirType(GameDirectoryType gameDirType) {
gameDirTypeProperty.set(gameDirType);
}
@@ -475,14 +465,21 @@ public final class VersionSetting {
}
public JavaVersion getJavaVersion() throws InterruptedException {
return getJavaVersion(true);
}
public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
// TODO: lazy initialization may result in UI suspension.
if (StringUtils.isBlank(getJava()))
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
} catch (IOException e) {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.PLATFORM);
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
@@ -531,34 +528,32 @@ public final class VersionSetting {
defaultJavaPathProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion()).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
.setMinMemory(getMinMemory())
.setMetaspace(Lang.toIntOrNull(getPermSize()))
.setWidth(getWidth())
.setHeight(getHeight())
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs());
if (config().hasProxy()) {
builder.setProxyHost(config().getProxyHost());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
return builder.create();
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setShowLogs(isShowLogs());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
versionSetting.setWidth(getWidth());
versionSetting.setHeight(getHeight());
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
return versionSetting;
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
@@ -595,7 +590,7 @@ public final class VersionSetting {
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject))
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
return null;
JsonObject obj = (JsonObject) json;
@@ -624,7 +619,7 @@ public final class VersionSetting {
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(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
return vs;
@@ -0,0 +1,17 @@
package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public class AwtUtils {
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
Object taskBar = getTaskBarMethod.invoke(null);
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
setIconImageMethod.invoke(taskBar, image);
} catch (Throwable ignore) {
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,64 +17,53 @@
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.download.java.JavaRepository;
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.AccountList;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
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.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
private static Scene scene;
private static Stage stage;
private static MainPage mainPage = null;
private static SettingsPage settingsPage = null;
private static VersionPage versionPage = null;
private static GameList gameListPage = null;
private static AccountList accountListPage = null;
private static ProfileList profileListPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static LeftPaneController leftPaneController;
private static RootPage rootPage;
private static DecoratorController decorator;
public static Scene getScene() {
@@ -85,47 +74,6 @@ public final class Controllers {
return stage;
}
// FXThread
public static SettingsPage getSettingsPage() {
if (settingsPage == null)
settingsPage = new SettingsPage();
return settingsPage;
}
// FXThread
public static GameList getGameListPage() {
if (gameListPage == null) {
gameListPage = new GameList();
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;
}
// FXThread
public static AccountList getAccountListPage() {
if (accountListPage == null) {
AccountList accountListPage = new AccountList();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
Controllers.accountListPage = accountListPage;
}
return accountListPage;
}
// FXThread
public static ProfileList getProfileListPage() {
if (profileListPage == null) {
ProfileList profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
Controllers.profileListPage = profileListPage;
}
return profileListPage;
}
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
@@ -133,6 +81,13 @@ public final class Controllers {
return versionPage;
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
}
// FXThread
public static AuthlibInjectorServersPage getServersPage() {
if (serversPage == null)
@@ -145,62 +100,25 @@ public final class Controllers {
return decorator;
}
public static MainPage getMainPage() {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
mainPage.setCurrentGame(version);
} else {
mainPage.setCurrentGame(i18n("version.empty"));
}
});
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(
BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L) : version.getReleaseTime())
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
.map(version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
node.setOnMouseClicked(e -> profile.setSelectedVersion(version.getId()));
return node;
})
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.getVersions().setAll(children);
});
});
Controllers.mainPage = mainPage;
}
return mainPage;
}
public static LeftPaneController getLeftPaneController() {
return leftPaneController;
public static void onApplicationStop() {
config().setHeight(stageHeight.get());
config().setWidth(stageWidth.get());
}
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());
stage.setWidth(config().getWidth());
stageWidth.bind(stage.widthProperty());
stage.setOnCloseRequest(e -> Launcher.stopApplication());
decorator = new DecoratorController(stage, getMainPage());
leftPaneController = new LeftPaneController();
decorator.getDecorator().drawerProperty().setAll(leftPaneController);
decorator = new DecoratorController(stage, getRootPage());
if (config().getCommonDirType() == EnumCommonDirectory.CUSTOM &&
!FileUtils.canCreateDirectory(config().getCommonDirectory())) {
@@ -208,13 +126,20 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.of(JavaVersion::initialize).start();
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
scene = new Scene(decorator.getDecorator(), 800, 519);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
stage.setMinHeight(482 + 32);
stage.setMinWidth(802 + 32);
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
}
public static void dialog(Region content) {
@@ -238,35 +163,40 @@ public final class Controllers {
dialog(new MessageDialogPane(text, title, type, onAccept));
}
public static void confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
}
public static InputDialogPane inputDialog(String text, FutureCallback<String> onResult) {
InputDialogPane pane = new InputDialogPane(text, onResult);
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
return prompt(title, onResult, "");
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue) {
InputDialogPane pane = new InputDialogPane(title, initialValue, onResult);
dialog(pane);
return pane;
return pane.getCompletableFuture();
}
public static Region taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, "");
public static CompletableFuture<List<PromptDialogPane.Builder.Question<?>>> prompt(PromptDialogPane.Builder builder) {
PromptDialogPane pane = new PromptDialogPane(builder);
dialog(pane);
return pane.getCompletableFuture();
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, null);
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, Consumer<Region> onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setSubtitle(subtitle);
pane.setExecutor(executor);
dialog(pane);
return pane;
}
public static void navigate(Node node) {
decorator.getNavigator().navigate(node);
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
}
public static boolean isStopped() {
@@ -274,15 +204,11 @@ public final class Controllers {
}
public static void shutdown() {
mainPage = null;
settingsPage = null;
rootPage = null;
versionPage = null;
serversPage = null;
decorator = null;
stage = null;
scene = null;
gameListPage = null;
accountListPage = null;
profileListPage = null;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -22,7 +22,6 @@ import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -30,6 +29,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -67,7 +67,7 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,9 +18,9 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
@@ -28,15 +28,14 @@ import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.beans.value.WritableValue;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
@@ -45,6 +44,7 @@ import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
@@ -59,12 +59,10 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.Objects;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -98,8 +96,10 @@ public final class FXUtils {
value.addListener((a, b, c) -> consumer.accept(c));
}
public static <T> void onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener(new WeakChangeListener<>((a, b, c) -> consumer.accept(c)));
public static <T> WeakChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
WeakChangeListener<T> listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c));
value.addListener(listener);
return listener;
}
public static <T> void onChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
@@ -107,9 +107,9 @@ public final class FXUtils {
onChange(value, consumer);
}
public static <T> void onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
public static <T> WeakChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
consumer.accept(value.getValue());
onWeakChange(value, consumer);
return onWeakChange(value, consumer);
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
@@ -199,19 +199,19 @@ public final class FXUtils {
return field.getProperties().containsKey("FXUtils.validation");
}
public static void setOverflowHidden(Region region, boolean hidden) {
if (hidden) {
Rectangle rectangle = new Rectangle();
rectangle.widthProperty().bind(region.widthProperty());
rectangle.heightProperty().bind(region.heightProperty());
region.setClip(rectangle);
} else {
region.setClip(null);
}
public static Rectangle setOverflowHidden(Region region) {
Rectangle rectangle = new Rectangle();
rectangle.widthProperty().bind(region.widthProperty());
rectangle.heightProperty().bind(region.heightProperty());
region.setClip(rectangle);
return rectangle;
}
public static boolean getOverflowHidden(Region region) {
return region.getClip() != null;
public static Rectangle setOverflowHidden(Region region, double arc) {
Rectangle rectangle = setOverflowHidden(region);
rectangle.setArcWidth(arc);
rectangle.setArcHeight(arc);
return rectangle;
}
public static void setLimitWidth(Region region, double width) {
@@ -299,6 +299,27 @@ public final class FXUtils {
});
}
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
animationKey = "FXUTILS.ANIMATION." + animationKey;
Object oldTimeline = node.getProperties().get(animationKey);
if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
if (timeline != null) timeline.play();
node.getProperties().put(animationKey, timeline);
}
public static <T> void playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
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);
} else {
playAnimation(node, animationKey, new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)),
new KeyFrame(duration, new KeyValue(property, to, interpolator))
));
}
}
public static void openFolder(File file) {
if (!FileUtils.makeDirectory(file)) {
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
@@ -395,7 +416,7 @@ public final class FXUtils {
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum> property) {
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
unbindEnum(comboBox);
ChangeListener<Number> listener = (a, b, newValue) ->
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
@@ -418,69 +439,18 @@ public final class FXUtils {
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
public static void smoothScrolling(ListView<?> listView) {
listView.skinProperty().addListener(o -> {
ScrollBar bar = (ScrollBar) listView.lookup(".scroll-bar");
Node virtualFlow = listView.lookup(".virtual-flow");
double[] frictions = new double[]{0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
double[] pushes = new double[]{1};
double[] derivatives = new double[frictions.length];
Timeline timeline = new Timeline();
bar.addEventHandler(MouseEvent.DRAG_DETECTED, e -> timeline.stop());
EventHandler<ScrollEvent> scrollEventHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
int direction = event.getDeltaY() > 0 ? -1 : 1;
for (int i = 0; i < pushes.length; ++i)
derivatives[i] += direction * pushes[i];
if (timeline.getStatus() == Animation.Status.STOPPED)
timeline.play();
event.consume();
}
};
bar.addEventHandler(ScrollEvent.ANY, scrollEventHandler);
virtualFlow.setOnScroll(scrollEventHandler);
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
for (int i = 0; i < derivatives.length; ++i)
derivatives[i] *= frictions[i];
for (int i = 1; i < derivatives.length; ++i)
derivatives[i] += derivatives[i - 1];
double dy = derivatives[derivatives.length - 1];
double height = listView.getLayoutBounds().getHeight();
bar.setValue(Math.min(Math.max(bar.getValue() + dy / height, 0), 1));
if (Math.abs(dy) < 0.001)
timeline.stop();
listView.requestLayout();
}));
timeline.setCycleCount(Animation.INDEFINITE);
});
}
public static <T> T runInUIThread(Supplier<T> supplier) {
if (javafx.application.Platform.isFxApplicationThread()) {
return supplier.get();
} else {
CountDownLatch doneLatch = new CountDownLatch(1);
AtomicReference<T> reference = new AtomicReference<>();
Platform.runLater(() -> {
try {
reference.set(supplier.get());
} finally {
doneLatch.countDown();
}
});
try {
doneLatch.await();
} catch (InterruptedException var3) {
Thread.currentThread().interrupt();
}
return reference.get();
/**
* 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
* @see ResourceNotFoundError
*/
public static Image newImage(String url) {
try {
return new Image(url);
} catch (IllegalArgumentException e) {
throw new ResourceNotFoundError("Cannot access image: " + url, e);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,51 +19,198 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import java.util.function.Consumer;
import org.jackhuang.hmcl.util.i18n.I18n;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
*/
public class InstallerItem extends BorderPane {
public class InstallerItem extends Control {
private final String id;
private final String imageUrl;
public final StringProperty libraryVersion = new SimpleStringProperty();
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
public final BooleanProperty incompatibleWithGame = new SimpleBooleanProperty();
public final BooleanProperty removable = new SimpleBooleanProperty();
public final BooleanProperty upgradable = new SimpleBooleanProperty(false);
public final BooleanProperty installable = new SimpleBooleanProperty(true);
public final ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
public final ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
public InstallerItem(String artifact, String version, Runnable upgrade, Consumer<InstallerItem> deleteCallback) {
getStyleClass().add("two-line-list-item");
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
public InstallerItem(LibraryAnalyzer.LibraryType id) {
this(id.getPatchId());
}
{
TwoLineListItem item = new TwoLineListItem();
item.setTitle(artifact);
item.setSubtitle(i18n("archive.version") + ": " + version);
setCenter(item);
}
public InstallerItem(String id) {
this.id = id;
{
HBox hBox = new HBox();
JFXButton upgradeButton = new JFXButton();
upgradeButton.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
upgradeButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(upgradeButton, i18n("install.change_version"));
upgradeButton.setOnMouseClicked(e -> upgrade.run());
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
deleteButton.getStyleClass().add("toggle-icon4");
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.getChildren().setAll(upgradeButton, deleteButton);
setRight(hBox);
switch (id) {
case "game":
imageUrl = "/assets/img/grass.png";
break;
case "fabric":
imageUrl = "/assets/img/fabric.png";
break;
case "forge":
imageUrl = "/assets/img/forge.png";
break;
case "liteloader":
imageUrl = "/assets/img/chicken.png";
break;
case "optifine":
imageUrl = "/assets/img/command.png";
break;
default:
imageUrl = null;
break;
}
}
public void setState(String libraryVersion, boolean incompatibleWithGame, boolean removable) {
this.libraryVersion.set(libraryVersion);
this.incompatibleWithGame.set(incompatibleWithGame);
this.removable.set(removable);
}
public String getLibraryId() {
return id;
}
@Override
protected Skin<?> createDefaultSkin() {
return new InstallerItemSkin(this);
}
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 InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
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));
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
}
}
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
InstallerItemSkin(InstallerItem control) {
super(control);
HBox hbox = new HBox();
getChildren().setAll(hbox);
JFXDepthManager.setDepth(hbox, 1);
hbox.getStyleClass().add("card");
hbox.setAlignment(Pos.CENTER_LEFT);
if (control.imageUrl != null) {
hbox.getChildren().add(FXUtils.limitingSize(new ImageView(new Image(control.imageUrl, 32, 32, true, true)), 32, 32));
}
Label nameLabel = new Label();
hbox.getChildren().add(nameLabel);
nameLabel.setPrefWidth(80);
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
Label label = new Label();
hbox.getChildren().add(label);
label.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(label, Priority.ALWAYS);
label.textProperty().bind(Bindings.createStringBinding(() -> {
String incompatibleWith = control.incompatibleLibraryName.get();
String version = control.libraryVersion.get();
if (control.incompatibleWithGame.get()) {
return i18n("install.installer.change_version", version);
} else if (incompatibleWith != null) {
return i18n("install.installer.incompatible", i18n("install.installer." + incompatibleWith));
} else if (version == null) {
return i18n("install.installer.not_installed");
} else {
return i18n("install.installer.version", version);
}
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
JFXButton closeButton = new JFXButton();
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
closeButton.getStyleClass().add("toggle-icon4");
closeButton.visibleProperty().bind(control.removable);
closeButton.managedProperty().bind(closeButton.visibleProperty());
closeButton.onMouseClickedProperty().bind(control.removeAction);
hbox.getChildren().add(closeButton);
JFXButton arrowButton = new JFXButton();
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
? SVG.update(Theme.blackFillBinding(), -1, -1)
: SVG.arrowRight(Theme.blackFillBinding(), -1, -1),
control.upgradable));
arrowButton.getStyleClass().add("toggle-icon4");
arrowButton.visibleProperty().bind(Bindings.createBooleanBinding(
() -> control.installable.get() && control.incompatibleLibraryName.get() == null,
control.installable, control.incompatibleLibraryName));
arrowButton.managedProperty().bind(arrowButton.visibleProperty());
arrowButton.onMouseClickedProperty().bind(control.action);
hbox.getChildren().add(arrowButton);
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
if (clickable) {
hbox.onMouseClickedProperty().bind(control.action);
hbox.setCursor(Cursor.HAND);
} else {
hbox.onMouseClickedProperty().unbind();
hbox.onMouseClickedProperty().set(null);
hbox.setCursor(Cursor.DEFAULT);
}
});
}
}
}
@@ -1,137 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController extends AdvancedListBox {
public LeftPaneController() {
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
}
});
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.setOnAction(e -> Controllers.navigate(Controllers.getProfileListPage()));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(new Image("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(new Image("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
this
.startCategory(i18n("account").toUpperCase())
.add(accountListItem)
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
}
// ==== Accounts ====
public void checkAccount() {
if (Accounts.getAccounts().isEmpty())
Platform.runLater(this::addNewAccount);
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane());
}
// ====
private boolean checkedModpack = false;
private void onRefreshedVersions(HMCLGameRepository repository) {
runInFX(() -> {
if (!checkedModpack) {
checkedModpack = true;
if (repository.getVersionCount() == 0) {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenApply(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenAccept(modpack -> {
AtomicReference<Region> region = new AtomicReference<>();
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
executor.start();
}).start();
}
}
}
checkAccount();
});
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -39,6 +39,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
VBox list = new VBox();
StackPane contentPane = new StackPane();
{
@@ -46,10 +47,8 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
{
scrollPane.setFitToWidth(true);
VBox list = new VBox();
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
list.setPadding(new Insets(10));
VBox content = new VBox();
content.getChildren().setAll(list, placeholder);
@@ -87,8 +86,13 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
refreshable -> {
if (refreshable) vBox.getChildren().setAll(btnRefresh, btnAdd);
else vBox.getChildren().setAll(btnAdd);
if (refreshable) {
list.setPadding(new Insets(10, 10, 15 + 40 + 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnRefresh, btnAdd);
} else {
list.setPadding(new Insets(10, 10, 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnAdd);
}
});
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,33 +17,47 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.beans.property.*;
import javafx.beans.value.WeakChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.concurrent.CountDownLatch;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayDeque;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -52,141 +66,94 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public final class LogWindow extends Stage {
private final ReadOnlyIntegerWrapper fatal = new ReadOnlyIntegerWrapper(0);
private final ReadOnlyIntegerWrapper error = new ReadOnlyIntegerWrapper(0);
private final ReadOnlyIntegerWrapper warn = new ReadOnlyIntegerWrapper(0);
private final ReadOnlyIntegerWrapper info = new ReadOnlyIntegerWrapper(0);
private final ReadOnlyIntegerWrapper debug = new ReadOnlyIntegerWrapper(0);
private final ArrayDeque<Log> logs = new ArrayDeque<>();
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
}
};
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) {
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
put(level, property);
}
}
};
private final LogWindowImpl impl = new LogWindowImpl();
private final CountDownLatch latch = new CountDownLatch(1);
public final EventManager<Event> onDone = new EventManager<>();
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private boolean stopCheckLogCount = false;
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
getIcons().add(newImage("/assets/img/icon.png"));
public LogWindow(String text) {
this();
onDone.register(() -> logLine(text, Log4jLevel.INFO));
}
public ReadOnlyIntegerProperty fatalProperty() {
return fatal.getReadOnlyProperty();
}
public int getFatal() {
return fatal.get();
}
public ReadOnlyIntegerProperty errorProperty() {
return error.getReadOnlyProperty();
}
public int getError() {
return error.get();
}
public ReadOnlyIntegerProperty warnProperty() {
return warn.getReadOnlyProperty();
}
public int getWarn() {
return warn.get();
}
public ReadOnlyIntegerProperty infoProperty() {
return info.getReadOnlyProperty();
}
public int getInfo() {
return info.get();
}
public ReadOnlyIntegerProperty debugProperty() {
return debug.getReadOnlyProperty();
}
public int getDebug() {
return debug.get();
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
}
public void logLine(String line, Log4jLevel level) {
Element div = impl.document.createElement("div");
// a <pre> element to prevent multiple spaces and tabs being removed.
Element pre = impl.document.createElement("pre");
pre.setTextContent(line);
div.appendChild(pre);
impl.body.appendChild(div);
impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");scrollToBottom();");
Log log = new Log(parseEscapeSequence(line), level);
logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
switch (level) {
case FATAL:
fatal.set(fatal.get() + 1);
break;
case ERROR:
error.set(error.get() + 1);
break;
case WARN:
warn.set(warn.get() + 1);
break;
case INFO:
info.set(info.get() + 1);
break;
case DEBUG:
debug.set(debug.get() + 1);
break;
default:
// ignore
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
if (!stopCheckLogCount) checkLogCount();
}
public void showGameCrashReport() {
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
Platform.runLater(() -> {
impl.listView.scrollTo(log);
});
break;
}
}
show();
}
private void shakeLogs() {
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
}
private void checkLogCount() {
while (logs.size() > config().getLogLines()) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
// TODO: fix O(n)
impl.listView.getItems().remove(0);
}
}
}
public void waitForLoaded() throws InterruptedException {
latch.await();
private static class Log {
private final String log;
private final Log4jLevel level;
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
}
public class LogWindowImpl extends StackPane {
public class LogWindowImpl extends Control {
@FXML
private WebView webView;
@FXML
private ToggleButton btnFatals;
@FXML
private ToggleButton btnErrors;
@FXML
private ToggleButton btnWarns;
@FXML
private ToggleButton btnInfos;
@FXML
private ToggleButton btnDebugs;
@FXML
private ComboBox<String> cboLines;
final WebEngine engine;
Node body;
Document document;
private ListView<Log> listView = new JFXListView<>();
private BooleanProperty autoScroll = new SimpleBooleanProperty();
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<>();
LogWindowImpl() {
FXUtils.loadFXML(this, "/assets/fxml/log.fxml");
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
body = document.getElementsByTagName("body").item(0);
engine.executeScript("limitedLogs=" + config().getLogLines());
latch.countDown();
onDone.fireEvent(new Event(LogWindow.this));
}
});
getStyleClass().add("log-window");
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
@@ -195,50 +162,173 @@ public final class LogWindow extends Stage {
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + config().getLogLines());
});
if (!flag)
cboLines.getSelectionModel().select(0);
btnFatals.textProperty().bind(Bindings.concat(fatal, " fatals"));
btnErrors.textProperty().bind(Bindings.concat(error, " errors"));
btnWarns.textProperty().bind(Bindings.concat(warn, " warns"));
btnInfos.textProperty().bind(Bindings.concat(info, " infos"));
btnDebugs.textProperty().bind(Bindings.concat(debug, " debugs"));
btnFatals.selectedProperty().addListener(o -> specificChanged());
btnErrors.selectedProperty().addListener(o -> specificChanged());
btnWarns.selectedProperty().addListener(o -> specificChanged());
btnInfos.selectedProperty().addListener(o -> specificChanged());
btnDebugs.selectedProperty().addListener(o -> specificChanged());
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
for (int i = 0; i < 5; ++i) {
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
levelShownMap.get(levels[i]).bind(showLevel.get(i));
}
}
private void specificChanged() {
String res = "";
if (btnFatals.isSelected())
res += "\"fatal\", ";
if (btnErrors.isSelected())
res += "\"error\", ";
if (btnWarns.isSelected())
res += "\"warn\", ";
if (btnInfos.isSelected())
res += "\"info\", ";
if (btnDebugs.isSelected())
res += "\"debug\", ";
if (StringUtils.isNotBlank(res))
res = StringUtils.substringBeforeLast(res, ", ");
engine.executeScript("specific([" + res + "])");
}
@FXML
private void onTerminateGame() {
LauncherHelper.stopManagedProcesses();
}
@FXML
private void onClear() {
engine.executeScript("clear()");
impl.listView.getItems().clear();
logs.clear();
}
private void onExportLogs() {
thread(() -> {
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to export logs", e);
return;
}
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) {
}
}
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new LogWindowSkin(this);
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + backgroundColor + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(buttonText);
button.setSelected(true);
showLevel.bind(button.selectedProperty());
return button;
}
protected LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
vbox.setPadding(new Insets(3, 0, 3, 0));
vbox.setStyle("-fx-background-color: white");
getChildren().setAll(vbox);
{
BorderPane borderPane = new BorderPane();
borderPane.setPadding(new Insets(0, 3, 0, 3));
{
HBox hBox = new HBox(3);
hBox.setPadding(new Insets(0, 0, 0, 4));
hBox.setAlignment(Pos.CENTER_LEFT);
Label label = new Label(i18n("logwindow.show_lines"));
hBox.getChildren().setAll(label, control.cboLines);
borderPane.setLeft(hBox);
}
{
HBox hBox = new HBox(3);
hBox.getChildren().setAll(
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
);
borderPane.setRight(hBox);
}
vbox.getChildren().add(borderPane);
}
{
ListView<Log> listView = control.listView;
listView.getItems().addListener((InvalidationListener) observable -> {
if (!listView.getItems().isEmpty() && control.autoScroll.get())
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
listView.setCellFactory(x -> new ListCell<Log>() {
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
}
setPadding(new Insets(2));
setWrapText(true);
setGraphic(null);
}
@Override
protected void updateItem(Log item, boolean empty) {
super.updateItem(item, empty);
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
if (empty) {
setText(null);
} else {
setText(item.log);
}
}
});
VBox.setVgrow(listView, Priority.ALWAYS);
vbox.getChildren().add(listView);
}
{
HBox hBox = new HBox(3);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setPadding(new Insets(0, 3, 0, 3));
JFXCheckBox autoScrollCheckBox = new JFXCheckBox(i18n("logwindow.autoscroll"));
autoScrollCheckBox.setSelected(true);
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
vbox.getChildren().add(hBox);
}
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -37,7 +37,7 @@ public final class SVG {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
path.fillProperty().bind(fill);
if (fill != null) path.fillProperty().bind(fill);
if (width < 0 || height < 0) {
StackPane pane = new StackPane(path);
@@ -67,10 +67,18 @@ public final class SVG {
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);
}
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
}
public static Node 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);
}
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);
}
@@ -170,4 +178,20 @@ public final class SVG {
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
}
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);
}
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);
}
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
}
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);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -44,12 +44,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
BorderPane root = new BorderPane();
{
List<Node> toolbarButtons = initializeToolbar(skinnable);
if (!toolbarButtons.isEmpty()) {
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(initializeToolbar(skinnable));
toolbar.getChildren().setAll(toolbarButtons);
root.setTop(toolbar);
}
@@ -92,5 +93,15 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
protected abstract List<Node> initializeToolbar(T skinnable);
}
@@ -0,0 +1,65 @@
/*
* 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;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
{
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);
}
});
setBody(webView);
}
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
setActions(updateButton, cancelButton);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,20 +17,53 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class WebStage extends Stage {
private final WebView webView = new WebView();
protected final StackPane pane = new StackPane();
protected final JFXProgressBar progressBar = new JFXProgressBar();
protected final WebView webView = new WebView();
protected final WebEngine webEngine = webView.getEngine();
public WebStage() {
setScene(new Scene(webView, 800, 480));
this(800, 480);
}
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
switch (webView.getEngine().getLoadWorker().getState()) {
case SUCCEEDED:
case FAILED:
case CANCELLED:
return false;
default:
return true;
}
}, webEngine.getLoadWorker().stateProperty()));
BorderPane borderPane = new BorderPane();
borderPane.setPickOnBounds(false);
borderPane.setTop(progressBar);
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
pane.getChildren().setAll(webView, borderPane);
}
public WebView getWebView() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,20 +18,26 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.scene.control.Tooltip;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
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 static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@Override
@@ -42,17 +48,34 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(new Image("/assets/img/craft_table.png"));
setImage(newImage("/assets/img/craft_table.png"));
tooltip.setText("");
} else {
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account));
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.setText(account.getCharacter() + " " + accountTooltip(account));
}
}
};
public AccountAdvancedListItem() {
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
setOnScroll(event -> {
Account current = account.get();
if (current == null) return;
ObservableList<Account> accounts = Accounts.getAccounts();
int currentIndex = accounts.indexOf(account.get());
if (event.getDeltaY() > 0) { // up
currentIndex--;
} else { // down
currentIndex++;
}
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
});
}
public ObjectProperty<Account> accountProperty() {
@@ -60,11 +83,22 @@ public class AccountAdvancedListItem extends AdvancedListItem {
}
private static String accountSubtitle(Account account) {
if (account instanceof OfflineAccount)
return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount)
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
if (account instanceof AuthlibInjectorAccount) {
return ((AuthlibInjectorAccount) account).getServer().getName();
} else {
return loginTypeName;
}
}
private static String accountTooltip(Account account) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
return account.getUsername() + ", " + i18n("account.injector.server") + ": " + server.getName();
} else if (account instanceof YggdrasilAccount) {
return account.getUsername();
else
} else {
return "";
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -24,11 +24,12 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class AccountList extends ListPage<AccountListItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.manage"));
private final 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;
@@ -51,7 +52,7 @@ public class AccountList extends ListPage<AccountListItem> implements DecoratorP
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,28 +18,49 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
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.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import static java.util.Collections.emptySet;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItem extends RadioButton {
private final Account account;
@@ -66,7 +87,9 @@ public class AccountListItem extends RadioButton {
if (account instanceof OfflineAccount) {
title.bind(characterName);
} else {
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
title.bind(
account.getUsername().isEmpty() ? characterName :
Bindings.concat(account.getUsername(), " - ", characterName));
}
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
@@ -77,9 +100,9 @@ public class AccountListItem extends RadioButton {
return new AccountListItemSkin(this);
}
public void refresh() {
account.clearCache();
thread(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
account.clearCache();
try {
account.logIn();
} catch (CredentialExpiredException e) {
@@ -89,13 +112,75 @@ public class AccountListItem extends RadioButton {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
throw e1;
}
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
throw e;
}
});
}
public ObservableBooleanValue canUploadSkin() {
if (account instanceof YggdrasilAccount) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
return createBooleanBinding(() -> {
Set<TextureType> uploadableTextures = profile.get()
.map(AuthlibInjectorAccount::getUploadableTextures)
.orElse(emptySet());
return uploadableTextures.contains(TextureType.SKIN);
}, profile);
} else {
return createBooleanBinding(() -> true);
}
} else {
return createBooleanBinding(() -> false);
}
}
/**
* @return the skin upload task, null if no file is selected
*/
@Nullable
public Task<?> uploadSkin() {
if (!(account instanceof YggdrasilAccount)) {
return null;
}
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("account.skin.upload"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) {
return null;
}
return refreshAsync()
.thenRunAsync(() -> {
BufferedImage skinImg;
try {
skinImg = ImageIO.read(selectedFile);
} catch (IOException e) {
throw new InvalidSkinException("Failed to read skin image", e);
}
if (skinImg == null) {
throw new InvalidSkinException("Failed to read skin image");
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
})
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
if (e != null) {
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
}
});
}
public void remove() {
Accounts.getAccounts().remove(account);
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -31,8 +31,11 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -84,12 +87,40 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnRefresh = new JFXButton();
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
SpinnerPane spinnerRefresh = new SpinnerPane();
btnRefresh.setOnMouseClicked(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
.whenComplete(ex -> spinnerRefresh.hideSpinner())
.start();
});
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
right.getChildren().add(btnRefresh);
spinnerRefresh.setContent(btnRefresh);
right.getChildren().add(spinnerRefresh);
JFXButton btnUpload = new JFXButton();
SpinnerPane spinnerUpload = new SpinnerPane();
btnUpload.setOnMouseClicked(e -> {
Task<?> uploadTask = skinnable.uploadSkin();
if (uploadTask != null) {
spinnerUpload.showSpinner();
uploadTask
.whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner())
.start();
}
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
spinnerUpload.setContent(btnUpload);
spinnerUpload.getStyleClass().add("small-spinner-pane");
right.getChildren().add(spinnerUpload);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
@@ -100,7 +131,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.getStyleClass().add("card");
root.setStyle("-fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -61,7 +61,7 @@ public class AccountLoginPane extends StackPane {
String password = txtPassword.getText();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult(() -> oldAccount.logInWithPassword(password))
Task.supplyAsync(() -> oldAccount.logInWithPassword(password))
.whenComplete(Schedulers.javafx(), authInfo -> {
success.accept(authInfo);
fireEvent(new DialogCloseEvent());
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -49,6 +49,7 @@ 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;
@@ -68,6 +69,7 @@ public class AddAccountPane extends StackPane {
@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;
@@ -77,7 +79,7 @@ public class AddAccountPane extends StackPane {
@FXML private SpinnerPane acceptPane;
@FXML private HBox linksContainer;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();
public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
@@ -88,7 +90,7 @@ public class AddAccountPane extends StackPane {
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.getItems().setAll(Accounts.FACTORIES);
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
// try selecting the preferred login type
cboType.getSelectionModel().select(
@@ -108,24 +110,26 @@ public class AddAccountPane extends StackPane {
// remember the last used login type
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
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"), str -> !txtPassword.isVisible() || str.contains("@")));
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !( // consider the opposite situation: input is valid
txtUsername.validate() &&
(!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(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
txtUsername.textProperty(), txtPassword.textProperty(),
loginType, cboServers.getSelectionModel().selectedItemProperty(),
txtPassword.visibleProperty(), cboServers.visibleProperty()));
// authlib-injector links
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
@@ -135,6 +139,20 @@ public class AddAccountPane extends StackPane {
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) {
@@ -197,7 +215,7 @@ public class AddAccountPane extends StackPane {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.ofResult(() -> factory.create(new Selector(), username, password, additionalData))
Task.supplyAsync(() -> factory.create(new Selector(), username, password, additionalData))
.whenComplete(Schedulers.javafx(), account -> {
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
@@ -241,7 +259,7 @@ public class AddAccountPane extends StackPane {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
private class Selector extends BorderPane implements CharacterSelector {
private static class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
@@ -317,12 +335,16 @@ public class AddAccountPane extends StackPane {
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 {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
@@ -28,7 +27,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
@@ -44,7 +43,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
@FXML private StackPane addServerContainer;
@FXML private TransitionPane root;
@FXML private Label lblServerUrl;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@@ -55,8 +54,6 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
@FXML private SpinnerPane nextPane;
@FXML private JFXButton btnAddNext;
private TransitionHandler transitionHandler;
private AuthlibInjectorServer serverBeingAdded;
public AddAuthlibInjectorServerPane(String url) {
@@ -67,8 +64,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
public AddAuthlibInjectorServerPane() {
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
transitionHandler = new TransitionHandler(addServerContainer);
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
@@ -104,19 +100,19 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.of(() -> {
Task.runAsync(() -> {
serverBeingAdded = AuthlibInjectorServer.locateServer(url);
}).whenComplete(Schedulers.javafx(), (isDependentSucceeded, exception) -> {
}).whenComplete(Schedulers.javafx(), exception -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();
if (isDependentSucceeded) {
if (exception == null) {
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
root.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
@@ -127,7 +123,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
@FXML
private void onAddPrev() {
transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
root.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
}
@FXML
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,38 +18,25 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.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.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title"));
public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.injector.manage.title")));
@FXML private ScrollPane scrollPane;
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private ObservableList<AuthlibInjectorServerItem> serverItems;
private final ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
Bindings.bindContent(itemsProperty(), serverItems);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
@@ -57,21 +44,13 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
@Override
public void add() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
public String getTitle() {
return title.get();
}
@Override
public ReadOnlyStringProperty titleProperty() {
return title.getReadOnlyProperty();
}
public void setTitle(String title) {
this.title.set(title);
public ReadOnlyObjectWrapper<State> stateProperty() {
return state;
}
}
@@ -0,0 +1,72 @@
/*
* 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;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.animation;
import javafx.animation.KeyFrame;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -25,4 +26,6 @@ public interface AnimationProducer {
void init(AnimationHandler handler);
List<KeyFrame> animate(AnimationHandler handler);
@Nullable AnimationProducer opposite();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -21,6 +21,8 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
@@ -41,6 +43,7 @@ public enum ContainerAnimations {
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c -> Collections.emptyList()),
/**
* A fade between the old and new view
*/
@@ -62,6 +65,36 @@ public enum ContainerAnimations {
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)))),
/**
* A fade between the old and new view
*/
FADE_IN(c -> {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)))),
/**
* A fade between the old and new view
*/
FADE_OUT(c -> {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)))),
/**
* A zoom effect
*/
@@ -143,6 +176,7 @@ public enum ContainerAnimations {
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))));
private final AnimationProducer animationProducer;
private ContainerAnimations opposite;
ContainerAnimations(Consumer<AnimationHandler> init, Function<AnimationHandler, List<KeyFrame>> animationProducer) {
this.animationProducer = new AnimationProducer() {
@@ -155,10 +189,30 @@ public enum ContainerAnimations {
public List<KeyFrame> animate(AnimationHandler handler) {
return animationProducer.apply(handler);
}
@Override
public @Nullable AnimationProducer opposite() {
return opposite != null ? opposite.getAnimationProducer() : null;
}
};
}
public AnimationProducer getAnimationProducer() {
return animationProducer;
}
public ContainerAnimations getOpposite() {
return opposite;
}
static {
NONE.opposite = NONE;
FADE.opposite = FADE;
SWIPE_LEFT.opposite = SWIPE_RIGHT;
SWIPE_RIGHT.opposite = SWIPE_LEFT;
FADE_IN.opposite = FADE_OUT;
FADE_OUT.opposite = FADE_IN;
ZOOM_IN.opposite = ZOOM_OUT;
ZOOM_OUT.opposite = ZOOM_IN;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -21,29 +21,17 @@ import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
public final class TransitionHandler implements AnimationHandler {
private final StackPane view;
private Timeline animation;
public class TransitionPane extends StackPane implements AnimationHandler {
private Duration duration;
private Node previousNode, currentNode;
/**
* @param view A stack pane that contains another control that is {@link Parent}
*/
public TransitionHandler(StackPane view) {
this.view = view;
currentNode = view.getChildren().stream().findFirst().orElse(null);
// prevent content overflow
Rectangle clip = new Rectangle();
clip.widthProperty().bind(view.widthProperty());
clip.heightProperty().bind(view.heightProperty());
view.setClip(clip);
{
currentNode = getChildren().stream().findFirst().orElse(null);
FXUtils.setOverflowHidden(this);
}
@Override
@@ -58,7 +46,7 @@ public final class TransitionHandler implements AnimationHandler {
@Override
public StackPane getCurrentRoot() {
return view;
return this;
}
@Override
@@ -67,35 +55,30 @@ public final class TransitionHandler implements AnimationHandler {
}
public void setContent(Node newView, AnimationProducer transition) {
setContent(newView, transition, Duration.millis(320));
setContent(newView, transition, Duration.millis(160));
}
public void setContent(Node newView, AnimationProducer transition, Duration duration) {
this.duration = duration;
Timeline prev = animation;
if (prev != null)
prev.stop();
updateContent(newView);
transition.init(this);
// runLater or "init" will not work
Platform.runLater(() -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(transition.animate(this));
nowAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
view.setMouseTransparent(false);
view.getChildren().remove(previousNode);
Timeline newAnimation = new Timeline();
newAnimation.getKeyFrames().addAll(transition.animate(this));
newAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
setMouseTransparent(false);
getChildren().remove(previousNode);
}));
nowAnimation.play();
animation = nowAnimation;
FXUtils.playAnimation(this, "transition_pane", newAnimation);
});
}
private void updateContent(Node newView) {
if (view.getWidth() > 0 && view.getHeight() > 0) {
if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null)
previousNode = EMPTY_PANE;
@@ -105,11 +88,11 @@ public final class TransitionHandler implements AnimationHandler {
if (previousNode == newView)
previousNode = EMPTY_PANE;
view.setMouseTransparent(true);
setMouseTransparent(true);
currentNode = newView;
view.getChildren().setAll(previousNode, currentNode);
getChildren().setAll(previousNode, currentNode);
}
private final StackPane EMPTY_PANE = new StackPane();
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -53,22 +53,37 @@ public class AdvancedListBox extends ScrollPane {
return this;
}
public AdvancedListBox remove(Node child) {
if (child instanceof Pane)
container.getChildren().remove(child);
public AdvancedListBox add(int index, Node child) {
if (child instanceof Pane || child instanceof AdvancedListItem)
container.getChildren().add(index, child);
else {
StackPane pane = null;
for (Node node : container.getChildren())
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
container.getChildren().add(index, pane);
}
return this;
}
public AdvancedListBox remove(Node child) {
container.getChildren().remove(indexOf(child));
return this;
}
public int indexOf(Node child) {
if (child instanceof Pane) {
return container.getChildren().indexOf(child);
} else {
for (int i = 0; i < container.getChildren().size(); ++i) {
Node node = container.getChildren().get(i);
if (node instanceof StackPane) {
ObservableList<Node> list = ((StackPane) node).getChildren();
if (list.size() == 1 && list.get(0) == child)
pane = (StackPane) node;
return i;
}
if (pane == null)
throw new Error();
container.getChildren().remove(pane);
}
return -1;
}
return this;
}
public AdvancedListBox startCategory(String category) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -30,10 +30,12 @@ public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public AdvancedListItem() {
getStyleClass().add("advanced-list-item");
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
@@ -73,6 +75,18 @@ public class AdvancedListItem extends Control {
this.title.set(title);
}
public boolean isActive() {
return active.get();
}
public BooleanProperty activeProperty() {
return active;
}
public void setActive(boolean active) {
this.active.set(active);
}
public String getSubtitle() {
return subtitle.get();
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@@ -30,13 +31,19 @@ import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
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.setPickOnBounds(false);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -34,6 +34,8 @@ import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.VBox;
import java.util.function.Consumer;
@DefaultProperty("content")
public class ComponentList extends Control {
private final StringProperty title = new SimpleStringProperty(this, "title", "Group");
@@ -41,6 +43,8 @@ public class ComponentList extends Control {
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
private boolean hasSubtitle = false;
public final ObservableList<Node> content = FXCollections.observableArrayList();
private boolean expanded = false;
private Consumer<ComponentList> lazyInitializer;
public ComponentList() {
getStyleClass().add("options-list");
@@ -94,6 +98,18 @@ public class ComponentList extends Control {
return content;
}
public void setLazyInitializer(Consumer<ComponentList> lazyInitializer) {
this.lazyInitializer = lazyInitializer;
}
public void onExpand() {
if (!expanded && lazyInitializer != null) {
lazyInitializer.accept(this);
}
expanded = true;
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new Skin(this);
@@ -101,9 +117,11 @@ public class ComponentList extends Control {
protected static class Skin extends SkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
private final ObservableList<Node> list;
private final ObjectBinding<Node> firstItem;
private final ObjectBinding<Node> lastItem;
protected Skin(ComponentList control) {
super(control);
@@ -124,6 +142,16 @@ public class ComponentList extends Control {
if (!list.isEmpty())
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
lastItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
});
if (!list.isEmpty())
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
VBox vbox = new VBox();
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -126,7 +126,7 @@ class ComponentListCell extends StackPane {
VBox container = new VBox();
container.setPadding(new Insets(8, 0, 0, 0));
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container, true);
FXUtils.setOverflowHidden(container);
container.getChildren().setAll(content);
groupNode.setBottom(container);
@@ -143,6 +143,7 @@ class ComponentListCell extends StackPane {
if (isExpanded()) {
updateClip(newHeight);
list.onExpand();
}
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
@@ -161,8 +162,9 @@ class ComponentListCell extends StackPane {
expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(groupNode);
} else
} else {
getChildren().setAll(content);
}
}
public boolean isExpanded() {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -23,7 +23,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
@DefaultProperty("content")
public final class ComponentSublist extends ComponentList {
public class ComponentSublist extends ComponentList {
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -75,7 +75,13 @@ public class FileItem extends BorderPane {
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
*/
private String processPath(String path) {
Path given = Paths.get(path).toAbsolutePath();
Path given;
try {
given = Paths.get(path).toAbsolutePath();
} catch (IllegalArgumentException e) {
return path;
}
if (isConvertToRelativePath()) {
try {
return Paths.get(".").normalize().toAbsolutePath().relativize(given).normalize().toString();
@@ -0,0 +1,59 @@
/*
* 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.construct;
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.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
public abstract class FloatListCell<T> extends ListCell<T> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
protected final StackPane pane = new StackPane();
{
setText(null);
setGraphic(null);
pane.getStyleClass().add("card");
pane.setCursor(Cursor.HAND);
setPadding(new Insets(9, 9, 0, 9));
JFXDepthManager.setDepth(pane, 1);
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
pane.pseudoClassStateChanged(SELECTED, selected);
});
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateControl(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(pane);
}
}
protected abstract void updateControl(T dataItem, boolean empty);
}
@@ -0,0 +1,191 @@
/*
* 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.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.Skin;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.Lang;
public class FloatScrollBarSkin implements Skin<ScrollBar> {
private ScrollBar scrollBar;
private Region group;
private Rectangle track = new Rectangle();
private Rectangle thumb = new Rectangle();
public FloatScrollBarSkin(final ScrollBar scrollBar) {
this.scrollBar = scrollBar;
scrollBar.setPrefHeight(1e-18);
scrollBar.setPrefWidth(1e-18);
this.group = new Region() {
Point2D dragStart;
double preDragThumbPos;
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
{
// Children are added unmanaged because for some reason the height of the bar keeps changing
// if they're managed in certain situations... not sure about the cause.
getChildren().addAll(track, thumb);
track.setManaged(false);
track.getStyleClass().add("track");
thumb.setManaged(false);
thumb.getStyleClass().add("thumb");
scrollBar.orientationProperty().addListener(obs -> setup());
setup();
thumb.setOnMousePressed(me -> {
if (me.isSynthesized()) {
// touch-screen events handled by Scroll handler
me.consume();
return;
}
/*
** if max isn't greater than min then there is nothing to do here
*/
if (getSkinnable().getMax() > getSkinnable().getMin()) {
dragStart = thumb.localToParent(me.getX(), me.getY());
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
me.consume();
}
});
thumb.setOnMouseDragged(me -> {
if (me.isSynthesized()) {
// touch-screen events handled by Scroll handler
me.consume();
return;
}
/*
** if max isn't greater than min then there is nothing to do here
*/
if (getSkinnable().getMax() > getSkinnable().getMin()) {
/*
** if the tracklength isn't greater then do nothing....
*/
if (trackLength() > thumbLength()) {
Point2D cur = thumb.localToParent(me.getX(), me.getY());
if (dragStart == null) {
// 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 position = preDragThumbPos + dragPos / (trackLength() - thumbLength());
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()));
}
}
me.consume();
}
});
}
private double trackLength() {
return getSkinnable().getOrientation() == Orientation.VERTICAL ? track.getHeight() : track.getWidth();
}
private double thumbLength() {
return getSkinnable().getOrientation() == Orientation.VERTICAL ? thumb.getHeight() : thumb.getWidth();
}
private void setup() {
track.widthProperty().unbind();
track.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
track.relocate(0, -5);
track.widthProperty().bind(scrollBar.widthProperty());
track.setHeight(5);
} else {
track.relocate(-5, 0);
track.setWidth(5);
track.heightProperty().bind(scrollBar.heightProperty());
}
thumb.xProperty().unbind();
thumb.yProperty().unbind();
thumb.widthProperty().unbind();
thumb.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
thumb.relocate(0, -5);
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
thumb.setHeight(5);
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
} else {
thumb.relocate(-5, 0);
thumb.setWidth(5);
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
}
}
@Override
protected double computeMaxWidth(double height) {
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
return Double.MAX_VALUE;
}
return 5;
}
@Override
protected double computeMaxHeight(double width) {
if (scrollBar.getOrientation() == Orientation.VERTICAL) {
return Double.MAX_VALUE;
}
return 5;
}
};
}
@Override
public void dispose() {
scrollBar = null;
group = null;
}
@Override
public Node getNode() {
return group;
}
@Override
public ScrollBar getSkinnable() {
return scrollBar;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,50 +19,53 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import java.util.concurrent.CompletableFuture;
public class InputDialogPane extends StackPane {
private final CompletableFuture<String> future = new CompletableFuture<>();
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private JFXTextField textField;
private Label title;
@FXML
private Label content;
private VBox vbox;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
public InputDialogPane(String text, FutureCallback<String> onResult) {
public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
title.setText(text);
JFXTextField textField = new JFXTextField();
textField.setText(initialValue);
vbox.getChildren().setAll(textField);
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner();
onResult.call(textField.getText(), () -> {
acceptPane.hideSpinner();
future.complete(textField.getText());
fireEvent(new DialogCloseEvent());
}, msg -> {
acceptPane.hideSpinner();
lblCreationWarning.setText(msg);
});
});
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> !textField.validate(),
textField.textProperty()
));
}
public void setInitialText(String text) {
textField.setText(text);
public CompletableFuture<String> getCompletableFuture() {
return future;
}
}
@@ -1,137 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S,T> extends TreeTableCell<S,T> {
private final StackPane pane;
private final CheckBox checkBox;
private boolean showLabel;
private ObservableValue<Boolean> booleanProperty;
public JFXCheckBoxTreeTableCell() {
this(null, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
this(getSelectedProperty, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox();
this.pane = new StackPane(checkBox);
this.pane.setAlignment(Pos.CENTER);
setGraphic(null);
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
}
private ObjectProperty<StringConverter<T>> converter =
new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
protected void invalidated() {
updateShowLabel();
}
};
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
@SuppressWarnings("unchecked")
@Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(pane);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTreeTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
}
}
private void updateShowLabel() {
this.showLabel = converter != null;
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
}
private ObservableValue<?> getSelectedProperty() {
return getSelectedStateCallback() != null ?
getSelectedStateCallback().call(getIndex()) :
getTableColumn().getCellObservableValue(getIndex());
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -32,13 +32,12 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public static enum MessageType {
public enum MessageType {
ERROR,
INFORMATION,
WARNING,
QUESTION,
FINE,
PLAIN,
}
@FXML
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -47,7 +47,7 @@ import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiFileItem<T> extends ComponentList {
public class MultiFileItem<T> extends ComponentSublist {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
@@ -148,6 +148,7 @@ public class MultiFileItem<T> extends ComponentList {
pane.setLeft(left);
Label right = new Label(subtitle);
right.setWrapText(true);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,32 +17,35 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
import java.util.Stack;
import java.util.logging.Level;
public class Navigator extends StackPane {
public class Navigator extends TransitionPane {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
private final BooleanProperty backable = new SimpleBooleanProperty(this, "backable");
private final Stack<Node> stack = new Stack<>();
private final TransitionHandler animationHandler = new TransitionHandler(this);
private boolean initialized = false;
public void init(Node init) {
stack.push(init);
backable.set(canGoBack());
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
@@ -50,7 +53,7 @@ public class Navigator extends StackPane {
initialized = true;
}
public void navigate(Node node) {
public void navigate(Node node, AnimationProducer animationProducer) {
FXUtils.checkFxUserThread();
if (!initialized)
@@ -63,15 +66,16 @@ public class Navigator extends StackPane {
Logging.LOG.info("Navigate to " + node);
stack.push(node);
backable.set(canGoBack());
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
node.getProperties().put("hmcl.navigator.animation", animationProducer);
setContent(node, animationProducer);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
node.fireEvent(navigated);
EventHandler<PageCloseEvent> handler = event -> close(node);
@@ -104,16 +108,21 @@ public class Navigator extends StackPane {
Logging.LOG.info("Closed page " + from);
stack.pop();
backable.set(canGoBack());
Node node = stack.peek();
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
setContent(node);
Object obj = from.getProperties().get("hmcl.navigator.animation");
if (obj instanceof AnimationProducer) {
setContent(node, (AnimationProducer) obj);
} else {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
}
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
fireEvent(navigated);
node.fireEvent(navigated);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
@@ -128,12 +137,28 @@ public class Navigator extends StackPane {
return stack.size() > 1;
}
private void setContent(Node content) {
animationHandler.setContent(content, ContainerAnimations.FADE.getAnimationProducer());
public boolean isBackable() {
return backable.get();
}
public BooleanProperty backableProperty() {
return backable;
}
public void setBackable(boolean backable) {
this.backable.set(backable);
}
public int size() {
return stack.size();
}
public void setContent(Node content, AnimationProducer animationProducer) {
super.setContent(content, animationProducer);
if (content instanceof Region) {
((Region) content).setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content, true);
FXUtils.setOverflowHidden((Region) content);
}
}
@@ -179,14 +204,21 @@ public class Navigator extends StackPane {
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
private final Navigator source;
private final Node node;
public NavigationEvent(Object source, Node target, EventType<? extends Event> eventType) {
public NavigationEvent(Navigator source, Node target, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.source = source;
this.node = target;
}
@Override
public Navigator getSource() {
return source;
}
public Node getNode() {
return node;
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -38,6 +38,7 @@ public class PopupMenu extends Control {
private final BooleanProperty alwaysShowingVBar = new SimpleBooleanProperty();
public PopupMenu() {
getStyleClass().add("popup-menu");
}
public ObservableList<Node> getContent() {
@@ -82,7 +83,7 @@ public class PopupMenu extends Control {
.otherwise(ScrollPane.ScrollBarPolicy.AS_NEEDED));
VBox content = new VBox();
content.getStyleClass().add("menu");
content.getStyleClass().add("content");
Bindings.bindContent(content.getChildren(), PopupMenu.this.getContent());
scrollPane.setContent(content);
@@ -0,0 +1,183 @@
/*
* 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.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PromptDialogPane extends StackPane {
private final CompletableFuture<List<Builder.Question<?>>> future = new CompletableFuture<>();
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private VBox vbox;
@FXML
private Label title;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
public PromptDialogPane(Builder builder) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
this.title.setText(builder.title);
List<BooleanBinding> bindings = new ArrayList<>();
for (Builder.Question<?> question : builder.questions) {
if (question instanceof Builder.StringQuestion) {
JFXTextField textField = new JFXTextField();
textField.textProperty().addListener((a, b, newValue) -> ((Builder.StringQuestion) question).value = textField.getText());
textField.setText(((Builder.StringQuestion) question).value);
textField.setValidators(((Builder.StringQuestion) question).validators.toArray(new ValidatorBase[0]));
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
}
VBox.setMargin(textField, new Insets(0, 0, 20, 0));
vbox.getChildren().add(textField);
} else if (question instanceof Builder.BooleanQuestion) {
HBox hBox = new HBox();
JFXCheckBox checkBox = new JFXCheckBox();
hBox.getChildren().setAll(checkBox);
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
checkBox.setText(question.question);
vbox.getChildren().add(hBox);
} else if (question instanceof Builder.CandidatesQuestion) {
HBox hBox = new HBox();
JFXComboBox<String> comboBox = new JFXComboBox<>();
hBox.getChildren().setAll(comboBox);
comboBox.getItems().setAll(((Builder.CandidatesQuestion) question).candidates);
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
((Builder.CandidatesQuestion) question).value = newValue.intValue());
comboBox.getSelectionModel().select(0);
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
}
vbox.getChildren().add(hBox);
}
}
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x),
bindings.toArray(new BooleanBinding[0])
));
acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner();
builder.callback.call(builder.questions, () -> {
acceptPane.hideSpinner();
future.complete(builder.questions);
fireEvent(new DialogCloseEvent());
}, msg -> {
acceptPane.hideSpinner();
lblCreationWarning.setText(msg);
});
});
}
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {
return future;
}
public static class Builder {
private final List<Question<?>> questions = new ArrayList<>();
private final String title;
private final FutureCallback<List<Question<?>>> callback;
public Builder(String title, FutureCallback<List<Question<?>>> callback) {
this.title = title;
this.callback = callback;
}
public <T> Builder addQuestion(Question<T> question) {
questions.add(question);
return this;
}
public static class Question<T> {
public final String question;
protected T value;
public Question(String question) {
this.question = question;
}
public T getValue() {
return value;
}
}
public static class StringQuestion extends Question<String> {
protected final List<ValidatorBase> validators;
public StringQuestion(String question, String defaultValue, ValidatorBase... validators) {
super(question);
this.value = defaultValue;
this.validators = Arrays.asList(validators);
}
}
public static class CandidatesQuestion extends Question<Integer> {
protected final List<String> candidates;
public CandidatesQuestion(String question, String... candidates) {
super(question);
this.value = null;
if (candidates == null || candidates.length == 0) {
throw new IllegalArgumentException("At least one candidate required");
}
this.candidates = new ArrayList<>(Arrays.asList(candidates));
}
}
public static class BooleanQuestion extends Question<Boolean> {
public BooleanQuestion(String question, boolean defaultValue) {
super(question);
this.value = defaultValue;
}
}
}
}
@@ -0,0 +1,29 @@
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;
public class RequiredValidator extends ValidatorBase {
public RequiredValidator() {
}
public RequiredValidator(@NamedArg("message") String message) {
super(message);
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
hasErrors.set(StringUtils.isBlank(textField.getText()));
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -71,6 +71,7 @@ public class RipplerContainer extends StackPane {
setContainer(container);
getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
buttonContainer.getChildren().add(buttonRippler);
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,38 +18,28 @@
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.scene.Node;
import javafx.scene.control.Control;
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.TransitionHandler;
@DefaultProperty("content")
public class SpinnerPane extends StackPane {
private final TransitionHandler transitionHandler = new TransitionHandler(this);
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading") {
protected void invalidated() {
if (get())
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
else
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
};
public SpinnerPane() {
getStyleClass().add("spinner-pane");
getChildren().setAll(contentPane);
content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue));
}
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
public void showSpinner() {
setLoading(true);
@@ -82,4 +72,72 @@ public class SpinnerPane extends StackPane {
public void setLoading(boolean loading) {
this.loading.set(loading);
}
@Override
protected Skin createDefaultSkin() {
return new Skin(this);
}
private static class Skin extends SkinBase<SpinnerPane> {
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;
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));
getChildren().setAll(root);
FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> {
Timeline prev = animation;
if (prev != null) prev.stop();
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();
AnimationHandler handler = new AnimationHandler() {
@Override
public Duration getDuration() {
return Duration.millis(160);
}
@Override
public Pane getCurrentRoot() {
return root;
}
@Override
public Node getPreviousNode() {
return null;
}
@Override
public Node getCurrentNode() {
return topPane;
}
};
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;
});
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -0,0 +1,249 @@
/*
* 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.construct;
import javafx.beans.property.*;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Node;
import javafx.scene.control.SingleSelectionModel;
import java.util.function.Supplier;
public interface TabControl {
ObservableList<Tab> getTabs();
class TabControlSelectionModel extends SingleSelectionModel<Tab> {
private final TabControl tabHeader;
public TabControlSelectionModel(final TabControl t) {
if (t == null) {
throw new NullPointerException("TabPane can not be null");
}
this.tabHeader = t;
// watching for changes to the items list content
final ListChangeListener<Tab> itemsContentObserver = c -> {
while (c.next()) {
for (Tab tab : c.getRemoved()) {
if (tab != null && !tabHeader.getTabs().contains(tab)) {
if (tab.isSelected()) {
tab.setSelected(false);
final int tabIndex = c.getFrom();
// we always try to select the nearest, non-disabled
// tab from the position of the closed tab.
findNearestAvailableTab(tabIndex, true);
}
}
}
if (c.wasAdded() || c.wasRemoved()) {
// The selected tab index can be out of sync with the list of tab if
// we add or remove tabs before the selected tab.
if (getSelectedIndex() != tabHeader.getTabs().indexOf(getSelectedItem())) {
clearAndSelect(tabHeader.getTabs().indexOf(getSelectedItem()));
}
}
}
if (getSelectedIndex() == -1 && getSelectedItem() == null && tabHeader.getTabs().size() > 0) {
// we go looking for the first non-disabled tab, as opposed to
// just selecting the first tab (fix for RT-36908)
findNearestAvailableTab(0, true);
} else if (tabHeader.getTabs().isEmpty()) {
clearSelection();
}
};
if (this.tabHeader.getTabs() != null) {
this.tabHeader.getTabs().addListener(itemsContentObserver);
}
}
// API Implementation
@Override public void select(int index) {
if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) ||
(index == getSelectedIndex() && getModelItem(index).isSelected())) {
return;
}
// Unselect the old tab
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
tabHeader.getTabs().get(getSelectedIndex()).setSelected(false);
}
setSelectedIndex(index);
Tab tab = getModelItem(index);
if (tab != null) {
setSelectedItem(tab);
}
// Select the new tab
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
tabHeader.getTabs().get(getSelectedIndex()).setSelected(true);
}
/* Does this get all the change events */
((Node) tabHeader).notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
}
@Override public void select(Tab tab) {
final int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
final Tab value = getModelItem(i);
if (value != null && value.equals(tab)) {
select(i);
return;
}
}
if (tab != null) {
setSelectedItem(tab);
}
}
@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();
return items == null ? 0 : items.size();
}
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;
while (true) {
// look leftwards
int downPos = tabIndex - i;
if (downPos >= 0) {
Tab _tab = getModelItem(downPos);
if (_tab != null) {
bestTab = _tab;
break;
}
}
// look rightwards. We subtract one as we need
// to take into account that a tab has been removed
// and if we don't do this we'll miss the tab
// to the right of the tab (as it has moved into
// the removed tabs position).
int upPos = tabIndex + i - 1;
if (upPos < tabCount) {
Tab _tab = getModelItem(upPos);
if (_tab != null) {
bestTab = _tab;
break;
}
}
if (downPos < 0 && upPos >= tabCount) {
break;
}
i++;
}
if (doSelect && bestTab != null) {
select(bestTab);
}
return bestTab;
}
}
public static class Tab {
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;
public Tab(String id) {
setId(id);
}
public Tab(String id, String text) {
setId(id);
setText(text);
}
public Supplier<Node> getNodeSupplier() {
return nodeSupplier;
}
public void setNodeSupplier(Supplier<Node> nodeSupplier) {
this.nodeSupplier = nodeSupplier;
}
public String getId() {
return id.get();
}
public StringProperty idProperty() {
return id;
}
public void setId(String id) {
this.id.set(id);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public boolean isSelected() {
return selected.get();
}
public ReadOnlyBooleanProperty selectedProperty() {
return selected.getReadOnlyProperty();
}
private void setSelected(boolean selected) {
this.selected.set(selected);
}
public Node getNode() {
return node.get();
}
public ObjectProperty<Node> nodeProperty() {
return node;
}
public void setNode(Node node) {
this.node.set(node);
}
}
}
@@ -0,0 +1,308 @@
/*
* 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.construct;
import com.jfoenix.controls.JFXRippler;
import javafx.animation.*;
import javafx.application.Platform;
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.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TabHeader extends Control implements TabControl {
public TabHeader(Tab... tabs) {
getStyleClass().setAll("tab-header");
if (tabs != null) {
getTabs().addAll(tabs);
}
}
private ObservableList<Tab> tabs = FXCollections.observableArrayList();
@Override
public ObservableList<Tab> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControlSelectionModel(this));
public SingleSelectionModel<Tab> getSelectionModel() {
return selectionModel.get();
}
public ObjectProperty<SingleSelectionModel<Tab>> selectionModelProperty() {
return selectionModel;
}
public void setSelectionModel(SingleSelectionModel<Tab> selectionModel) {
this.selectionModel.set(selectionModel);
}
@Override
protected Skin<?> createDefaultSkin() {
return new TabHeaderSkin(this);
}
public static class TabHeaderSkin extends SkinBase<TabHeader> {
private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("selected");
private final Color ripplerColor = Color.valueOf("#FFFF8D");
private final HeaderContainer header;
private boolean isSelectingTab = false;
private Tab selectedTab;
protected TabHeaderSkin(TabHeader control) {
super(control);
header = new HeaderContainer();
getChildren().setAll(header);
FXUtils.onChangeAndOperate(control.getSelectionModel().selectedItemProperty(), item -> {
isSelectingTab = true;
selectedTab = item;
Platform.runLater(() -> {
header.setNeedsLayout2(true);
header.layout();
});
});
this.selectedTab = control.getSelectionModel().getSelectedItem();
if (this.selectedTab == null && control.getSelectionModel().getSelectedIndex() != -1) {
control.getSelectionModel().select(control.getSelectionModel().getSelectedIndex());
this.selectedTab = control.getSelectionModel().getSelectedItem();
}
if (this.selectedTab == null) {
control.getSelectionModel().selectFirst();
}
this.selectedTab = control.getSelectionModel().getSelectedItem();
}
protected class HeaderContainer extends StackPane {
private Timeline timeline;
private StackPane selectedTabLine;
private StackPane headersRegion;
private Scale scale = new Scale(1, 1, 0, 0);
private Rotate rotate = new Rotate(0, 0, 1);
private double selectedTabLineOffset;
private ObservableList<Node> binding;
public HeaderContainer() {
getStyleClass().add("tab-header-area");
setPickOnBounds(false);
headersRegion = new StackPane() {
@Override
protected double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
width += child.prefWidth(height);
}
return snapSize(width) + snappedLeftInset() + snappedRightInset();
}
@Override
protected double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
height = Math.max(height, child.prefHeight(width));
}
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
@Override
protected void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
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);
child.relocate(tabStartX, headerHeight - h - snappedBottomInset());
tabStartX += w;
}
selectedTabLine.resizeRelocate(0,
headerHeight - selectedTabLine.prefHeight(-1),
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);
}
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);
}
});
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;
}
offset += tabHeaderPrefWidth;
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
protected class TabHeaderContainer extends StackPane {
private final Tab tab;
private final Label tabText;
private final BorderPane inner;
private final JFXRippler rippler;
public TabHeaderContainer(Tab tab) {
this.tab = tab;
tabText = new Label();
tabText.textProperty().bind(tab.textProperty());
tabText.getStyleClass().add("tab-label");
inner = new BorderPane();
inner.setCenter(tabText);
inner.getStyleClass().add("tab-container");
rippler = new JFXRippler(inner, JFXRippler.RipplerPos.FRONT);
rippler.setRipplerFill(ripplerColor);
getChildren().setAll(rippler);
FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected));
this.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.PRIMARY) {
this.setOpacity(1);
getSkinnable().getSelectionModel().select(tab);
}
});
}
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,14 +18,13 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -38,14 +37,11 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Consumer<Region> onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
@FXML
private JFXProgressBar progressBar;
@FXML
private Label lblTitle;
@FXML
private Label lblSubtitle;
@FXML
private Label lblProgress;
@FXML
private JFXButton btnCancel;
@@ -62,10 +58,24 @@ public class TaskExecutorDialogPane extends StackPane {
onCancel.accept(this);
});
lblProgress.textProperty().bind(Bindings.createStringBinding(
() -> taskListPane.finishedTasksProperty().get() + "/" + taskListPane.totTasksProperty().get(),
taskListPane.finishedTasksProperty(), taskListPane.totTasksProperty()
));
speedEventHandler = speedEvent -> {
String unit = "B/s";
double speed = speedEvent.getSpeed();
if (speed > 1024) {
speed /= 1024;
unit = "KB/s";
}
if (speed > 1024) {
speed /= 1024;
unit = "MB/s";
}
double finalSpeed = speed;
String finalUnit = unit;
Platform.runLater(() -> {
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
});
};
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
}
public void setExecutor(TaskExecutor executor) {
@@ -100,25 +110,6 @@ public class TaskExecutorDialogPane extends StackPane {
lblTitle.setText(currentState);
}
public StringProperty subtitleProperty() {
return lblSubtitle.textProperty();
}
public String getSubtitle() {
return lblSubtitle.getText();
}
public void setSubtitle(String subtitle) {
lblSubtitle.setText(subtitle);
}
public void setProgress(double progress) {
if (progress == Double.MAX_VALUE)
progressBar.setVisible(false);
else
progressBar.setProgress(progress);
}
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -19,32 +19,55 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
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.FabricInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private TaskExecutor executor;
private final AdvancedListBox listBox = new AdvancedListBox();
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
private final List<StageNode> stageNodes = new ArrayList<>();
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
public TaskListPane() {
listBox.setSpacing(0);
@@ -52,43 +75,49 @@ public final class TaskListPane extends StackPane {
getChildren().setAll(listBox);
}
public ReadOnlyIntegerProperty finishedTasksProperty() {
return finishedTasks.getReadOnlyProperty();
}
public ReadOnlyIntegerProperty totTasksProperty() {
return totTasks.getReadOnlyProperty();
}
public void setExecutor(TaskExecutor executor) {
List<String> stages = Lang.removingDuplicates(executor.getStages());
this.executor = executor;
executor.addTaskListener(new TaskListener() {
@Override
public void onStart() {
Platform.runLater(() -> {
stageNodes.clear();
listBox.clear();
finishedTasks.set(0);
totTasks.set(0);
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
stageNodes.forEach(listBox::add);
if (stages.isEmpty()) progressNodePadding.setValue(new Insets(0, 0, 8, 0));
else progressNodePadding.setValue(new Insets(0, 0, 8, 26));
});
}
@Override
public void onReady(Task task) {
Platform.runLater(() -> totTasks.set(totTasks.getValue() + 1));
public void onReady(Task<?> task) {
if (task instanceof Task.StageTask) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
});
}
}
@Override
public void onRunning(Task task) {
if (!task.getSignificance().shouldShow())
public void onRunning(Task<?> task) {
if (!task.getSignificance().shouldShow() || task.getName() == null)
return;
if (task instanceof GameAssetDownloadTask) {
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) {
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")));
} else if (task instanceof OptiFineInstallTask) {
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 CurseCompletionTask) {
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof ModpackInstallTask) {
@@ -101,48 +130,130 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof HMCLModpackExportTask) {
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
}
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
Platform.runLater(() -> listBox.add(node));
}
@Override
public void onFinished(Task task) {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
Platform.runLater(() -> {
listBox.remove(node);
finishedTasks.set(finishedTasks.getValue() + 1);
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
listBox.add(listBox.indexOf(stageNode) + 1, node);
});
}
@Override
public void onFailed(Task task, Throwable throwable) {
public void onFinished(Task<?> task) {
if (task instanceof Task.StageTask) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
});
}
Platform.runLater(() -> {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
listBox.remove(node);
});
}
@Override
public void onFailed(Task<?> task, Throwable throwable) {
if (task instanceof Task.StageTask) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
});
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
Platform.runLater(() -> {
node.setThrowable(throwable);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
@Override
public void onPropertiesUpdate(Map<String, Map<String, Object>> stageProperties) {
stageProperties.forEach((stage, properties) -> {
int count = tryCast(properties.get("count"), Integer.class).orElse(0),
total = tryCast(properties.get("total"), Integer.class).orElse(0);
if (total > 0)
Platform.runLater(() ->
stageNodes.stream().filter(x -> x.stage.equals(stage)).findAny().ifPresent(stageNode -> stageNode.updateCounter(count, total)));
});
}
});
}
private static class ProgressListNode extends BorderPane {
private static class StageNode extends BorderPane {
private final String stage;
private final Label title = new Label();
private final String message;
private boolean started = false;
public StageNode(String stage) {
this.stage = stage;
String stageKey = StringUtils.substringBefore(stage, ':');
String stageValue = StringUtils.substringAfter(stage, ':');
// @formatter:off
switch (stageKey) {
case "hmcl.modpack": message = i18n("install.modpack"); break;
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
case "hmcl.install.assets": message = i18n("assets.download"); break;
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
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;
default: message = i18n(stageKey); break;
}
// @formatter:on
title.setText(message);
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
setPadding(new Insets(0, 0, 8, 4));
setCenter(title);
setLeft(FXUtils.limitingSize(SVG.dotsHorizontal(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void begin() {
if (started) return;
started = true;
setLeft(FXUtils.limitingSize(SVG.arrowRight(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void fail() {
setLeft(FXUtils.limitingSize(SVG.close(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void succeed() {
setLeft(FXUtils.limitingSize(SVG.check(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void updateCounter(int count, int total) {
if (total > 0)
title.setText(String.format("%s - %d/%d", message, count, total));
else
title.setText(message);
}
}
private class ProgressListNode extends BorderPane {
private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label();
private final Label state = new Label();
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
getWidth() - getPadding().getLeft() - getPadding().getRight(),
paddingProperty(), widthProperty());
public ProgressListNode(Task task) {
public ProgressListNode(Task<?> task) {
bar.progressProperty().bind(task.progressProperty());
title.setText(task.getName());
state.textProperty().bind(task.messageProperty());
@@ -151,9 +262,11 @@ public final class TaskListPane extends StackPane {
setRight(state);
setBottom(bar);
bar.minWidthProperty().bind(widthProperty());
bar.prefWidthProperty().bind(widthProperty());
bar.maxWidthProperty().bind(widthProperty());
bar.minWidthProperty().bind(binding);
bar.prefWidthProperty().bind(binding);
bar.maxWidthProperty().bind(binding);
paddingProperty().bind(progressNodePadding);
}
public void unbind() {
Loaded 100 of 523 files, more files were not shown because too many files have changed in this diff. Show more