Compare commits

..
254 Commits
Author SHA1 Message Date
huanghongxun 1fb9fa0af9 fix: icon of selected item should be highlighted. 2021-08-04 00:09:22 +08:00
huanghongxun d35fb9a486 fix: (modpack download): add default category. 2021-08-04 00:08:23 +08:00
Yuhui Huang 37fb9a0d65 feat: (modpack download): category & sort. 2021-08-03 22:47:53 +08:00
Yuhui Huang af7cf393dc feat: download mods and modpacks from CurseForge. 2021-08-03 22:07:19 +08:00
Yuhui Huang 7a20af462b feat: (WIP) Curse mod/modpack download 2021-08-02 23:11:30 +08:00
Yuhui Huang 0939ed819e feat: title bar swipe animation. 2021-08-02 16:46:45 +08:00
Yuhui Huang 5da4ad7a62 feat: refine GameListPage UI. 2021-08-02 16:16:21 +08:00
Yuhui Huang 123c863e3d feat: refine profile list UI. 2021-08-02 16:05:37 +08:00
Yuhui Huang 6fc0b13dc7 feat: refine UI icons. 2021-08-02 15:28:57 +08:00
Yuhui Huang 59bf8d3ebc style: fix checkstyle. 2021-08-02 15:09:34 +08:00
Yuhui Huang 805393b184 fix: GameListItem launch icon -> launchOutline icon. 2021-08-02 15:07:31 +08:00
Yuhui Huang 5d0d77aff4 style: fix checkstyle. 2021-08-02 15:03:39 +08:00
Yuhui Huang d2e4ddc876 fix: inconsistent svg icon size. 2021-08-02 15:02:54 +08:00
huanghongxun 5b7ca8c751 feat: redesign UI of game management. 2021-08-02 13:36:36 +08:00
huanghongxun 16c7558497 fix: active tab indicator animation aligned to Material Design standard. 2021-07-31 23:23:07 +08:00
huanghongxun 9b6b3f8938 fix: no longer validate Forge dependencies since provided checksums are wrong. Closes #913. 2021-07-31 02:10:04 +08:00
huanghongxun 331d8d1bb6 fix: does not remove the old library when updating the library 2021-07-31 02:02:06 +08:00
huanghongxun e5cca24740 fix: Version.jar becomes wrong value after resolvePreservingPatches 2021-07-31 02:01:17 +08:00
huanghongxun 6f40335322 feat: print more effective logs when OptiFine Patcher failed to patch game. 2021-07-31 02:00:13 +08:00
huanghongxun b6de9d4ea2 fix: failed to duplicate version when no jar 2021-07-31 00:56:26 +08:00
Yuhui Huang 5f8f01e0ab Revert "fix: Version.jar should be resolved to id of self if null instead of the id of ancestor.". Closes #934.
This reverts commit 5de150b06e.
2021-07-28 16:15:11 +08:00
xtex 0d69e944f6 fix: #930(Search new Java registry location first) 2021-07-25 17:29:44 +08:00
huanghongxun 1dc4e3f730 fix: 1.17 Forge auto installation 2021-07-25 17:29:00 +08:00
huanghongxun 14d1bccd14 fix: update condition check for whether the game is modded. 2021-07-25 15:58:22 +08:00
huanghongxun 5de150b06e fix: Version.jar should be resolved to id of self if null instead of the id of ancestor. 2021-07-25 01:21:08 +08:00
huanghongxun e34dd39b9a fix: malformed locale name. Closes #925. 2021-07-24 14:56:18 +08:00
Haowei Wen e7d5faeeac fix: DirectoryStream not closed
https://github.com/huanghongxun/HMCL/commit/dc3167472175d023878f9ae32230b93964588bc1#r53347194
2021-07-12 13:19:16 +08:00
huanghongxun 46a7269087 chore: ignore hmcl.json 2021-07-10 23:25:26 +08:00
huanghongxun dc31674721 fix: Do not throw exception when path to cached jvm installations does not exist. 2021-07-10 23:24:31 +08:00
Glavo afe6bc7456 用户 JVM 参数优先于默认参数 (#903)
* close #885: use the GC selected by user by default

* fix: default JVM args conflict with user args

* Options take precedence over JVM args

* compile huge methods by default

* update settings.advanced.jvm_args text

* Using JVM args suppression options

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

* Automatically add pack200 to class path

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

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

No files matched your search

+11 -2
View File
@@ -1,6 +1,6 @@
name: Java CI
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -8,11 +8,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
java-package: jdk+fx
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
run: ./gradlew checkstyleTest
- name: Build with Gradle
run: ./gradlew build
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: HMCL
path: HMCL/build/libs
+12 -2
View File
@@ -11,23 +11,25 @@ hs_err_pid*
*.log
.mine*
NVIDIA
*.json
# gradle build
/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
@@ -35,4 +37,12 @@ NVIDIA
# netbeans
.nb-gradle
*.exe
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
# test
/hmcl.json
/.hmcl.json
+81 -22
View File
@@ -1,6 +1,17 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
}
}
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
@@ -10,25 +21,28 @@ import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
import org.glavo.pack200.Pack200
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
def dev = null
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.3"
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 +83,73 @@ 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 {
if(JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
}
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
sourceCompatibility = 11
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,
'Class-Path': 'pack200.jar',
'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)
}
@@ -117,8 +169,9 @@ processResources {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
javaexec {
main = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
@@ -126,6 +179,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,6 +198,7 @@ task makePack(dependsOn: jar) {
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
// 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)
}
@@ -23,38 +23,46 @@ 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.io.UnsupportedEncodingException;
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()));
}
@@ -65,6 +73,8 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
initIcon();
UpdateChecker.init();
primaryStage.show();
@@ -74,7 +84,23 @@ 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));
@@ -106,7 +132,6 @@ public final class Launcher extends Application {
Schedulers.shutdown();
Controllers.shutdown();
Platform.exit();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
@@ -135,6 +160,16 @@ public final class Launcher extends Application {
} catch (URISyntaxException e) {
return null;
}
} else {
try {
File jarFile = new File(URLDecoder.decode(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
} catch (UnsupportedEncodingException e) {
LOG.log(Level.WARNING, "Failed to decode jar path", e);
return null;
}
}
if (result.isEmpty())
return null;
@@ -17,8 +17,8 @@
*/
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.*;
import javax.swing.*;
@@ -43,12 +43,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
private Main() {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
checkJavaFX();
checkDirectoryPath();
// This environment check will take ~300ms
@@ -59,9 +61,10 @@ public final class Main {
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);
}
@@ -77,9 +80,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"));
}
}
@@ -128,7 +135,10 @@ public final class Main {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
defaultKeyStore.load(Files.newInputStream(ksPath), "changeit".toCharArray());
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");
@@ -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.
*/
@@ -39,5 +40,14 @@ public final class Metadata {
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");
}
}
@@ -21,8 +21,16 @@ 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
@@ -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);
}
}
@@ -19,12 +19,24 @@ 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.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;
@@ -32,6 +44,7 @@ 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 {
@@ -62,10 +75,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
@Override
public File getRunDirectory(String id) {
switch (getGameDirectoryType(id)) {
case VERSION_FOLDER: return getVersionRoot(id);
case ROOT_FOLDER: return super.getRunDirectory(id);
case CUSTOM: return new File(getVersionSetting(id).getGameDir());
default: throw new Error();
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();
}
}
@@ -74,6 +93,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
localVersionSettings.clear();
super.refreshVersionsImpl();
versions.keySet().forEach(this::loadLocalVersionSetting);
versions.keySet().forEach(version -> {
if (isModpack(version)) {
specializeVersionSetting(version);
}
});
try {
File file = new File(getBaseDirectory(), "launcher_profiles.json");
@@ -105,7 +129,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (dstDir.exists()) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
}
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -211,10 +237,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (version.getMainClass() != null &&
("net.minecraft.launchwrapper.Launch".equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| "cpw.mods.modlauncher.Launcher".equals(version.getMainClass())))
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
@@ -261,6 +284,56 @@ 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())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir());
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");
@@ -1,74 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.ModAdviser;
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.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends Task<Void> {
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<>(ModAdviser.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 -> Modpack.acceptFile(path, blackList, whitelist));
Version mv = repository.getResolvedPreservingPatchesVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IOException("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.
}
}
}
@@ -77,6 +77,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
} 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
@@ -103,8 +104,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
}
dependencies.add(libraryTask.thenComposeAsync(repository::save));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
@Override
@@ -31,15 +31,17 @@ import java.nio.file.Path;
* @author huangyuhui
*/
public final class HMCLModpackManager {
private HMCLModpackManager() {
}
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
@@ -23,9 +23,12 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
import org.jackhuang.hmcl.launch.ProcessCreationException;
@@ -33,14 +36,15 @@ 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;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
@@ -49,20 +53,22 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.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.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.RejectedExecutionException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -91,7 +97,6 @@ public final class LauncherHelper {
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
private CountDownLatch launchingLatch;
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
@@ -108,9 +113,9 @@ public final class LauncherHelper {
try {
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().execute(this::launch0);
Schedulers.defaultScheduler().execute(this::launch0);
});
} catch (InterruptedException ignore) {
} catch (InterruptedException | RejectedExecutionException ignore) {
}
});
}
@@ -126,26 +131,34 @@ public final class LauncherHelper {
DefaultDependencyManager dependencyManager = profile.getDependency();
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.allOf(
Task.composeAsync(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version, repository.unmarkVersionLaunchedAbnormally(selectedVersion));
}), Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if ("Server".equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
})).withStage("launch.state.dependencies")
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);
})
.thenComposeAsync(Task.supplyAsync(() -> {
try {
return account.logIn();
@@ -162,10 +175,10 @@ public final class LauncherHelper {
repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo,
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
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, gameVersion.isPresent())
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
@@ -192,15 +205,14 @@ public final class LauncherHelper {
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}
}).thenRunAsync(Schedulers.defaultScheduler(), () -> {
launchingLatch = new CountDownLatch(1);
}).thenRunAsync(() -> {
launchingLatch.await();
}).withStage("launch.state.waiting_launching"))
.withStagesHint(Lang.immutableListOf(
"launch.state.dependencies",
"launch.state.logging_in",
"launch.state.waiting_launching"))
.cancellableExecutor();
.executor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
@@ -239,6 +251,20 @@ public final class LauncherHelper {
} 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) {
@@ -292,6 +318,32 @@ 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.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), () -> {
downloadJava(version.getJavaVersion(), profile)
.thenAcceptAsync(x -> {
try {
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
newAcceptableJava.ifPresent(setting::setJavaVersion);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Cannot list javas", e);
}
}, Platform::runLater).thenAccept(x -> onAccept.run());
}, null);
flag = true;
}
}
}
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
@@ -314,8 +366,8 @@ 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_AND_LATER
&& version.getMainClass().contains("launchwrapper")
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
@@ -347,7 +399,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.
@@ -383,13 +435,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;
}
@@ -401,7 +453,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;
}
}
@@ -410,6 +474,36 @@ public final class LauncherHelper {
onAccept.run();
}
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<Void> future = new CompletableFuture<>();
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
profile.getDependency().getDownloadProvider()).executor(false);
executor.addTaskListener(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
} else {
future.complete(null);
}
});
}
});
javaDownloadingPane.setExecutor(executor, true);
Controllers.dialog(javaDownloadingPane);
executor.start();
return future;
}
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
@@ -449,11 +543,13 @@ public final class LauncherHelper {
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(HMCLGameRepository repository, String version, AuthInfo authInfo, boolean detectWindow) {
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)
@@ -482,7 +578,7 @@ public final class LauncherHelper {
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
latch.countDown();
logWindowLatch.countDown();
});
}
@@ -502,9 +598,7 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
Platform.runLater(() -> {
launchingLatch.countDown();
});
Platform.runLater(launchingLatch::countDown);
break;
case HIDE:
launchingLatch.countDown();
@@ -539,7 +633,7 @@ public final class LauncherHelper {
if (showLogs) {
try {
latch.await();
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
@@ -548,7 +642,7 @@ public final class LauncherHelper {
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || !detectWindow)) {
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
lwjgl = true;
finishLaunch();
}
@@ -23,6 +23,8 @@ import org.jackhuang.hmcl.mod.*;
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;
@@ -43,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) {
@@ -148,15 +158,28 @@ 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)
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, ((ServerModpackManifest) modpack.getManifest()), name)
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<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);
@@ -176,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();
}
@@ -218,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,46 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.Objects;
public final class MultiMCInstallVersionSettingTask extends Task<Void> {
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;
setExecutor(Schedulers.javafx());
}
@Override
public void execute() {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
}
}
@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
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.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
@@ -35,7 +37,6 @@ import java.awt.*;
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;
@@ -134,6 +135,7 @@ 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);
@@ -142,8 +144,8 @@ public final class TexturesLoader {
private static void loadDefaultSkin(String path, TextureModel model) {
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);
}
}
@@ -181,6 +183,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 ====
@@ -205,16 +231,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;
}
}
@@ -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);
}
@@ -206,6 +231,10 @@ public final class Accounts {
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
triggerAuthlibInjectorUpdateCheck();
}
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@@ -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))
@@ -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();
}
@@ -62,6 +62,9 @@ public final class ConfigHolder {
}
configLocation = locateConfig();
LOG.log(Level.INFO, "Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
@@ -88,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;
@@ -97,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 {
@@ -17,19 +17,14 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
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.task.Schedulers;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -40,39 +35,61 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public final class DownloadProviders {
private DownloadProviders() {}
public static final Map<String, DownloadProvider> providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider("https://bmclapi2.bangbang93.com")),
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
public static final Map<String, DownloadProvider> providersById;
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
private static ObjectBinding<DownloadProvider> downloadProviderProperty;
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 String getPrimaryDownloadProviderId() {
String downloadType = config().getDownloadType();
if (providersById.containsKey(downloadType))
return downloadType;
else
return DEFAULT_PROVIDER_ID;
}
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 DownloadProvider getDownloadProvider() {
return downloadProviderProperty.get();
}
/**
* Preferred download providers have the primary one first, the official one next.
* @return the preferred download providers
*/
public static List<DownloadProvider> getPreferredDownloadProviders() {
DownloadProvider provider = getDownloadProvider();
return Stream.concat(Stream.of(provider), providersById.values().stream().filter(x -> x != provider)).collect(Collectors.toList());
}
public static ObservableObjectValue<DownloadProvider> downloadProviderProperty() {
return downloadProviderProperty;
public static AdaptedDownloadProvider getDownloadProvider() {
return DOWNLOAD_PROVIDER;
}
}
@@ -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,7 +163,11 @@ public final class Profile implements Observable {
}
public DefaultDependencyManager getDependency() {
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), DownloadProviders.getPreferredDownloadProviders(), HMCLCacheRepository.REPOSITORY);
return getDependency(DownloadProviders.getDownloadProvider());
}
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
return new DefaultDependencyManager(repository, downloadProvider, HMCLCacheRepository.REPOSITORY);
}
public VersionSetting getVersionSetting(String id) {
@@ -203,6 +208,24 @@ public final class Profile implements Observable {
Platform.runLater(observableHelper::invalidate);
}
public static class ProfileVersion {
private final Profile profile;
private final String version;
public ProfileVersion(Profile profile, String version) {
this.profile = profile;
this.version = version;
}
public Profile getProfile() {
return profile;
}
public String getVersion() {
return version;
}
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
@@ -23,9 +23,11 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class Settings {
public final class Settings {
private static Settings instance;
@@ -44,6 +46,9 @@ public class Settings {
}
private Settings() {
config().localizationProperty().addListener(unused -> updateSystemLocale());
updateSystemLocale();
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
@@ -74,4 +79,8 @@ public class Settings {
return null;
}
}
private static void updateSystemLocale() {
Locale.setDefault(config().getLocalization().getLocale());
}
}
@@ -87,9 +87,10 @@ public class Theme {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%base-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("%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);
@@ -21,16 +21,14 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.NativesDirectoryType;
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;
@@ -39,8 +37,6 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
*
* @author huangyuhui
@@ -48,8 +44,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public transient String id;
private boolean global = false;
public boolean isGlobal() {
@@ -71,7 +65,7 @@ public final class VersionSetting implements Cloneable {
* 1. Global settings.
* 2. Version settings.
* If a version claims that it uses global settings, its version setting will be disabled.
*
* <p>
* Defaults false because if one version uses global first, custom version file will not be generated.
*/
public boolean isUsesGlobal() {
@@ -124,6 +118,39 @@ public final class VersionSetting implements Cloneable {
defaultJavaPathProperty.set(defaultJavaPath);
}
/**
* 0 - .minecraft/versions/&lt;version&gt;/natives/<br/>
*/
private final ObjectProperty<NativesDirectoryType> nativesDirTypeProperty = new SimpleObjectProperty<>(this, "nativesDirType", NativesDirectoryType.VERSION_FOLDER);
public ObjectProperty<NativesDirectoryType> nativesDirTypeProperty() {
return nativesDirTypeProperty;
}
public NativesDirectoryType getNativesDirType() {
return nativesDirTypeProperty.get();
}
public void setNativesDirType(NativesDirectoryType nativesDirType) {
nativesDirTypeProperty.set(nativesDirType);
}
// Path to lwjgl natives directory
private final StringProperty nativesDirProperty = new SimpleStringProperty(this, "nativesDirProperty", "");
public StringProperty nativesDirProperty() {
return nativesDirProperty;
}
public String getNativesDir() {
return nativesDirProperty.get();
}
public void setNativesDir(String nativesDir) {
nativesDirProperty.set(nativesDir);
}
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public StringProperty javaDirProperty() {
@@ -341,7 +368,7 @@ public final class VersionSetting implements Cloneable {
/**
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* <p>
* Format: ip:port or without port.
*/
public String getServerIp() {
@@ -378,7 +405,7 @@ public final class VersionSetting implements Cloneable {
/**
* The width of Minecraft window, defaults 800.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -400,7 +427,7 @@ public final class VersionSetting implements Cloneable {
/**
* The height of Minecraft window, defaults 480.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -531,37 +558,8 @@ public final class VersionSetting implements Cloneable {
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir, boolean checkJava) throws InterruptedException {
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(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();
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
}
@Override
@@ -589,6 +587,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
return versionSetting;
}
@@ -620,6 +619,8 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
return obj;
}
@@ -649,6 +650,7 @@ public final class VersionSetting implements Cloneable {
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
@@ -657,6 +659,7 @@ public final class VersionSetting implements Cloneable {
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
return vs;
}
@@ -0,0 +1,20 @@
package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public final class AwtUtils {
private 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) {
}
}
}
@@ -17,16 +17,24 @@
*/
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.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.download.java.JavaRepository;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -34,13 +42,19 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -50,13 +64,35 @@ 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 VersionPage versionPage = null;
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
private static AuthlibInjectorServersPage serversPage = null;
private static RootPage rootPage;
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<ModDownloadListPage> modDownloadListPage = new Lazy<>(() -> {
return new ModDownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
{
state.set(State.fromTitle(i18n("modpack.download")));
}
};
});
private Controllers() {
}
public static Scene getScene() {
return scene;
@@ -68,16 +104,17 @@ public final class Controllers {
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
return versionPage;
return versionPage.get();
}
// FXThread
public static GameListPage getGameListPage() {
return gameListPage.get();
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
return rootPage.get();
}
// FXThread
@@ -87,16 +124,31 @@ public final class Controllers {
return serversPage;
}
// FXThread
public static ModDownloadListPage getModpackDownloadListPage() {
return modDownloadListPage.get();
}
// FXThread
public static DecoratorController getDecorator() {
return decorator;
}
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());
@@ -108,15 +160,19 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).start();
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
scene = new Scene(decorator.getDecorator(), 800, 480);
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(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
}
@@ -142,7 +198,11 @@ public final class Controllers {
}
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
confirm(text, title, MessageType.QUESTION, onAccept, onCancel);
}
public static void confirm(String text, String title, MessageType type, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, type, onAccept, onCancel));
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
@@ -185,6 +245,8 @@ public final class Controllers {
rootPage = null;
versionPage = null;
serversPage = null;
gameListPage = null;
modDownloadListPage = null;
decorator = null;
stage = null;
scene = null;
@@ -31,6 +31,8 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
private DialogController() {
}
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
@@ -38,8 +40,8 @@ public final class DialogController {
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
res.set(it);
latch.countDown();
}, latch::countDown);
Controllers.dialog(pane);
});
@@ -18,15 +18,19 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.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;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.beans.value.WritableValue;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -34,8 +38,6 @@ 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;
@@ -43,7 +45,6 @@ import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -61,12 +62,9 @@ import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -116,6 +114,15 @@ public final class FXUtils {
return onWeakChange(value, consumer);
}
public static WeakInvalidationListener observeWeak(Runnable runnable, Observable... observables) {
WeakInvalidationListener listener = new WeakInvalidationListener(observable -> runnable.run());
for (Observable observable : observables) {
observable.addListener(listener);
}
runnable.run();
return listener;
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();
@@ -434,6 +441,7 @@ public final class FXUtils {
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
* @see #bindEnum(JFXComboBox, Property)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
@@ -443,72 +451,6 @@ 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(Lang.clamp(0, bar.getValue() + dy / height, 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.
@@ -19,95 +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 org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
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 libraryId, String libraryVersion, @Nullable Runnable upgrade, @Nullable 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());
}
String[] urls = new String[]{"/assets/img/grass.png", "/assets/img/fabric.png", "/assets/img/forge.png", "/assets/img/chicken.png", "/assets/img/command.png"};
String[] libraryIds = new String[]{"game", "fabric", "forge", "liteloader", "optifine"};
public InstallerItem(String id) {
this.id = id;
boolean regularLibrary = false;
for (int i = 0; i < 5; ++i) {
if (libraryIds[i].equals(libraryId)) {
setLeft(FXUtils.limitingSize(new ImageView(new Image(urls[i], 32, 32, true, true)), 32, 32));
Label label = new Label();
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
if (libraryVersion == null) {
label.setText(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
} else {
label.setText(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + libraryVersion);
}
setCenter(label);
regularLibrary = true;
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;
}
}
if (!regularLibrary) {
String title = I18n.hasKey("install.installer." + libraryId) ? i18n("install.installer." + libraryId) : libraryId;
if (libraryVersion != null) {
TwoLineListItem item = new TwoLineListItem();
item.setTitle(title);
item.setSubtitle(i18n("archive.version") + ": " + libraryVersion);
setCenter(item);
} else {
Label label = new Label();
label.setStyle("-fx-font-size: 15px;");
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
setCenter(label);
}
}
{
HBox hBox = new HBox();
if (upgrade != null) {
JFXButton upgradeButton = new JFXButton();
if (libraryVersion == null) {
upgradeButton.setGraphic(SVG.arrowRight(Theme.blackFillBinding(), -1, -1));
} else {
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());
hBox.getChildren().add(upgradeButton);
}
if (deleteCallback != null) {
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
deleteButton.getStyleClass().add("toggle-icon4");
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
hBox.getChildren().add(deleteButton);
}
hBox.setAlignment(Pos.CENTER_RIGHT);
setRight(hBox);
}
}
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 final InstallerItem game = new InstallerItem(MINECRAFT);
public final InstallerItem fabric = new InstallerItem(FABRIC);
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
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);
}
});
}
}
}
@@ -40,7 +40,6 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
VBox list = new VBox();
spinnerPane.getStyleClass().add("content-background");
StackPane contentPane = new StackPane();
{
@@ -62,10 +61,9 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
VBox vBox = new VBox();
{
vBox.getStyleClass().add("card-list");
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
vBox.setPadding(new Insets(15));
vBox.setSpacing(15);
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
@@ -150,6 +150,8 @@ public final class LogWindow extends Stage {
private JFXComboBox<String> cboLines = new JFXComboBox<>();
LogWindowImpl() {
getStyleClass().add("log-window");
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000");
for (String i : cboLines.getItems())
@@ -178,6 +180,7 @@ public final class LogWindow extends Stage {
}
private void onClear() {
impl.listView.getItems().clear();
logs.clear();
}
@@ -195,7 +198,7 @@ public final class LogWindow extends Stage {
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException ignored) {
} catch (IOException | IllegalArgumentException ignored) {
}
}
});
@@ -279,7 +282,6 @@ public final class LogWindow extends Stage {
prefWidthProperty().bind(clippedContainer.widthProperty());
}
setPadding(new Insets(2));
getStyleClass().add("log");
setWrapText(true);
setGraphic(null);
}
@@ -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);
@@ -46,7 +46,7 @@ public final class SVG {
}
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
double scale = Math.min(width / 24, height / 24);
svg.setScaleX(scale);
svg.setScaleY(scale);
@@ -59,6 +59,10 @@ public final class SVG {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
}
public static Node gearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z", fill, width, height);
}
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
}
@@ -83,6 +87,10 @@ public final class SVG {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
public static Node deleteOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z", fill, width, height);
}
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
}
@@ -103,7 +111,7 @@ public final class SVG {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
}
public static Node launch2(ObjectBinding<? extends Paint> fill, double width, double height) {
public static Node launchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
@@ -115,6 +123,10 @@ public final class SVG {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
}
public static Node pencilOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z", fill, width, height);
}
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
}
@@ -123,6 +135,10 @@ public final class SVG {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
}
public static Node folderOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z", fill, width, height);
}
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
}
@@ -151,6 +167,10 @@ public final class SVG {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
public static Node plusCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z", fill, width, height);
}
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height);
}
@@ -186,4 +206,56 @@ public final class SVG {
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 wrenchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z", fill, width, height);
}
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
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);
}
public static Node puzzle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z", fill, width, height);
}
public static Node cube(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z", fill, width, height);
}
public static Node pack(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z", fill, width, height);
}
public static Node gamepad(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z", fill, width, height);
}
public static Node fire(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z", fill, width, height);
}
public static Node monitorScreenshot(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2", fill, width, height);
}
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z", fill, width, height);
}
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", fill, width, height);
}
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
}
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", fill, width, height);
}
}
@@ -70,7 +70,13 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
root.setCenter(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
FXUtils.onChangeAndOperate(skinnable.loadingProperty(), loading -> {
if (loading) {
spinnerPane.showSpinner();
} else {
spinnerPane.hideSpinner();
}
});
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
@@ -93,5 +99,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);
}
@@ -22,6 +22,7 @@ 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;
@@ -29,8 +30,6 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class UpgradeDialog extends JFXDialogLayout {
private final WebView webView = new WebView();
public UpgradeDialog(Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
@@ -38,6 +37,7 @@ public class UpgradeDialog extends JFXDialogLayout {
{
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) -> {
@@ -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.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(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() {
@@ -21,22 +21,25 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.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 org.jackhuang.hmcl.util.Pair;
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 final ImageView imageView;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@@ -47,23 +50,26 @@ public class AccountAdvancedListItem extends AdvancedListItem {
titleProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(newImage("/assets/img/craft_table.png"));
imageView.imageProperty().unbind();
imageView.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() + " " + accountSubtitle(account));
imageView.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);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
setOnScroll(event -> {
Account current = account.get();
if (current == null) return;
@@ -83,11 +89,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 "";
}
}
}
@@ -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);
}
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
@@ -32,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;
@@ -85,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());
@@ -102,7 +132,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
root.setRight(right);
root.getStyleClass().add("card");
root.setPadding(new Insets(8, 8, 8, 0));
root.setStyle("-fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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;
@@ -85,10 +87,10 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboServers.getItems().addListener(onInvalidating(this::resetServerSelection));
resetServerSelection();
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) {
@@ -156,12 +174,13 @@ public class AddAccountPane extends StackPane {
return unmodifiableList(result);
}
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
private void resetServerSelection() {
if (!cboServers.getItems().isEmpty()) {
Platform.runLater(() -> {
// the selection will not be updated as expected
// if we call it immediately
cboServers.getSelectionModel().selectFirst();
});
}
}
@@ -241,7 +260,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 +336,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) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -129,7 +129,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
@FXML
private void onAddFinish() {
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
config().getAuthlibInjectorServers().add(serverBeingAdded);
config().getAuthlibInjectorServers().add(0, serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
@@ -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;
}
}
@@ -23,7 +23,10 @@ import javafx.util.Duration;
public interface AnimationHandler {
Duration getDuration();
Pane getCurrentRoot();
Node getPreviousNode();
Node getCurrentNode();
}
@@ -173,7 +173,49 @@ public enum ContainerAnimations {
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))));
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
SWIPE_LEFT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
SWIPE_RIGHT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))));
private final AnimationProducer animationProducer;
private ContainerAnimations opposite;
@@ -37,7 +37,6 @@ public class AdvancedListBox extends ScrollPane {
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.setSpacing(5);
container.getStyleClass().add("advanced-list-box-content");
}
@@ -24,10 +24,16 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.util.Pair.pair;
public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> leftGraphic = new SimpleObjectProperty<>(this, "leftGraphic");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
@@ -39,16 +45,16 @@ public class AdvancedListItem extends Control {
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
public Image getImage() {
return image.get();
public Node getLeftGraphic() {
return leftGraphic.get();
}
public ObjectProperty<Image> imageProperty() {
return image;
public ObjectProperty<Node> leftGraphicProperty() {
return leftGraphic;
}
public void setImage(Image image) {
this.image.set(image);
public void setLeftGraphic(Node leftGraphic) {
this.leftGraphic.set(leftGraphic);
}
public Node getRightGraphic() {
@@ -134,4 +140,17 @@ public class AdvancedListItem extends Control {
protected Skin<?> createDefaultSkin() {
return new AdvancedListItemSkin(this);
}
public static Pair<Node, ImageView> createImageView(Image image) {
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.setImage(image);
imageViewContainer.getChildren().setAll(imageView);
return pair(imageViewContainer, imageView);
}
}
@@ -18,16 +18,13 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
@@ -48,35 +45,22 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
root.setPickOnBounds(false);
HBox left = new HBox();
left.setAlignment(Pos.CENTER);
left.setAlignment(Pos.CENTER_LEFT);
left.setMouseTransparent(true);
StackPane imageViewContainer = new StackPane();
FXUtils.setLimitWidth(imageViewContainer, 32);
FXUtils.setLimitHeight(imageViewContainer, 32);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.setPreserveRatio(true);
imageView.imageProperty().bind(skinnable.imageProperty());
imageViewContainer.getChildren().setAll(imageView);
VBox vbox = new VBox();
root.setCenter(vbox);
vbox.setMouseTransparent(true);
vbox.setAlignment(Pos.CENTER_LEFT);
vbox.setPadding(new Insets(0, 0, 0, 10));
Label title = new Label();
title.textProperty().bind(skinnable.titleProperty());
title.setMaxWidth(90);
title.setStyle("-fx-font-size: 15;");
title.setTextAlignment(TextAlignment.JUSTIFY);
title.getStyleClass().add("title");
vbox.getChildren().add(title);
Label subtitle = new Label();
subtitle.textProperty().bind(skinnable.subtitleProperty());
subtitle.setMaxWidth(90);
subtitle.setStyle("-fx-font-size: 10;");
subtitle.setTextAlignment(TextAlignment.JUSTIFY);
subtitle.getStyleClass().add("subtitle");
vbox.getChildren().add(subtitle);
FXUtils.onChangeAndOperate(skinnable.subtitleProperty(), subtitleString -> {
@@ -84,7 +68,14 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
else vbox.getChildren().setAll(title, subtitle);
});
left.getChildren().setAll(imageViewContainer, vbox);
FXUtils.onChangeAndOperate(skinnable.leftGraphicProperty(),
newGraphic -> {
if (newGraphic == null) {
left.getChildren().clear();
} else {
left.getChildren().setAll(newGraphic);
}
});
root.setLeft(left);
HBox right = new HBox();
@@ -100,13 +91,10 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
right.getChildren().setAll(newGraphic);
}
});
root.setRight(right);
FXUtils.onChangeAndOperate(skinnable.actionButtonVisibleProperty(),
visible -> root.setRight(visible ? right : null));
stackPane.setStyle("-fx-padding: 10 16 10 16;");
stackPane.getStyleClass().add("transparent");
stackPane.setPickOnBounds(false);
stackPane.getChildren().setAll(root);
@@ -80,7 +80,7 @@ class ComponentListCell extends StackPane {
BorderPane groupNode = new BorderPane();
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
@@ -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();
@@ -36,8 +36,7 @@ public abstract class FloatListCell<T> extends ListCell<T> {
pane.getStyleClass().add("card");
pane.setCursor(Cursor.HAND);
pane.setPadding(new Insets(8));
setPadding(new Insets(5));
setPadding(new Insets(9, 9, 0, 9));
JFXDepthManager.setDepth(pane, 1);
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
@@ -99,9 +99,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
// we're getting dragged without getting a mouse press
dragStart = thumb.localToParent(me.getX(), me.getY());
}
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY() : cur.getX() - dragStart.getX();
double position = preDragThumbPos + dragPos / (trackLength() - thumbLength());
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus();
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable())
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
@@ -121,10 +122,6 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return getSkinnable().getOrientation() == Orientation.VERTICAL ? thumb.getHeight() : thumb.getWidth();
}
private double boundedSize(double min, double value, double max) {
return Math.min(Math.max(value, min), Math.max(min, max));
}
private void setup() {
track.widthProperty().unbind();
track.heightProperty().unbind();
@@ -146,13 +143,13 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
thumb.relocate(0, -5);
thumb.widthProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
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(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
}
}
@@ -54,12 +54,12 @@ public final class ImagePickerItem extends BorderPane {
imageView.setPreserveRatio(true);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 20, 20));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 15, 15));
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 20, 20));
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
@@ -38,7 +38,6 @@ public final class MessageDialogPane extends StackPane {
WARNING,
QUESTION,
FINE,
PLAIN,
}
@FXML
@@ -89,8 +88,8 @@ public final class MessageDialogPane extends StackPane {
}
}
public MessageDialogPane(String text, String title, Runnable onAccept, Runnable onCancel) {
this(text, title, MessageType.QUESTION, onAccept);
public MessageDialogPane(String text, String title, MessageType type, Runnable onAccept, Runnable onCancel) {
this(text, title, type, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
@@ -148,6 +148,7 @@ public class MultiFileItem<T> extends ComponentSublist {
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);
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
@@ -48,7 +49,7 @@ public class Navigator extends TransitionPane {
backable.set(canGoBack());
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
initialized = true;
}
@@ -68,14 +69,14 @@ public class Navigator extends TransitionPane {
stack.push(node);
backable.set(canGoBack());
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
NavigationEvent navigating = new NavigationEvent(this, from, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
node.getProperties().put("hmcl.navigator.animation", animationProducer);
setContent(node, animationProducer);
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
EventHandler<PageCloseEvent> handler = event -> close(node);
@@ -111,7 +112,7 @@ public class Navigator extends TransitionPane {
backable.set(canGoBack());
Node node = stack.peek();
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
NavigationEvent navigating = new NavigationEvent(this, from, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATING);
fireEvent(navigating);
node.fireEvent(navigating);
@@ -122,7 +123,7 @@ public class Navigator extends TransitionPane {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
}
NavigationEvent navigated = new NavigationEvent(this, node, NavigationEvent.NAVIGATED);
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
Optional.ofNullable(from.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
@@ -206,12 +207,14 @@ public class Navigator extends TransitionPane {
private final Navigator source;
private final Node node;
private final Navigation.NavigationDirection direction;
public NavigationEvent(Navigator source, Node target, EventType<? extends Event> eventType) {
public NavigationEvent(Navigator source, Node target, Navigation.NavigationDirection direction, EventType<? extends Event> eventType) {
super(source, target, eventType);
this.source = source;
this.node = target;
this.direction = direction;
}
@Override
@@ -222,5 +225,9 @@ public class Navigator extends TransitionPane {
public Node getNode() {
return node;
}
public Navigation.NavigationDirection getDirection() {
return direction;
}
}
}
@@ -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);
@@ -19,6 +19,7 @@ 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;
@@ -81,6 +82,18 @@ public class PromptDialogPane extends StackPane {
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);
}
}
@@ -146,6 +159,19 @@ public class PromptDialogPane extends StackPane {
}
}
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) {
@@ -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()));
}
}
@@ -18,35 +18,31 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.*;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@DefaultProperty("content")
public class SpinnerPane extends Control {
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
private final StringProperty failedReason = new SimpleStringProperty(this, "failedReason");
public void showSpinner() {
setLoading(true);
setLoading(true);
}
public void hideSpinner() {
setLoading(false);
setFailedReason(null);
setLoading(false);
}
public Node getContent() {
@@ -62,15 +58,27 @@ public class SpinnerPane extends Control {
}
public boolean isLoading() {
return loading.get();
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
this.loading.set(loading);
}
public String getFailedReason() {
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
}
@Override
@@ -82,62 +90,57 @@ public class SpinnerPane extends Control {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
private final StackPane root = new StackPane();
private Timeline animation;
private final TransitionPane root = new TransitionPane();
private final StackPane failedPane = new StackPane();
private final Label failedReasonLabel = new Label();
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final WeakInvalidationListener observer;
protected Skin(SpinnerPane control) {
super(control);
root.getStyleClass().add("spinner-pane");
topPane.getChildren().setAll(spinner);
root.getChildren().setAll(contentPane, topPane);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> contentPane.getChildren().setAll(newValue));
topPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
contentPane.getChildren().clear();
} else {
contentPane.getChildren().setAll(newValue);
}
});
getChildren().setAll(root);
FXUtils.onChangeAndOperate(getSkinnable().loadingProperty(), newValue -> {
Timeline prev = animation;
if (prev != null) prev.stop();
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
root.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
}
AnimationProducer transition;
topPane.setMouseTransparent(true);
topPane.setVisible(true);
topPane.getStyleClass().add("gray-background");
if (newValue)
transition = ContainerAnimations.FADE_IN.getAnimationProducer();
else
transition = ContainerAnimations.FADE_OUT.getAnimationProducer();
public interface State {}
AnimationHandler handler = new AnimationHandler() {
@Override
public Duration getDuration() {
return Duration.millis(160);
}
public static class LoadedState implements State {}
@Override
public Pane getCurrentRoot() {
return root;
}
public static class LoadingState implements State {}
@Override
public Node getPreviousNode() {
return null;
}
public static class FailedState implements State {
private final String reason;
@Override
public Node getCurrentNode() {
return topPane;
}
};
public FailedState(String reason) {
this.reason = reason;
}
Timeline now = new Timeline();
now.getKeyFrames().addAll(transition.animate(handler));
now.getKeyFrames().add(new KeyFrame(handler.getDuration(), e -> {
topPane.setMouseTransparent(!newValue);
topPane.setVisible(newValue);
}));
now.play();
animation = now;
});
public String getReason() {
return reason;
}
}
}
@@ -174,7 +174,7 @@ public interface TabControl {
}
}
public static class Tab {
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");
@@ -49,6 +49,7 @@ public class TabHeader extends Control implements TabControl {
}
private ObservableList<Tab> tabs = FXCollections.observableArrayList();
private ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP);
@Override
public ObservableList<Tab> getTabs() {
@@ -69,6 +70,27 @@ public class TabHeader extends Control implements TabControl {
this.selectionModel.set(selectionModel);
}
/**
* The position to place the tabs.
*/
public Side getSide() {
return side.get();
}
/**
* The position of the tabs.
*/
public ObjectProperty<Side> sideProperty() {
return side;
}
/**
* The position the place the tabs in this TabHeader.
*/
public void setSide(Side side) {
this.side.set(side);
}
@Override
protected Skin<?> createDefaultSkin() {
return new TabHeaderSkin(this);
@@ -115,7 +137,7 @@ public class TabHeader extends Control implements TabControl {
protected class HeaderContainer extends StackPane {
private Timeline timeline;
private StackPane selectedTabLine;
private StackPane headersRegion;
private HeadersRegion headersRegion;
private Scale scale = new Scale(1, 1, 0, 0);
private Rotate rotate = new Rotate(0, 0, 1);
private double selectedTabLineOffset;
@@ -125,9 +147,115 @@ public class TabHeader extends Control implements TabControl {
getStyleClass().add("tab-header-area");
setPickOnBounds(false);
headersRegion = new StackPane() {
headersRegion = new HeadersRegion();
headersRegion.sideProperty().bind(getSkinnable().sideProperty());
selectedTabLine = new StackPane();
selectedTabLine.setManaged(false);
selectedTabLine.getTransforms().addAll(scale, rotate);
selectedTabLine.setCache(true);
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(2);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
prefWidthProperty().bind(headersRegion.widthProperty());
rotate.pivotXProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 0.0 : 1, getSkinnable().sideProperty()));
rotate.pivotYProperty().bind(Bindings.createDoubleBinding(() -> getSkinnable().getSide().isHorizontal() ? 1.0 : 0, getSkinnable().sideProperty()));
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
TabHeaderContainer container = new TabHeaderContainer(tab);
container.setVisible(true);
return container;
}));
}
public void setNeedsLayout2(boolean value) {
setNeedsLayout(value);
}
private boolean isAnimating() {
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelectingTab) {
headersRegion.animateSelectionLine();
isSelectingTab = false;
}
}
private class HeadersRegion extends StackPane {
private SideAction action;
private final ObjectProperty<Side> side = new SimpleObjectProperty<Side>() {
@Override
protected double computePrefWidth(double height) {
protected void invalidated() {
super.invalidated();
switch (get()) {
case TOP: action = new Top(); break;
case BOTTOM: action = new Bottom(); break;
case LEFT: action = new Left(); break;
case RIGHT: action = new Right(); break;
default: throw new InternalError();
}
}
};
public Side getSide() {
return side.get();
}
public ObjectProperty<Side> sideProperty() {
return side;
}
public void setSide(Side side) {
this.side.set(side);
}
@Override
protected double computePrefWidth(double height) {
return action.computePrefWidth(height);
}
@Override
protected double computePrefHeight(double width) {
return action.computePrefHeight(width);
}
@Override
protected void layoutChildren() {
action.layoutChildren();
}
protected void animateSelectionLine() {
action.animateSelectionLine();
}
private abstract class SideAction {
abstract double computePrefWidth(double height);
abstract double computePrefHeight(double width);
void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
}
abstract void animateSelectionLine();
}
private abstract class Horizontal extends SideAction {
@Override
public double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
@@ -137,7 +265,7 @@ public class TabHeader extends Control implements TabControl {
}
@Override
protected double computePrefHeight(double width) {
public double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
@@ -146,13 +274,81 @@ public class TabHeader extends Control implements TabControl {
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
@Override
protected void layoutChildren() {
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
private void runTimeline(double newTransX, double newWidth) {
double lineWidth = selectedTabLine.prefWidth(-1.0D);
if (isAnimating()) {
timeline.stop();
double tempScaleX = scale.getX();
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
double tempWidth = tempScaleX * lineWidth;
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - tempWidth);
}
}
double oldScaleX = scale.getX();
double oldWidth = lineWidth * oldScaleX;
double oldTransX = selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth;
selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX;
if (transDiff < 0.0D) {
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() + oldWidth);
newTransX += newWidth;
rotate.setAngle(180.0D);
}
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(selectedTabLine.translateXProperty(), selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)
),
new KeyFrame(
Duration.seconds(0.24D),
new KeyValue(scale.xProperty(), newScaleX, Interpolator.EASE_BOTH),
new KeyValue(selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)
)
);
timeline.setOnFinished((finish) -> {
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
selectedTabLine.setTranslateX(selectedTabLine.getTranslateX() - newWidth);
}
});
timeline.play();
}
@Override
public void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabWidth = 0.0D;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer) node;
double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = offset;
selectedTabWidth = tabHeaderPrefWidth;
break;
}
offset += tabHeaderPrefWidth;
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
private class Top extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerHeight = snapSize(prefHeight(-1));
double tabStartX = 0;
for (Node node : getChildren()) {
@@ -171,106 +367,171 @@ public class TabHeader extends Control implements TabControl {
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
};
selectedTabLine = new StackPane();
selectedTabLine.setManaged(false);
selectedTabLine.getTransforms().addAll(scale, rotate);
selectedTabLine.setCache(true);
selectedTabLine.getStyleClass().addAll("tab-selected-line");
selectedTabLine.setPrefHeight(2);
selectedTabLine.setPrefWidth(1);
selectedTabLine.setBackground(new Background(new BackgroundFill(ripplerColor, CornerRadii.EMPTY, Insets.EMPTY)));
getChildren().setAll(headersRegion, selectedTabLine);
headersRegion.setPickOnBounds(false);
headersRegion.prefHeightProperty().bind(heightProperty());
prefWidthProperty().bind(headersRegion.widthProperty());
Bindings.bindContent(headersRegion.getChildren(), binding = MappedObservableList.create(getSkinnable().getTabs(), tab -> {
TabHeaderContainer container = new TabHeaderContainer(tab);
container.setVisible(true);
return container;
}));
}
public void setNeedsLayout2(boolean value) {
setNeedsLayout(value);
}
private void runTimeline(double newTransX, double newWidth) {
double tempScaleX = 0.0D;
double tempWidth = 0.0D;
double lineWidth = this.selectedTabLine.prefWidth(-1.0D);
if (this.isAnimating()) {
this.timeline.stop();
tempScaleX = this.scale.getX();
if (this.rotate.getAngle() != 0.0D) {
this.rotate.setAngle(0.0D);
tempWidth = tempScaleX * lineWidth;
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - tempWidth);
}
}
double oldScaleX = this.scale.getX();
double oldWidth = lineWidth * oldScaleX;
double oldTransX = this.selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth;
this.selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX;
double midScaleX = tempScaleX != 0.0D ? tempScaleX : (Math.abs(transDiff) / 1.3D + oldWidth) * oldScaleX / oldWidth;
if (transDiff < 0.0D) {
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() + oldWidth);
newTransX += newWidth;
this.rotate.setAngle(180.0D);
}
private class Bottom extends Horizontal {
@Override
public void layoutChildren() {
super.layoutChildren();
this.timeline = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.12D), new KeyValue(this.scale.xProperty(), midScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), this.selectedTabLine.getTranslateX(), Interpolator.EASE_BOTH)), new KeyFrame(Duration.seconds(0.24D), new KeyValue(this.scale.xProperty(), newScaleX, Interpolator.EASE_BOTH), new KeyValue(this.selectedTabLine.translateXProperty(), newTransX, Interpolator.EASE_BOTH)));
this.timeline.setOnFinished((finish) -> {
if (this.rotate.getAngle() != 0.0D) {
this.rotate.setAngle(0.0D);
this.selectedTabLine.setTranslateX(this.selectedTabLine.getTranslateX() - newWidth);
}
double headerHeight = snapSize(prefHeight(-1));
double tabStartX = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
});
this.timeline.play();
}
private boolean isAnimating() {
return this.timeline != null && this.timeline.getStatus() == Animation.Status.RUNNING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelectingTab) {
animateSelectionLine();
isSelectingTab = false;
}
}
private void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabWidth = 0.0D;
Side side = Side.TOP;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer)node;
double tabHeaderPrefWidth = this.snapSize(tabHeader.prefWidth(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = side != Side.LEFT && side != Side.BOTTOM ? offset : -offset - tabHeaderPrefWidth;
selectedTabWidth = tabHeaderPrefWidth;
break;
child.relocate(tabStartX, snappedTopInset());
tabStartX += w;
}
offset += tabHeaderPrefWidth;
selectedTabLine.resizeRelocate(0, 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
private abstract class Vertical extends SideAction {
@Override
public double computePrefWidth(double height) {
double width = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
width = Math.max(width, child.prefWidth(height));
}
return snapSize(width) + snappedLeftInset() + snappedRightInset();
}
@Override
public double computePrefHeight(double width) {
double height = 0;
for (Node child : getChildren()) {
if (!(child instanceof TabHeaderContainer) || !child.isVisible()) continue;
height += child.prefHeight(width);
}
return snapSize(height) + snappedTopInset() + snappedBottomInset();
}
private void runTimeline(double newTransY, double newHeight) {
double lineHeight = selectedTabLine.prefHeight(-1.0D);
if (isAnimating()) {
timeline.stop();
double tempScaleY = scale.getY();
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
double tempHeight = tempScaleY * lineHeight;
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - tempHeight);
}
}
double oldScaleY = scale.getY();
double oldHeight = lineHeight * oldScaleY;
double oldTransY = selectedTabLine.getTranslateY();
double newScaleY = newHeight * oldScaleY / oldHeight;
selectedTabLineOffset = newTransY;
// newTransY += offsetStart * (double)this.direction;
double transDiff = newTransY - oldTransY;
if (transDiff < 0.0D) {
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() + oldHeight);
newTransY += newHeight;
rotate.setAngle(180.0D);
}
timeline = new Timeline(
new KeyFrame(
Duration.ZERO,
new KeyValue(selectedTabLine.translateYProperty(), selectedTabLine.getTranslateY(), Interpolator.EASE_BOTH)
),
new KeyFrame(
Duration.seconds(1.24D),
new KeyValue(scale.yProperty(), newScaleY, Interpolator.EASE_BOTH),
new KeyValue(selectedTabLine.translateYProperty(), newTransY, Interpolator.EASE_BOTH)
)
);
timeline.setOnFinished((finish) -> {
if (rotate.getAngle() != 0.0D) {
rotate.setAngle(0.0D);
selectedTabLine.setTranslateY(selectedTabLine.getTranslateY() - newHeight);
}
});
timeline.play();
}
@Override
public void animateSelectionLine() {
double offset = 0.0D;
double selectedTabOffset = 0.0D;
double selectedTabHeight = 0.0D;
for (Node node : headersRegion.getChildren()) {
if (node instanceof TabHeaderContainer) {
TabHeaderContainer tabHeader = (TabHeaderContainer) node;
double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1.0D));
if (selectedTab != null && selectedTab.equals(tabHeader.tab)) {
selectedTabOffset = offset;
selectedTabHeight = tabHeaderPrefHeight;
break;
}
offset += tabHeaderPrefHeight;
}
}
this.runTimeline(selectedTabOffset, selectedTabHeight);
}
}
private class Left extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerWidth = snapSize(prefWidth(-1));
double tabStartY = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
child.relocate(headerWidth - w - snappedRightInset(), tabStartY);
tabStartY += h;
}
selectedTabLine.resizeRelocate(headerWidth - selectedTabLine.prefWidth(-1), 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
private class Right extends Vertical {
@Override
public void layoutChildren() {
super.layoutChildren();
double headerWidth = snapSize(prefWidth(-1));
double tabStartY = 0;
for (Node node : getChildren()) {
if (!(node instanceof TabHeaderContainer)) continue;
TabHeaderContainer child = (TabHeaderContainer) node;
double w = snapSize(child.prefWidth(-1));
double h = snapSize(child.prefHeight(-1));
child.resize(w, h);
child.relocate(snappedLeftInset(), tabStartY);
tabStartY += h;
}
selectedTabLine.resizeRelocate(0, 0,
snapSize(selectedTabLine.prefWidth(-1)),
snapSize(selectedTabLine.prefHeight(-1)));
}
}
this.runTimeline(selectedTabOffset, selectedTabWidth);
}
}
@@ -305,4 +566,5 @@ public class TabHeader extends Control implements TabControl {
}
}
}
}
@@ -34,14 +34,16 @@ 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.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;
@@ -128,7 +130,7 @@ 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"));
@@ -17,17 +17,25 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final ObservableList<Label> tagLabels;
public TwoLineListItem(String titleString, String subtitleString) {
this();
@@ -37,15 +45,30 @@ public class TwoLineListItem extends VBox {
public TwoLineListItem() {
setMouseTransparent(true);
HBox firstLine = new HBox();
Label lblTitle = new Label();
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
HBox tagContainer = new HBox();
tagLabels = MappedObservableList.create(tags, tag -> {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
return tagLabel;
});
Bindings.bindContent(tagContainer.getChildren(), tagLabels);
firstLine.getChildren().addAll(lblTitle, tagContainer);
Label lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");
lblSubtitle.textProperty().bind(subtitle);
getChildren().setAll(lblTitle, lblSubtitle);
getChildren().setAll(firstLine, lblSubtitle);
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
@@ -73,6 +96,10 @@ public class TwoLineListItem extends VBox {
this.subtitle.set(subtitle);
}
public ObservableList<String> getTags() {
return tags;
}
@Override
public String toString() {
return getTitle();
@@ -22,13 +22,19 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public class Decorator extends Control {
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
@@ -46,11 +52,17 @@ public class Decorator extends Control {
private final BooleanProperty canClose = new SimpleBooleanProperty(false);
private final BooleanProperty showCloseAsHome = new SimpleBooleanProperty(false);
private final Stage primaryStage;
private Navigation.NavigationDirection navigationDirection = Navigation.NavigationDirection.START;
private StackPane drawerWrapper;
private final ReadOnlyBooleanWrapper allowMove = new ReadOnlyBooleanWrapper();
private final ReadOnlyBooleanWrapper dragging = new ReadOnlyBooleanWrapper();
public Decorator(Stage primaryStage) {
this.primaryStage = primaryStage;
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
primaryStage.initStyle(StageStyle.UNDECORATED);
}
@@ -62,7 +74,7 @@ public class Decorator extends Control {
return drawerWrapper;
}
void setDrawerWrapper(StackPane drawerWrapper) {
public void setDrawerWrapper(StackPane drawerWrapper) {
this.drawerWrapper = drawerWrapper;
}
@@ -166,6 +178,30 @@ public class Decorator extends Control {
return showCloseAsHome;
}
public boolean isAllowMove() {
return allowMove.get();
}
public ReadOnlyBooleanProperty allowMoveProperty() {
return allowMove.getReadOnlyProperty();
}
void setAllowMove(boolean allowMove) {
this.allowMove.set(allowMove);
}
public boolean isDragging() {
return dragging.get();
}
public ReadOnlyBooleanProperty draggingProperty() {
return dragging.getReadOnlyProperty();
}
void setDragging(boolean dragging) {
this.dragging.set(dragging);
}
public ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonActionProperty() {
return onBackNavButtonAction;
}
@@ -190,4 +226,27 @@ public class Decorator extends Control {
public void close() {
onCloseButtonAction.get().run();
}
public void capableDraggingWindow(Node node) {
node.addEventHandler(MouseEvent.MOUSE_MOVED, e -> allowMove.set(true));
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
if (!isDragging()) allowMove.set(false);
});
}
public void forbidDraggingWindow(Node node) {
node.addEventHandler(MouseEvent.MOUSE_MOVED, e -> {
allowMove.set(false);
e.consume();
});
}
// TODO: Dirty implementation.
public Navigation.NavigationDirection getNavigationDirection() {
return navigationDirection;
}
public void setNavigationDirection(Navigation.NavigationDirection navigationDirection) {
this.navigationDirection = navigationDirection;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,26 +18,18 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -55,21 +47,22 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.logging.Level;
import java.util.stream.Stream;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
private final Decorator decorator;
private final ImageView welcomeView;
private final Navigator navigator;
private JFXDialog dialog;
@@ -80,7 +73,6 @@ public class DecoratorController {
decorator.setOnCloseButtonAction(Launcher::stopApplication);
navigator = new Navigator();
navigator.setOnNavigating(this::onNavigating);
navigator.setOnNavigated(this::onNavigated);
navigator.init(mainPage);
@@ -89,27 +81,6 @@ public class DecoratorController {
decorator.onBackNavButtonActionProperty().set(e -> back());
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(newImage("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> decorator.getContainer().remove(welcomeView))
);
nowAnimation.play();
});
if (switchedToNewUI()) {
if (config().getLocalization().getLocale() == Locale.CHINA) {
// currently, user guide is only available in Chinese
decorator.getContainer().setAll(welcomeView);
}
}
setupBackground();
setupAuthlibInjectorDnD();
@@ -119,21 +90,10 @@ public class DecoratorController {
return decorator;
}
/**
* @return true if the user is seeing the current version of UI for the first time.
*/
private boolean switchedToNewUI() {
if (config().getUiVersion() < Config.CURRENT_UI_VERSION) {
config().setUiVersion(Config.CURRENT_UI_VERSION);
return true;
}
return false;
}
// ==== Background ====
private void setupBackground() {
decorator.backgroundProperty().bind(
decorator.contentBackgroundProperty().bind(
Bindings.createObjectBinding(
() -> {
Image image = null;
@@ -172,12 +132,12 @@ public class DecoratorController {
}
List<Path> candidates;
try {
candidates = Files.list(imageDir)
.filter(Files::isRegularFile)
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(Files::isReadable)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
String ext = getExtension(it).toLowerCase();
return ext.equals("png") || ext.equals("jpg");
})
.collect(toList());
} catch (IOException e) {
@@ -199,13 +159,23 @@ public class DecoratorController {
}
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
if (!Files.isReadable(path))
return Optional.empty();
Image img;
try {
img = new Image(path.toAbsolutePath().toUri().toString());
} catch (IllegalArgumentException e) {
LOG.log(WARNING, "Couldn't load background image", e);
return Optional.empty();
}
return Optional.empty();
if (img.getException() != null) {
LOG.log(WARNING, "Couldn't load background image", img.getException());
return Optional.empty();
}
return Optional.of(img);
}
// ==== Navigation ====
@@ -273,6 +243,8 @@ public class DecoratorController {
decorator.showCloseAsHomeProperty().set(true);
}
decorator.setNavigationDirection(event.getDirection());
// state property should be updated at last.
if (to instanceof DecoratorPage) {
decorator.stateProperty().bind(((DecoratorPage) to).stateProperty());
@@ -302,16 +274,16 @@ public class DecoratorController {
Platform.runLater(() -> showDialog(node));
return;
}
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialog.setContent(dialogPane);
decorator.capableDraggingWindow(dialog);
decorator.forbidDraggingWindow(dialogPane);
dialog.setDialogContainer(decorator.getDrawerWrapper());
dialog.setOverlayClose(false);
dialog.show();
}
dialogPane.push(node);
EventHandler<DialogCloseEvent> handler = event -> closeDialog(node);
@@ -30,6 +30,7 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -40,8 +41,10 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Lang;
public class DecoratorSkin extends SkinBase<Decorator> {
@@ -49,14 +52,13 @@ public class DecoratorSkin extends SkinBase<Decorator> {
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
private final BorderPane root;
private final StackPane root, parent;
private final StackPane titleContainer;
private final Stage primaryStage;
private final TransitionPane navBarPane;
private final StackPane leftPane;
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging;
private boolean titleBarTransparent = true;
/**
@@ -72,14 +74,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
minus.fillProperty().bind(Theme.foregroundFillBinding());
Decorator skinnable = getSkinnable();
StackPane parent = new StackPane();
parent.backgroundProperty().bind(skinnable.backgroundProperty());
root = new StackPane();
root.getStyleClass().add("window");
parent = new StackPane();
parent.getStyleClass().add("body");
parent.backgroundProperty().bind(skinnable.contentBackgroundProperty());
parent.setPickOnBounds(false);
parent.prefHeightProperty().bind(control.prefHeightProperty());
parent.prefWidthProperty().bind(control.prefWidthProperty());
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
root = new BorderPane();
root.getStyleClass().addAll("jfx-decorator", "resize-border");
root.getChildren().setAll(parent);
// animation layer at bottom
{
@@ -89,12 +97,18 @@ public class DecoratorSkin extends SkinBase<Decorator> {
leftPane.getStyleClass().add("jfx-decorator-drawer");
layer.getChildren().setAll(leftPane);
parent.getChildren().add(layer);
parent.getChildren().add(root);
}
StackPane wrapper = new StackPane();
BorderPane frame = new BorderPane();
frame.getStyleClass().addAll("jfx-decorator");
wrapper.getChildren().setAll(frame);
skinnable.setDrawerWrapper(wrapper);
parent.getChildren().add(wrapper);
// center node with a animation layer at bottom, a container layer at middle and a "welcome" layer at top.
StackPane container = new StackPane();
skinnable.setDrawerWrapper(container);
FXUtils.setOverflowHidden(container);
// content layer at middle
@@ -125,19 +139,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
container.getChildren().add(floatLayer);
}
root.setCenter(container);
frame.setCenter(container);
titleContainer = new StackPane();
root.setOnMouseReleased(this::onMouseReleased);
root.setOnMouseDragged(this::onMouseDragged);
root.setOnMouseMoved(this::onMouseMoved);
titleContainer.setPickOnBounds(false);
titleContainer.setMinHeight(40);
titleContainer.getStyleClass().addAll("jfx-tool-bar");
titleContainer.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> allowMove = true);
titleContainer.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
if (!isDragging) allowMove = false;
});
control.capableDraggingWindow(titleContainer);
StackPane titleBarBackground = new StackPane();
titleBarBackground.getStyleClass().add("background");
@@ -151,6 +158,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
rectangle.widthProperty().bind(titleContainer.widthProperty());
rectangle.heightProperty().bind(titleContainer.heightProperty().add(100));
titleContainer.setClip(rectangle);
Rectangle buttonsContainerPlaceHolder = new Rectangle();
{
navBarPane = new TransitionPane();
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
@@ -158,7 +167,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
Node node = createNavBar(skinnable, s.isTitleBarTransparent(), s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
double targetOpacity = s.isTitleBarTransparent() ? 0 : 1;
if (s.isAnimate()) {
navBarPane.setContent(node, ContainerAnimations.FADE.getAnimationProducer());
AnimationProducer animation;
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) {
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT.getAnimationProducer();
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) {
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT.getAnimationProducer();
} else {
animation = ContainerAnimations.FADE.getAnimationProducer();
}
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
navBarPane.setContent(node, animation);
} else {
navBarPane.getChildren().setAll(node);
}
@@ -170,11 +188,14 @@ public class DecoratorSkin extends SkinBase<Decorator> {
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
});
titleBar.setCenter(navBarPane);
titleBar.setRight(buttonsContainerPlaceHolder);
}
frame.setTop(titleContainer);
{
HBox buttonsContainer = new HBox();
buttonsContainer.setStyle("-fx-background-color: transparent;");
buttonsContainer.setAlignment(Pos.CENTER_RIGHT);
buttonsContainer.setPadding(new Insets(4));
buttonsContainer.setAlignment(Pos.TOP_RIGHT);
buttonsContainer.setMaxHeight(40);
{
JFXButton btnMin = new JFXButton();
StackPane pane = new StackPane(minus);
@@ -190,11 +211,16 @@ public class DecoratorSkin extends SkinBase<Decorator> {
buttonsContainer.getChildren().setAll(btnMin, btnClose);
}
titleBar.setRight(buttonsContainer);
AnchorPane layer = new AnchorPane();
layer.setPickOnBounds(false);
layer.getChildren().add(buttonsContainer);
AnchorPane.setTopAnchor(buttonsContainer, 0.0);
AnchorPane.setRightAnchor(buttonsContainer, 0.0);
buttonsContainerPlaceHolder.widthProperty().bind(buttonsContainer.widthProperty());
parent.getChildren().add(layer);
}
root.setTop(titleContainer);
getChildren().add(parent);
getChildren().add(root);
}
private Node createNavBar(Decorator skinnable, boolean titleBarTransparent, double leftPaneWidth, boolean canBack, boolean canClose, boolean showCloseAsHome, boolean canRefresh, String title, Node titleNode) {
@@ -203,32 +229,32 @@ public class DecoratorSkin extends SkinBase<Decorator> {
HBox navLeft = new HBox();
navLeft.setAlignment(Pos.CENTER_LEFT);
navLeft.setPadding(new Insets(0, 5, 0, 5));
{
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().set(canBack);
JFXButton closeNavButton = new JFXButton();
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().set(canBack);
JFXButton closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
if (canBack) navLeft.getChildren().add(backNavButton);
if (canClose) navLeft.getChildren().add(closeNavButton);
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
else
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
if (canBack) navLeft.getChildren().add(backNavButton);
if (canClose) navLeft.getChildren().add(closeNavButton);
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
else
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
}
navBar.setLeft(navLeft);
BorderPane center = new BorderPane();
if (title != null) {
Label titleLabel = new Label();
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.getStyleClass().add("jfx-decorator-title");
if (titleBarTransparent) titleLabel.pseudoClassStateChanged(TRANSPARENT, true);
if (!canClose && !canBack) {
@@ -237,6 +263,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
// we should have width 2 * 10 reduced
titleLabel.setPrefWidth(leftPaneWidth - 20);
}
if (titleNode != null) {
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
// 18 = 10 (horizontal padding of navLeft) + 8 (margin-left)
return leftPaneWidth - 18 - backNavButton.getWidth() - closeNavButton.getWidth();
}, backNavButton.widthProperty(), closeNavButton.widthProperty()));
}
titleLabel.setText(title);
center.setLeft(titleLabel);
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
@@ -277,19 +309,19 @@ public class DecoratorSkin extends SkinBase<Decorator> {
}
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
return x < root.getWidth() && x > root.getWidth() - root.snappedLeftInset();
return x < root.getWidth() && x >= root.getWidth() - root.snappedLeftInset();
}
private boolean isTopEdge(double x, double y, Bounds boundsInParent) {
return y >= 0 && y < root.snappedLeftInset();
return y >= 0 && y <= root.snappedTopInset();
}
private boolean isBottomEdge(double x, double y, Bounds boundsInParent) {
return y < root.getHeight() && y > root.getHeight() - root.snappedLeftInset();
return y < root.getHeight() && y >= root.getHeight() - root.snappedLeftInset();
}
private boolean isLeftEdge(double x, double y, Bounds boundsInParent) {
return x >= 0 && x < root.snappedLeftInset();
return x >= 0 && x <= root.snappedLeftInset();
}
private boolean setStageWidth(double width) {
@@ -326,43 +358,41 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (primaryStage.isResizable()) {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = root.getBoundsInParent();
if (root.getBorder() != null && root.getBorder().getStrokes().size() > 0) {
double diagonalSize = root.snappedLeftInset() + 10;
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else {
root.setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else {
root.setCursor(Cursor.N_RESIZE);
}
} else if (this.isBottomEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.S_RESIZE);
}
double diagonalSize = root.snappedLeftInset() + 10;
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.DEFAULT);
root.setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else {
root.setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else {
root.setCursor(Cursor.N_RESIZE);
}
} else if (this.isBottomEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.S_RESIZE);
}
} else {
root.setCursor(Cursor.DEFAULT);
}
}
} else {
@@ -371,11 +401,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
}
protected void onMouseReleased(MouseEvent mouseEvent) {
isDragging = false;
getSkinnable().setDragging(false);
}
protected void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
getSkinnable().setDragging(true);
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress()) {
this.newX = mouseEvent.getScreenX();
@@ -429,7 +459,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
}
mouseEvent.consume();
} else if (this.allowMove) {
} else if (getSkinnable().isAllowMove()) {
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
mouseEvent.consume();
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public abstract class DecoratorTabPage extends DecoratorTransitionPage implements TabControl {
@@ -36,10 +37,10 @@ public abstract class DecoratorTabPage extends DecoratorTransitionPage implement
}
if (newValue.getNode() != null) {
onNavigating(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATING));
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING));
navigate(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
onNavigated(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATED));
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED));
}
});
}
@@ -19,16 +19,27 @@ package org.jackhuang.hmcl.ui.decorator;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.wizard.*;
import java.util.Queue;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements TaskExecutorDialogWizardDisplayer {
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private final WizardDisplayer displayer = new TaskExecutorDialogWizardDisplayer(new ConcurrentLinkedQueue<>()) {
@Override
public void onEnd() {
super.onEnd();
fireEvent(new PageCloseEvent());
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
}
};
private final String category;
@@ -45,28 +56,24 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
}
@Override
public WizardController getWizardController() {
return wizardController;
}
@Override
public Queue<Object> getCancelQueue() {
return cancelQueue;
}
@Override
public void onStart() {
displayer.onStart();
}
@Override
public void onCancel() {
displayer.onCancel();
}
@Override
public void onEnd() {
fireEvent(new PageCloseEvent());
displayer.onEnd();
}
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
displayer.navigateTo(page, nav);
navigate(page, nav.getAnimation().getAnimationProducer());
String prefix = category == null ? "" : category + " - ";
@@ -86,6 +93,11 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
}
}
@Override
public void handleTask(Map<String, Object> settings, Task<?> task) {
displayer.handleTask(settings, task);
}
@Override
public boolean isPageCloseable() {
return true;
@@ -20,11 +20,11 @@ package org.jackhuang.hmcl.ui.download;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.util.Lang;
@@ -40,7 +40,7 @@ class AdditionalInstallersPage extends InstallersPage {
protected final String gameVersion;
protected final Version version;
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, GameRepository repository, DownloadProvider downloadProvider) {
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, GameRepository repository, InstallerWizardDownloadProvider downloadProvider) {
super(controller, repository, gameVersion, downloadProvider);
this.gameVersion = gameVersion;
this.version = version;
@@ -54,10 +54,8 @@ class AdditionalInstallersPage extends InstallersPage {
() -> compatible.get() && txtName.validate(),
txtName.textProperty(), compatible));
InstallerPageItem[] libraries = new InstallerPageItem[]{game, fabric, forge, liteLoader, optiFine};
for (InstallerPageItem library : libraries) {
String libraryId = library.id;
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
library.removeAction.set(e -> {
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
@@ -91,28 +89,25 @@ class AdditionalInstallersPage extends InstallersPage {
String liteLoader = analyzer.getVersion(LITELOADER).orElse(null);
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
InstallerPageItem[] libraries = new InstallerPageItem[]{this.game, this.fabric, this.forge, this.liteLoader, this.optiFine};
InstallerItem[] libraries = group.getLibraries();
String[] versions = new String[]{game, fabric, forge, liteLoader, optiFine};
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
boolean compatible = true;
for (int i = 0; i < libraries.length; ++i) {
String libraryId = libraries[i].id;
String libraryId = libraries[i].getLibraryId();
String libraryVersion = Lang.nonNull(getVersion(libraryId), versions[i]);
boolean alreadyInstalled = versions[i] != null && !(controller.getSettings().get(libraryId) instanceof UpdateInstallerWizardProvider.RemoveVersionAction);
if (!"game".equals(libraryId) && currentGameVersion != null && !currentGameVersion.equals(game) && getVersion(libraryId) == null && alreadyInstalled) {
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
// warns the user that we should update the library.
libraries[i].label.set(i18n("install.installer.change_version", i18n("install.installer." + libraryId), libraryVersion));
libraries[i].removable.set(true);
libraries[i].setState(libraryVersion, /* incompatibleWithGame */ true, /* removable */ true);
compatible = false;
} else if (alreadyInstalled || getVersion(libraryId) != null) {
libraries[i].label.set(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + libraryVersion);
libraries[i].removable.set(true);
libraries[i].setState(libraryVersion, /* incompatibleWithGame */ false, /* removable */ true);
} else {
libraries[i].label.set(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
libraries[i].removable.set(false);
libraries[i].setState(/* libraryVersion */ null, /* incompatibleWithGame */ false, /* removable */ false);
}
}
this.compatible.set(compatible);
@@ -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.download;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import java.net.URL;
import java.util.List;
public class InstallerWizardDownloadProvider implements DownloadProvider {
private DownloadProvider fallback;
public InstallerWizardDownloadProvider(DownloadProvider fallback) {
this.fallback = fallback;
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
fallback = downloadProvider;
}
@Override
public String getVersionListURL() {
return fallback.getVersionListURL();
}
@Override
public String getAssetBaseURL() {
return fallback.getAssetBaseURL();
}
@Override
public List<URL> getAssetObjectCandidates(String assetObjectLocation) {
return fallback.getAssetObjectCandidates(assetObjectLocation);
}
@Override
public String injectURL(String baseURL) {
return fallback.injectURL(baseURL);
}
@Override
public List<URL> injectURLWithCandidates(String baseURL) {
return fallback.injectURLWithCandidates(baseURL);
}
@Override
public VersionList<?> getVersionListById(String id) {
return fallback.getVersionListById(id);
}
@Override
public int getConcurrency() {
return fallback.getConcurrency();
}
}
@@ -19,30 +19,21 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
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.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -56,15 +47,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InstallersPage extends Control implements WizardPage {
protected final WizardController controller;
protected InstallerPageItem game = new InstallerPageItem("game");
protected InstallerPageItem fabric = new InstallerPageItem("fabric");
protected InstallerPageItem forge = new InstallerPageItem("forge");
protected InstallerPageItem liteLoader = new InstallerPageItem("liteloader");
protected InstallerPageItem optiFine = new InstallerPageItem("optifine");
protected InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup();
protected JFXTextField txtName = new JFXTextField();
protected BooleanProperty installable = new SimpleBooleanProperty();
public InstallersPage(WizardController controller, GameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
public InstallersPage(WizardController controller, GameRepository repository, String gameVersion, InstallerWizardDownloadProvider downloadProvider) {
this.controller = controller;
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
@@ -76,13 +63,15 @@ public class InstallersPage extends Control implements WizardPage {
txtName.textProperty()));
txtName.setText(gameVersion);
InstallerPageItem[] libraries = new InstallerPageItem[]{game, fabric, forge, liteLoader, optiFine};
group.game.installable.setValue(false);
for (InstallerPageItem library : libraries) {
String libraryId = library.id;
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
library.action.set(e ->
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false))));
library.action.set(e -> {
if (library.incompatibleLibraryName.get() == null)
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
});
library.removeAction.set(e -> {
controller.getSettings().remove(libraryId);
reload();
@@ -100,15 +89,13 @@ public class InstallersPage extends Control implements WizardPage {
}
protected void reload() {
InstallerPageItem[] libraries = new InstallerPageItem[]{game, fabric, forge, liteLoader, optiFine};
for (InstallerPageItem library : libraries) {
String libraryId = library.id;
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (controller.getSettings().containsKey(libraryId)) {
library.label.set(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + getVersion(libraryId));
library.libraryVersion.set(getVersion(libraryId));
library.removable.set(true);
} else {
library.label.setValue(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
library.libraryVersion.set(null);
library.removable.set(false);
}
}
@@ -134,55 +121,8 @@ public class InstallersPage extends Control implements WizardPage {
return new InstallersPageSkin(this);
}
protected static class InstallerPageItem {
String id;
StringProperty label = new SimpleStringProperty();
BooleanProperty removable = new SimpleBooleanProperty();
ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
public InstallerPageItem(String id) {
this.id = id;
}
}
protected static class InstallersPageSkin extends SkinBase<InstallersPage> {
protected static class InstallersPageItemSkin extends BorderPane {
final ImageView imageView;
final Label label;
InstallersPageItemSkin(String imageUrl, InstallerPageItem item, boolean clickable) {
setPadding(new Insets(8));
getStyleClass().add("card");
setLeft(FXUtils.limitingSize(imageView = new ImageView(new Image(imageUrl, 32, 32, true, true)), 32, 32));
setCenter(label = new Label());
label.textProperty().bind(item.label);
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
if (clickable) {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
setRight(right);
JFXButton closeButton = new JFXButton();
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
right.getChildren().add(closeButton);
closeButton.getStyleClass().add("toggle-icon4");
closeButton.visibleProperty().bind(item.removable);
closeButton.onMouseClickedProperty().bind(item.removeAction);
onMouseClickedProperty().bind(item.action);
JFXButton arrowButton = new JFXButton();
arrowButton.setGraphic(SVG.arrowRight(Theme.blackFillBinding(), -1, -1));
arrowButton.onMouseClickedProperty().bind(item.action);
arrowButton.getStyleClass().add("toggle-icon4");
right.getChildren().add(arrowButton);
setCursor(Cursor.HAND);
}
}
}
/**
* Constructor for all SkinBase instances.
*
@@ -200,7 +140,7 @@ public class InstallersPage extends Control implements WizardPage {
HBox versionNamePane = new HBox(8);
versionNamePane.setAlignment(Pos.CENTER_LEFT);
versionNamePane.getStyleClass().add("card");
versionNamePane.setPadding(new Insets(20, 8, 20, 16));
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
versionNamePane.getChildren().add(new Label(i18n("archive.name")));
@@ -209,13 +149,7 @@ public class InstallersPage extends Control implements WizardPage {
list.getChildren().add(versionNamePane);
}
InstallersPageItemSkin game = new InstallersPageItemSkin("/assets/img/grass.png", control.game, false);
InstallersPageItemSkin fabric = new InstallersPageItemSkin("/assets/img/fabric.png", control.fabric, true);
InstallersPageItemSkin forge = new InstallersPageItemSkin("/assets/img/forge.png", control.forge, true);
InstallersPageItemSkin liteLoader = new InstallersPageItemSkin("/assets/img/chicken.png", control.liteLoader, true);
InstallersPageItemSkin optiFine = new InstallersPageItemSkin("/assets/img/command.png", control.optiFine, true);
list.getChildren().addAll(game, fabric, forge, liteLoader, optiFine);
list.getChildren().forEach(node -> JFXDepthManager.setDepth(node, 1));
list.getChildren().addAll(control.group.getLibraries());
{
JFXButton installButton = new JFXButton(i18n("button.install"));
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@@ -90,8 +91,17 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
if (name.isPresent()) {
txtModpackName.setText(name.get());
txtModpackName.setDisable(true);
} else {
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
}
btnInstall.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtModpackName.validate(),
txtModpackName.textProperty()));
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
selectedFile = filePath.get();
@@ -101,7 +111,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
controller.onEnd();
return;
}
@@ -121,12 +131,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
txtModpackName.setText(manifest.getName());
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
@@ -85,7 +85,11 @@ public class ModpackInstallWizardProvider implements WizardProvider {
return null;
}
try {
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
if (serverModpackManifest != null) {
return ModpackHelper.getUpdateTask(profile, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
} else {
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
}
} catch (UnsupportedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
@@ -24,13 +24,8 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebStage;
@@ -40,10 +35,6 @@ import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@@ -110,7 +101,7 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
lblAuthor.setText(manifest.getAuthor());
if (!name.isPresent()) {
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
txtModpackName.setText(manifest.getName());
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
@@ -18,12 +18,16 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.ArtifactMalformedException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Task;
@@ -53,14 +57,16 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
private final Version version;
private final String libraryId;
private final String oldLibraryVersion;
private final InstallerWizardDownloadProvider downloadProvider;
public UpdateInstallerWizardProvider(@NotNull Profile profile, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
this.profile = profile;
this.dependencyManager = profile.getDependency();
this.gameVersion = gameVersion;
this.version = version;
this.libraryId = libraryId;
this.oldLibraryVersion = oldLibraryVersion;
this.downloadProvider = new InstallerWizardDownloadProvider(DownloadProviders.getDownloadProvider());
this.dependencyManager = profile.getDependency(downloadProvider);
}
@Override
@@ -90,20 +96,19 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
}
}
return ret.thenComposeAsync(profile.getRepository()::save).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHint(stages);
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHint(stages);
}
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getPrimaryDownloadProvider();
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, provider, libraryId, () -> {
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> {
if (oldLibraryVersion == null) {
controller.onFinish();
} else if ("game".equals(libraryId)) {
String newGameVersion = ((RemoteVersion) settings.get(libraryId)).getSelfVersion();
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, profile.getRepository(), provider));
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, profile.getRepository(), downloadProvider));
} else {
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibraryVersion, ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
i18n("install.change_version"), controller::onFinish, controller::onCancel);
@@ -142,17 +147,18 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
}
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException ||
exception instanceof FabricInstallTask.UnsupportedFabricInstallationException) {
@@ -18,9 +18,10 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -33,9 +34,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VanillaInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final DefaultDependencyManager dependencyManager;
private final InstallerWizardDownloadProvider downloadProvider;
public VanillaInstallWizardProvider(Profile profile) {
this.profile = profile;
this.downloadProvider = new InstallerWizardDownloadProvider(DownloadProviders.getDownloadProvider());
this.dependencyManager = profile.getDependency(downloadProvider);
}
@Override
@@ -44,7 +49,7 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
}
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
GameBuilder builder = profile.getDependency().gameBuilder();
GameBuilder builder = dependencyManager.gameBuilder();
String name = (String) settings.get("name");
builder.name(name);
@@ -69,11 +74,10 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getPrimaryDownloadProvider();
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", provider, "game",
() -> controller.onNext(new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), provider)));
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
() -> controller.onNext(new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
import javafx.application.Platform;
@@ -31,7 +32,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -54,6 +55,7 @@ import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -83,23 +85,34 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private HBox checkPane;
@FXML
private VBox centrePane;
@FXML
private JFXComboBox<String> downloadSourceComboBox;
private final VersionList<?> versionList;
private VersionList<?> versionList;
private TaskExecutor executor;
public VersionsPage(WizardController controller, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
public VersionsPage(WizardController controller, String title, String gameVersion, InstallerWizardDownloadProvider downloadProvider, String libraryId, Runnable callback) {
this.title = title;
this.gameVersion = gameVersion;
this.libraryId = libraryId;
this.controller = controller;
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else
centrePane.getChildren().setAll(list);
downloadSourceComboBox.getItems().setAll(DownloadProviders.providersById.keySet());
downloadSourceComboBox.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourceComboBox.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
controller.getSettings().put("downloadProvider", newValue);
downloadProvider.setDownloadProvider(DownloadProviders.getDownloadProviderByPrimaryId(newValue));
versionList = downloadProvider.getVersionListById(libraryId);
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else {
centrePane.getChildren().setAll(list);
}
refresh();
});
downloadSourceComboBox.getSelectionModel().select((String)controller.getSettings().getOrDefault("downloadProvider", DownloadProviders.getPrimaryDownloadProviderId()));
InvalidationListener listener = o -> list.getItems().setAll(loadVersions());
chkRelease.selectedProperty().addListener(listener);
@@ -184,13 +197,15 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
executor = versionList.refreshAsync(gameVersion).whenComplete(exception -> {
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(exception -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
Platform.runLater(() -> {
if (versionList.getVersions(gameVersion).isEmpty()) {
if (versionList != currentVersionList) return;
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
root.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
if (items.isEmpty()) {
@@ -206,6 +221,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
} else {
LOG.log(Level.WARNING, "Failed to fetch versions list", exception);
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
});
}
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Collections;
@@ -63,72 +62,97 @@ public final class ExportWizardProvider implements WizardProvider {
@SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
String modpackName = (String) settings.get(ModpackInfoPage.MODPACK_NAME);
String modpackAuthor = (String) settings.get(ModpackInfoPage.MODPACK_AUTHOR);
String modpackFileApi = (String) settings.get(ModpackInfoPage.MODPACK_FILE_API);
String modpackVersion = (String) settings.get(ModpackInfoPage.MODPACK_VERSION);
String modpackDescription = (String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION);
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
exportInfo.setWhitelist(whitelist);
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
switch (modpackType) {
case ModpackTypeSelectionPage.MODPACK_TYPE_HMCL:
return exportAsHMCL(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, includeLauncher);
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
return exportAsMultiMC(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription);
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
return exportAsServer(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi);
default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
}
return exportWithLauncher(modpackType, exportInfo, modpackFile);
}
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) {
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
return new Task<Object>() {
File tempModpack;
Task<?> exportTask;
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
File dest;
if (packWithLauncher) {
dest = tempModpack = Files.createTempFile("hmcl", ".zip").toFile();
} else {
dest = modpackFile;
}
switch (modpackType) {
case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
exportTask = exportAsMcbbs(exportInfo, dest);
break;
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
exportTask = exportAsMultiMC(exportInfo, dest);
break;
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
exportTask = exportAsServer(exportInfo, dest);
break;
default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
}
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(exportTask);
}
@Override
public void execute() throws Exception {
if (!packWithLauncher) return;
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.setPreferredLoginType(config().getPreferredLoginType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
}
};
}
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, File modpackFile) {
return new Task<Void>() {
Task<?> dependency = null;
@Override
public void execute() throws Exception {
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(modpackName, modpackAuthor, modpackVersion, null, modpackDescription, StandardCharsets.UTF_8, null), tempModpack);
if (includeLauncher) {
dependency = dependency.thenRunAsync(() -> {
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.setPreferredLoginType(config().getPreferredLoginType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
});
}
public void execute() {
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
}
@Override
@@ -138,30 +162,30 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) {
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
VersionSetting vs = profile.getVersionSetting(version);
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist,
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
new MultiMCInstanceConfiguration(
"OneSix",
modpackName + "-" + modpackVersion,
exportInfo.getName() + "-" + exportInfo.getVersion(),
null,
Lang.toIntOrNull(vs.getPermSize()),
vs.getWrapper(),
vs.getPreLaunchCommand(),
null,
modpackDescription,
exportInfo.getDescription(),
null,
vs.getJavaArgs(),
exportInfo.getJavaArguments(),
vs.isFullscreen(),
vs.getWidth(),
vs.getHeight(),
vs.getMaxMemory(),
vs.getMinMemory(),
exportInfo.getMinMemory(),
vs.isShowLogs(),
/* showConsoleOnError */ true,
/* autoCloseConsole */ false,
@@ -181,13 +205,13 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) {
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile);
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
}
@Override
@@ -203,7 +227,7 @@ public final class ExportWizardProvider implements WizardProvider {
case 0:
return new ModpackTypeSelectionPage(controller);
case 1:
return new ModpackInfoPage(controller, version);
return new ModpackInfoPage(controller, profile.getRepository(), version);
case 2:
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
default:
@@ -87,21 +87,24 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null)
if (files != null) {
for (File it : files) {
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + it.getName());
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected())
node.setIndeterminate(true);
node.getChildren().add(subNode);
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected()) {
node.setIndeterminate(true);
}
node.getChildren().add(subNode);
}
}
}
if (!node.isSelected()) node.setIndeterminate(false);
// Empty folder need not to be displayed.
if (node.getChildren().isEmpty())
if (node.getChildren().isEmpty()) {
return null;
}
}
HBox graphic = new HBox();
@@ -126,12 +129,13 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
private void getFilesNeeded(CheckBoxTreeItem<String> node, String basePath, List<String> list) {
if (node == null) return;
if (node.isSelected()) {
if (node.isSelected() || node.isIndeterminate()) {
if (basePath.length() > "minecraft/".length())
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
for (TreeItem<String> child : node.getChildren()) {
if (child instanceof CheckBoxTreeItem)
if (child instanceof CheckBoxTreeItem) {
getFilesNeeded(((CheckBoxTreeItem<String>) child), basePath + "/" + child.getValue(), list);
}
}
}
}
@@ -17,76 +17,96 @@
*/
package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.validation.RequiredFieldValidator;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackInfoPage extends Control implements WizardPage {
private final WizardController controller;
private final HMCLGameRepository gameRepository;
private final ModpackExportInfo.Options options;
private final String versionName;
private final boolean canIncludeLauncher;
private final boolean showFileApi;
private SimpleStringProperty versionName = new SimpleStringProperty();
private SimpleStringProperty modpackName = new SimpleStringProperty();
private SimpleStringProperty modpackFileApi = new SimpleStringProperty();
private SimpleStringProperty modpackAuthor = new SimpleStringProperty();
private SimpleStringProperty modpackVersion = new SimpleStringProperty("1.0");
private SimpleStringProperty modpackDescription = new SimpleStringProperty();
private SimpleBooleanProperty includingLauncher = new SimpleBooleanProperty();
private ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final ModpackExportInfo exportInfo = new ModpackExportInfo();
public ModpackInfoPage(WizardController controller, String version) {
private final SimpleStringProperty name = new SimpleStringProperty("");
private final SimpleStringProperty author = new SimpleStringProperty("");
private final SimpleStringProperty version = new SimpleStringProperty("1.0");
private final SimpleStringProperty description = new SimpleStringProperty("");
private final SimpleStringProperty url = new SimpleStringProperty("");
private final SimpleBooleanProperty forceUpdate = new SimpleBooleanProperty();
private final SimpleBooleanProperty packWithLauncher = new SimpleBooleanProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty("");
private final SimpleIntegerProperty minMemory = new SimpleIntegerProperty(0);
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller;
modpackName.set(version);
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
versionName.set(version);
this.gameRepository = gameRepository;
this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
.orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
this.versionName = version;
name.set(version);
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
VersionSetting versionSetting = gameRepository.getVersionSetting(versionName);
minMemory.set(Optional.ofNullable(versionSetting.getMinMemory()).orElse(0));
launchArguments.set(versionSetting.getMinecraftArgs());
javaArguments.set(versionSetting.getJavaArgs());
List<File> launcherJar = Launcher.getCurrentJarFiles();
canIncludeLauncher = launcherJar != null;
showFileApi = controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER;
next.set(e -> onNext());
}
@FXML
private void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
@@ -96,24 +116,35 @@ public final class ModpackInfoPage extends Control implements WizardPage {
controller.onEnd();
return;
}
controller.getSettings().put(MODPACK_NAME, modpackName.get());
controller.getSettings().put(MODPACK_FILE_API, modpackFileApi.get());
controller.getSettings().put(MODPACK_VERSION, modpackVersion.get());
controller.getSettings().put(MODPACK_AUTHOR, modpackAuthor.get());
exportInfo.setName(name.get());
exportInfo.setFileApi(fileApi.get());
exportInfo.setVersion(version.get());
exportInfo.setAuthor(author.get());
exportInfo.setDescription(description.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setUrl(url.get());
exportInfo.setForceUpdate(forceUpdate.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setMinMemory(minMemory.get());
exportInfo.setLaunchArguments(launchArguments.get());
exportInfo.setJavaArguments(javaArguments.get());
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
"mcbbs", Integer.parseInt(mcbbsThreadId.get())
)));
}
controller.getSettings().put(MODPACK_INFO, exportInfo);
controller.getSettings().put(MODPACK_FILE, file);
controller.getSettings().put(MODPACK_DESCRIPTION, modpackDescription.get());
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, includingLauncher.get());
controller.onNext();
}
@Override
public void cleanup(Map<String, Object> settings) {
controller.getSettings().remove(MODPACK_NAME);
controller.getSettings().remove(MODPACK_VERSION);
controller.getSettings().remove(MODPACK_AUTHOR);
controller.getSettings().remove(MODPACK_DESCRIPTION);
controller.getSettings().remove(MODPACK_INCLUDE_LAUNCHER);
controller.getSettings().remove(MODPACK_FILE);
controller.getSettings().remove(MODPACK_INFO);
}
@Override
@@ -126,19 +157,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
return new ModpackInfoPageSkin(this);
}
public static final String MODPACK_NAME = "modpack.name";
public static final String MODPACK_FILE_API = "modpack.file_api";
public static final String MODPACK_VERSION = "modpack.version";
public static final String MODPACK_AUTHOR = "archive.author";
public static final String MODPACK_DESCRIPTION = "modpack.description";
public static final String MODPACK_INCLUDE_LAUNCHER = "modpack.include_launcher";
public static final String MODPACK_INFO = "modpack.info";
public static final String MODPACK_FILE = "modpack.file";
public static final String MODPACK_INFO_OPTION = "modpack.info.option";
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private final JFXTextField txtModpackName;
private final JFXTextField txtModpackFileApi;
private final JFXTextField txtModpackAuthor;
private final JFXTextField txtModpackVersion;
private ObservableList<Node> originList;
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
super(skinnable);
@@ -151,6 +175,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
scroll.setFitToHeight(true);
getChildren().setAll(scroll);
List<JFXTextField> validatingFields = new ArrayList<>();
{
BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-padding: 16;");
@@ -179,31 +205,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
Label versionNameLabel = new Label();
versionNameLabel.textProperty().bind(skinnable.versionName);
versionNameLabel.setText(skinnable.versionName);
borderPane1.setRight(versionNameLabel);
list.getContent().add(borderPane1);
}
{
txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName);
JFXTextField txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.name);
txtModpackName.setLabelFloat(true);
txtModpackName.setPromptText(i18n("modpack.name"));
RequiredFieldValidator validator = new RequiredFieldValidator();
validator.setMessage(i18n("modpack.not_a_valid_name"));
txtModpackName.getValidators().add(validator);
txtModpackName.getValidators().add(new RequiredValidator(i18n("modpack.not_a_valid_name")));
StackPane.setMargin(txtModpackName, insets);
list.getContent().add(txtModpackName);
validatingFields.add(txtModpackName);
}
if (skinnable.showFileApi) {
txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi);
if (skinnable.options.isRequireFileApi()) {
JFXTextField txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
txtModpackFileApi.setLabelFloat(true);
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackFileApi.getValidators().add(validator);
if (skinnable.options.isValidateFileApi()) {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new Validator(s -> {
if (s.isEmpty()) {
return true;
}
try {
new URL(s).toURI();
return true;
@@ -213,35 +245,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
}));
StackPane.setMargin(txtModpackFileApi, insets);
list.getContent().add(txtModpackFileApi);
} else {
txtModpackFileApi = null;
validatingFields.add(txtModpackFileApi);
}
{
txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor);
JFXTextField txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
txtModpackAuthor.setLabelFloat(true);
txtModpackAuthor.setPromptText(i18n("archive.author"));
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackAuthor.getValidators().add(validator);
txtModpackAuthor.getValidators().add(new RequiredValidator());
StackPane.setMargin(txtModpackAuthor, insets);
list.getContent().add(txtModpackAuthor);
validatingFields.add(txtModpackAuthor);
}
{
txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion);
JFXTextField txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
txtModpackVersion.setLabelFloat(true);
txtModpackVersion.setPromptText(i18n("archive.version"));
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackVersion.getValidators().add(validator);
txtModpackVersion.getValidators().add(new RequiredValidator());
StackPane.setMargin(txtModpackVersion, insets);
list.getContent().add(txtModpackVersion);
validatingFields.add(txtModpackVersion);
}
{
JFXTextArea area = new JFXTextArea();
area.textProperty().bindBidirectional(skinnable.modpackDescription);
area.textProperty().bindBidirectional(skinnable.description);
area.setLabelFloat(true);
area.setPromptText(i18n("modpack.desc"));
area.setMinHeight(400);
@@ -249,18 +283,123 @@ public final class ModpackInfoPage extends Control implements WizardPage {
list.getContent().add(area);
}
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_HMCL) {
BorderPane borderPane1 = new BorderPane();
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
list.getContent().add(borderPane1);
if (skinnable.options.isRequireForceUpdate()) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
list.getContent().add(pane);
JFXToggleButton button = new JFXToggleButton();
button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.includingLauncher);
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
borderPane1.setRight(button);
pane.setRight(button);
}
if (skinnable.options.isRequireAuthlibInjectorServer()) {
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
.map(AuthlibInjectorServer::getUrl)
.orElse(null)));
BorderPane pane = new BorderPane();
Label left = new Label(i18n("account.injector.server"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
pane.setLeft(left);
pane.setRight(cboServers);
list.getContent().add(pane);
}
if (skinnable.options.isRequireMinMemory()) {
JFXTextField txtMinMemory = new JFXTextField();
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
FXUtils.setLimitWidth(txtMinMemory, 300);
validatingFields.add(txtMinMemory);
BorderPane pane = new BorderPane();
Label label = new Label(i18n("settings.min_memory"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
pane.setLeft(label);
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
pane.setRight(txtMinMemory);
list.getContent().add(pane);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
txtLaunchArguments.setLabelFloat(true);
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
StackPane.setMargin(txtLaunchArguments, insets);
list.getContent().add(txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
txtJavaArguments.setLabelFloat(true);
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
StackPane.setMargin(txtJavaArguments, insets);
list.getContent().add(txtJavaArguments);
}
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
BorderPane originPane = new BorderPane();
Label title = new Label(i18n("modpack.origin"));
originPane.setTop(title);
VBox container = new VBox();
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
originPane.setCenter(container);
list.getContent().add(originPane);
if (skinnable.options.isRequireUrl()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.url"));
pane.setLeft(left);
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.setRight(txtModpackUrl);
container.getChildren().add(pane);
}
if (skinnable.options.isRequireOrigins()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.mcbbs"));
pane.setLeft(left);
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
pane.setRight(txtMcbbs);
container.getChildren().add(pane);
validatingFields.add(txtMcbbs);
}
}
{
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
list.getContent().add(pane);
JFXToggleButton button = new JFXToggleButton();
button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
}
}
@@ -276,15 +415,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
nextButton.setText(i18n("wizard.next"));
nextButton.getStyleClass().add("jfx-button-raised");
if (skinnable.showFileApi) {
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate() || !txtModpackFileApi.validate(),
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty(), txtModpackFileApi.textProperty()));
} else {
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate(),
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty()));
}
nextButton.disableProperty().bind(
// Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill
// our requirement.
Bindings.createBooleanBinding(() -> validatingFields.stream()
.map(field -> !field.validate())
.reduce(false, (left, right) -> left || right),
validatingFields.stream().map(JFXTextField::textProperty).toArray(StringProperty[]::new)));
hbox.getChildren().add(nextButton);
}
}
@@ -20,18 +20,23 @@ package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML
private JFXButton btnHMCL;
private JFXButton btnMCBBS;
@FXML
private JFXButton btnMultiMC;
@FXML
@@ -41,12 +46,15 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
JFXButton[] buttons = new JFXButton[]{btnMCBBS, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_MCBBS, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
ModpackExportInfo.Options[] options = new ModpackExportInfo.Options[]{McbbsModpackExportTask.OPTION, MultiMCModpackExportTask.OPTION, ServerModpackExportTask.OPTION};
for (int i = 0; i < types.length; ++i) {
String type = types[i];
ModpackExportInfo.Options option = options[i];
buttons[i].setOnMouseClicked(e -> {
controller.getSettings().put(MODPACK_TYPE, type);
controller.getSettings().put(MODPACK_INFO_OPTION, option);
controller.onNext();
});
}
@@ -63,7 +71,7 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
public static final String MODPACK_TYPE = "modpack.type";
public static final String MODPACK_TYPE_HMCL = "hmcl";
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
public static final String MODPACK_TYPE_MULTIMC = "multimc";
public static final String MODPACK_TYPE_SERVER = "server";
}
@@ -115,6 +115,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
StackPane launchPane = new StackPane();
launchPane.getStyleClass().add("launch-pane");
launchPane.setMaxWidth(230);
launchPane.setMaxHeight(55);
launchPane.setOnScroll(event -> {
@@ -134,8 +135,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
JFXButton launchButton = new JFXButton();
launchButton.setPrefWidth(230);
launchButton.setPrefHeight(55);
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.getStyleClass().add("jfx-button-raised");
//launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.setOnAction(e -> launch());
launchButton.setDefaultButton(true);
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
@@ -161,7 +161,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
Rectangle separator = new Rectangle();
separator.getStyleClass().add("darker-fill");
separator.setWidth(1);
separator.setHeight(57);
separator.setTranslateX(95);
@@ -170,13 +169,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
menuButton = new JFXButton();
menuButton.setPrefHeight(55);
menuButton.setPrefWidth(230);
menuButton.setButtonType(JFXButton.ButtonType.RAISED);
menuButton.getStyleClass().add("jfx-button-raised");
//menuButton.setButtonType(JFXButton.ButtonType.RAISED);
menuButton.setStyle("-fx-font-size: 15px;");
menuButton.setOnMouseClicked(e -> onMenu());
menuButton.setClip(new Rectangle(211, -100, 100, 200));
StackPane graphic = new StackPane();
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
Node svg = SVG.triangle(Theme.foregroundFillBinding(), 10, 10);
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
graphic.getChildren().setAll(svg);
graphic.setTranslateX(12);
@@ -18,11 +18,9 @@
package org.jackhuang.hmcl.ui.main;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -43,10 +41,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorTabPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.profile.ProfileList;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.GameList;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -67,15 +62,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorTabPage {
private MainPage mainPage = null;
private SettingsPage settingsPage = null;
private GameList gameListPage = null;
private AccountList accountListPage = null;
private ProfileList profileListPage = null;
private final TabHeader.Tab mainTab = new TabHeader.Tab("main");
private final TabHeader.Tab settingsTab = new TabHeader.Tab("settings");
private final TabHeader.Tab gameTab = new TabHeader.Tab("game");
private final TabHeader.Tab accountTab = new TabHeader.Tab("account");
private final TabHeader.Tab profileTab = new TabHeader.Tab("profile");
public RootPage() {
setLeftPaneWidth(200);
@@ -88,10 +79,8 @@ public class RootPage extends DecoratorTabPage {
mainTab.setNodeSupplier(this::getMainPage);
settingsTab.setNodeSupplier(this::getSettingsPage);
gameTab.setNodeSupplier(this::getGameListPage);
accountTab.setNodeSupplier(this::getAccountListPage);
profileTab.setNodeSupplier(this::getProfileListPage);
getTabs().setAll(mainTab, settingsTab, gameTab, accountTab, profileTab);
getTabs().setAll(mainTab, settingsTab, accountTab);
}
@Override
@@ -153,17 +142,6 @@ public class RootPage extends DecoratorTabPage {
return settingsPage;
}
private 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;
}
private AccountList getAccountListPage() {
if (accountListPage == null) {
accountListPage = new AccountList();
@@ -173,13 +151,24 @@ public class RootPage extends DecoratorTabPage {
return accountListPage;
}
private ProfileList getProfileListPage() {
if (profileListPage == null) {
profileListPage = new ProfileList();
profileListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
profileListPage.profilesProperty().bindContent(Profiles.profilesProperty());
public Tab getMainTab() {
return mainTab;
}
public Tab getSettingsTab() {
return settingsTab;
}
public Tab getAccountTab() {
return accountTab;
}
private void selectPage(Tab tab) {
if (getSelectionModel().getSelectedItem() == tab) {
getSelectionModel().select(getMainTab());
} else {
getSelectionModel().select(tab);
}
return profileListPage;
}
private static class Skin extends SkinBase<RootPage> {
@@ -190,17 +179,16 @@ public class RootPage extends DecoratorTabPage {
// first item in left sidebar
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.activeProperty().bind(control.accountTab.selectedProperty());
accountListItem.setOnAction(e -> control.getSelectionModel().select(control.accountTab));
accountListItem.setOnAction(e -> control.selectPage(control.accountTab));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
// second item in left sidebar
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.actionButtonVisibleProperty().bind(Profiles.selectedVersionProperty().isNotNull());
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
control.getSelectionModel().select(control.gameTab);
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
}
@@ -208,23 +196,16 @@ public class RootPage extends DecoratorTabPage {
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.activeProperty().bind(control.gameTab.selectedProperty());
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
gameItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/bookshelf.png")).getKey());
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> control.getSelectionModel().select(control.gameTab));
// forth item in left sidebar
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.activeProperty().bind(control.profileTab.selectedProperty());
profileListItem.setOnAction(e -> control.getSelectionModel().select(control.profileTab));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.activeProperty().bind(control.settingsTab.selectedProperty());
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
launcherSettingsItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> control.getSelectionModel().select(control.settingsTab));
launcherSettingsItem.setOnAction(e -> control.selectPage(control.settingsTab));
// the left sidebar
AdvancedListBox sideBar = new AdvancedListBox()
@@ -233,8 +214,6 @@ public class RootPage extends DecoratorTabPage {
.startCategory(i18n("version").toUpperCase())
.add(gameListItem)
.add(gameItem)
.startCategory(i18n("profile.title").toUpperCase())
.add(profileListItem)
.startCategory(i18n("launcher").toUpperCase())
.add(launcherSettingsItem);
@@ -247,9 +226,7 @@ public class RootPage extends DecoratorTabPage {
control.transitionPane.getStyleClass().add("jfx-decorator-content-container");
control.transitionPane.getChildren().setAll(getSkinnable().getMainPage());
FXUtils.setOverflowHidden(control.transitionPane, 8);
StackPane wrapper = new StackPane(control.transitionPane);
wrapper.setPadding(new Insets(4));
root.setCenter(wrapper);
root.setCenter(control.transitionPane);
}
getChildren().setAll(root);
@@ -258,7 +235,12 @@ public class RootPage extends DecoratorTabPage {
}
// ==== Accounts ====
private boolean checkedAccont = false;
public void checkAccount() {
if (checkedAccont) return;
checkedAccont = true;
if (Accounts.getAccounts().isEmpty())
Platform.runLater(this::addNewAccount);
}
@@ -34,7 +34,6 @@ import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
@@ -63,7 +62,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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 static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reservedSelectedPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.reversedSelectedPropertyFor;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
public final class SettingsPage extends SettingsView implements DecoratorPage {
@@ -73,7 +72,6 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
public SettingsPage() {
FXUtils.smoothScrolling(scroll);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
// ==== Download sources ====
cboDownloadSource.getItems().setAll(DownloadProviders.providersById.keySet());
@@ -112,7 +110,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
reservedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
reversedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
@@ -535,8 +535,12 @@ public abstract class SettingsView extends StackPane {
}
protected abstract void onUpdate();
protected abstract void onHelp();
protected abstract void onExportLogs();
protected abstract void onSponsor();
protected abstract void clearCacheDirectory();
}
@@ -42,7 +42,7 @@ public class ProfileAdvancedListItem extends AdvancedListItem {
};
public ProfileAdvancedListItem() {
setImage(newImage("/assets/img/craft_table.png"));
setLeftGraphic(createImageView(newImage("/assets/img/craft_table.png")).getKey());
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
@@ -1,58 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.profile;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import org.jackhuang.hmcl.setting.Profile;
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 ProfileList extends ListPage<ProfileListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("profile.manage")));
private final ListProperty<Profile> profiles = new SimpleListProperty<>(FXCollections.observableArrayList());
private ObjectProperty<Profile> selectedProfile;
public ProfileList() {
setItems(MappedObservableList.create(profilesProperty(), ProfileListItem::new));
selectedProfile = createSelectedItemPropertyFor(getItems(), Profile.class);
}
public ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
public ListProperty<Profile> profilesProperty() {
return profiles;
}
@Override
public void add() {
Controllers.navigate(new ProfilePage(null));
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
}
@@ -32,7 +32,7 @@ public class ProfileListItem extends RadioButton {
public ProfileListItem(Profile profile) {
this.profile = profile;
getStyleClass().clear();
getStyleClass().setAll("profile-list-item", "navigation-drawer-item");
setUserData(profile);
title.set(Profiles.getProfileDisplayName(profile));
@@ -18,50 +18,47 @@
package org.jackhuang.hmcl.ui.profile;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Insets;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public ProfileListItemSkin(ProfileListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
root.setPickOnBounds(false);
RipplerContainer container = new RipplerContainer(root);
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
FXUtils.onChangeAndOperate(skinnable.selectedProperty(), active -> {
skinnable.pseudoClassStateChanged(SELECTED, active);
});
HBox center = new HBox();
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
getSkinnable().addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
getSkinnable().setSelected(true);
});
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().set(newImage("/assets/img/craft_table.png"));
Node left = VersionPage.wrap(SVG.folderOutline(null, 24, 24));
root.setLeft(left);
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setPickOnBounds(false);
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
root.setCenter(center);
root.setCenter(item);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
@@ -70,16 +67,13 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
right.getChildren().add(btnRemove);
root.setRight(right);
root.getStyleClass().add("card");
root.setPadding(new Insets(8, 8, 8, 0));
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
getChildren().setAll(root);
getChildren().setAll(container);
}
}
@@ -68,6 +68,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
{
setMessage(i18n("profile.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
@@ -132,7 +132,7 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
String getSubtitle() {
return StringUtils.parseColorEscapes(packInfo.getDescription());
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
}
Datapack.Pack getPackInfo() {
@@ -17,38 +17,45 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
public GameAdvancedListItem() {
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
setImage(newImage("/assets/img/grass.png"));
imageView.setImage(newImage("/assets/img/grass.png"));
tooltip.setText("");
}
});
setRightGraphic(SVG.gear(Theme.blackFillBinding(), -1, -1));
setActionButtonVisible(false);
}
}
@@ -27,16 +27,20 @@ import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -48,6 +52,7 @@ public class GameItem extends Control {
private final Profile profile;
private final String version;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty tag = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
@@ -75,6 +80,17 @@ public class GameItem extends Control {
}, Platform::runLater)
.exceptionally(handleUncaught);
CompletableFuture.runAsync(() -> {
try {
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
if (config == null) return;
tag.set(config.getVersion());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read modpack configuration from ", e);
}
}, Platform::runLater)
.exceptionally(handleUncaught);
title.set(id);
image.set(profile.getRepository().getVersionIconImage(version));
}
@@ -96,6 +112,10 @@ public class GameItem extends Control {
return title;
}
public StringProperty tagProperty() {
return tag;
}
public StringProperty subtitleProperty() {
return subtitle;
}
@@ -45,6 +45,9 @@ public class GameItemSkin extends SkinBase<GameItem> {
TwoLineListItem item = new TwoLineListItem();
item.titleProperty().bind(skinnable.titleProperty());
FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> {
item.getTags().setAll(tag);
});
item.subtitleProperty().bind(skinnable.subtitleProperty());
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
@@ -1,150 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.Node;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.download.VanillaInstallWizardProvider;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameList extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
private ToggleGroup toggleGroup;
public GameList() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
runInFX(() -> setLoading(true));
});
Profiles.registerVersionsListener(this::loadVersions);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
toggleGroup = new ToggleGroup();
WeakListenerHolder listenerHolder = new WeakListenerHolder();
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
List<GameListItem> 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 -> new GameListItem(toggleGroup, profile, version.getId()))
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
setLoading(false);
itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
FXUtils.checkFxUserThread();
children.forEach(it -> it.selectedProperty().set(false));
children.stream()
.filter(it -> it.getVersion().equals(newValue))
.findFirst()
.ifPresent(it -> it.selectedProperty().set(true));
}));
}
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null) return;
GameListItem model = (GameListItem) toggle.getUserData();
model.getProfile().setSelectedVersion(model.getVersion());
});
});
}
@Override
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
void addNewGame() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile), i18n("install.new_game"));
}
}
void importModpack() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack"));
}
}
public void refresh() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
public void modifyGlobalGameSettings() {
Versions.modifyGlobalSettings(Profiles.getSelectedProfile());
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
public GameListSkin() {
super(GameList.this);
HBox hbox = new HBox(
createToolbarButton(i18n("install.new_game"), SVG::plus, GameList.this::addNewGame),
createToolbarButton(i18n("install.modpack"), SVG::importIcon, GameList.this::importModpack),
createToolbarButton(i18n("button.refresh"), SVG::refresh, GameList.this::refresh),
createToolbarButton(i18n("settings.type.global.manage"), SVG::gear, GameList.this::modifyGlobalGameSettings)
);
hbox.setPickOnBounds(false);
state.set(new State(i18n("version.manage"), hbox, true, false, true));
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
}
}
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.SkinBase;
@@ -63,17 +62,17 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
popup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.launch(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.launchOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.launch(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.generateLaunchScript(), popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.gearOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencilOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.duplicate(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.deleteOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.export(), popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.browse(), popup)));
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.browse(), popup)));
}
HBox right = new HBox();
@@ -82,24 +81,36 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
JFXButton btnUpgrade = new JFXButton();
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.update(Theme.blackFillBinding(), 24, 24), 24, 24));
runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
right.getChildren().add(btnUpgrade);
}
JFXButton btnManage = new JFXButton();
btnManage.setOnMouseClicked(e -> {
currentSkinnable = skinnable;
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(SVG.dotsVertical(Theme.blackFillBinding(), -1, -1));
right.getChildren().add(btnManage);
{
JFXButton btnLaunch = new JFXButton();
btnLaunch.setOnMouseClicked(e -> skinnable.launch());
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.launchOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
right.getChildren().add(btnLaunch);
}
{
JFXButton btnManage = new JFXButton();
btnManage.setOnMouseClicked(e -> {
currentSkinnable = skinnable;
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
});
btnManage.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnManage, Pos.CENTER);
btnManage.setGraphic(FXUtils.limitingSize(SVG.dotsVertical(Theme.blackFillBinding(), 24, 24), 24, 24));
right.getChildren().add(btnManage);
}
root.setRight(right);
root.getStyleClass().add("card");
root.setPadding(new Insets(8, 8, 8, 0));
root.setStyle("-fx-padding: 8 8 8 0");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
@@ -0,0 +1,257 @@
/*
* 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.versions;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.profile.ProfileListItem;
import org.jackhuang.hmcl.ui.profile.ProfilePage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
public class GameListPage extends ListPageBase<GameListItem> implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
private final ListProperty<Profile> profiles = new SimpleListProperty<>(FXCollections.observableArrayList());
@SuppressWarnings("FieldCanBeLocal")
private final ObservableList<ProfileListItem> profileListItems;
private final ObjectProperty<Profile> selectedProfile;
private ToggleGroup toggleGroup;
public GameListPage() {
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == Profiles.getSelectedProfile().getRepository())
runInFX(() -> setLoading(true));
});
profileListItems = MappedObservableList.create(profilesProperty(), profile -> {
ProfileListItem item = new ProfileListItem(profile);
FXUtils.setLimitWidth(item, 200);
return item;
});
selectedProfile = createSelectedItemPropertyFor(profileListItems, Profile.class);
}
public ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
public ObservableList<Profile> getProfiles() {
return profiles.get();
}
public ListProperty<Profile> profilesProperty() {
return profiles;
}
public void setProfiles(ObservableList<Profile> profiles) {
this.profiles.set(profiles);
}
@Override
protected GameListPageSkin createDefaultSkin() {
return new GameListPageSkin();
}
public void modifyGlobalGameSettings() {
Versions.modifyGlobalSettings(Profiles.getSelectedProfile());
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private class GameListPageSkin extends SkinBase<GameListPage> {
protected GameListPageSkin() {
super(GameListPage.this);
BorderPane root = new BorderPane();
GameList gameList = new GameList();
{
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
root.setLeft(left);
{
AdvancedListItem addProfileItem = new AdvancedListItem();
addProfileItem.getStyleClass().add("navigation-drawer-item");
addProfileItem.setTitle(i18n("profile.new"));
addProfileItem.setActionButtonVisible(false);
addProfileItem.setLeftGraphic(VersionPage.wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
ScrollPane pane = new ScrollPane();
VBox wrapper = new VBox();
wrapper.getStyleClass().add("advanced-list-box-content");
VBox box = new VBox();
Bindings.bindContent(box.getChildren(), profileListItems);
wrapper.getChildren().setAll(box, addProfileItem);
pane.setContent(wrapper);
left.setCenter(pane);
}
{
AdvancedListItem installNewGameItem = new AdvancedListItem();
installNewGameItem.getStyleClass().add("navigation-drawer-item");
installNewGameItem.setTitle(i18n("install.new_game"));
installNewGameItem.setActionButtonVisible(false);
installNewGameItem.setLeftGraphic(VersionPage.wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
installNewGameItem.setOnAction(e -> Versions.addNewGame());
AdvancedListItem installModpackItem = new AdvancedListItem();
installModpackItem.getStyleClass().add("navigation-drawer-item");
installModpackItem.setTitle(i18n("install.modpack"));
installModpackItem.setActionButtonVisible(false);
installModpackItem.setLeftGraphic(VersionPage.wrap(SVG.pack(Theme.blackFillBinding(), 24, 24)));
installModpackItem.setOnAction(e -> Versions.importModpack());
AdvancedListItem downloadModpackItem = new AdvancedListItem();
downloadModpackItem.getStyleClass().add("navigation-drawer-item");
downloadModpackItem.setTitle(i18n("modpack.download"));
downloadModpackItem.setActionButtonVisible(false);
downloadModpackItem.setLeftGraphic(VersionPage.wrap(SVG.fire(Theme.blackFillBinding(), 24, 24)));
downloadModpackItem.setOnAction(e -> Versions.downloadModpack());
AdvancedListItem refreshItem = new AdvancedListItem();
refreshItem.getStyleClass().add("navigation-drawer-item");
refreshItem.setTitle(i18n("button.refresh"));
refreshItem.setActionButtonVisible(false);
refreshItem.setLeftGraphic(VersionPage.wrap(SVG.refresh(Theme.blackFillBinding(), 24, 24)));
refreshItem.setOnAction(e -> gameList.refreshList());
AdvancedListItem globalManageItem = new AdvancedListItem();
globalManageItem.getStyleClass().add("navigation-drawer-item");
globalManageItem.setTitle(i18n("settings.type.global.manage"));
globalManageItem.setActionButtonVisible(false);
globalManageItem.setLeftGraphic(VersionPage.wrap(SVG.gearOutline(Theme.blackFillBinding(), 24, 24)));
globalManageItem.setOnAction(e -> modifyGlobalGameSettings());
AdvancedListBox bottomLeftCornerList = new AdvancedListBox()
.add(installNewGameItem)
.add(installModpackItem)
.add(downloadModpackItem)
.add(refreshItem)
.add(globalManageItem);
FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 5 + 12 * 2);
left.setBottom(bottomLeftCornerList);
}
}
root.setCenter(gameList);
getChildren().setAll(root);
}
}
private class GameList extends ListPageBase<GameListItem> {
public GameList() {
super();
Profiles.registerVersionsListener(this::loadVersions);
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
toggleGroup = new ToggleGroup();
WeakListenerHolder listenerHolder = new WeakListenerHolder();
toggleGroup.getProperties().put("ReferenceHolder", listenerHolder);
List<GameListItem> 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 -> new GameListItem(toggleGroup, profile, version.getId()))
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
setLoading(false);
itemsProperty().setAll(children);
children.forEach(GameListItem::checkSelection);
profile.selectedVersionProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
FXUtils.checkFxUserThread();
children.forEach(it -> it.selectedProperty().set(false));
children.stream()
.filter(it -> it.getVersion().equals(newValue))
.findFirst()
.ifPresent(it -> it.selectedProperty().set(true));
}));
}
toggleGroup.selectedToggleProperty().addListener((o, a, toggle) -> {
if (toggle == null) return;
GameListItem model = (GameListItem) toggle.getUserData();
model.getProfile().setSelectedVersion(model.getVersion());
});
});
}
public void refreshList() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
}
@Override
protected GameListSkin createDefaultSkin() {
return new GameListSkin();
}
private class GameListSkin extends ToolbarListPageSkin<GameList> {
public GameListSkin() {
super(GameList.this);
state.set(new State(i18n("version.manage"), null, true, false, true, false, 200));
}
@Override
protected List<Node> initializeToolbar(GameList skinnable) {
return Collections.emptyList();
}
}
}
}
@@ -39,7 +39,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -74,9 +73,9 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId));
}).thenAcceptAsync(analyzer -> {
Function<String, Consumer<InstallerItem>> removeAction = libraryId -> x -> {
Function<String, Runnable> removeAction = libraryId -> () -> {
profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::save)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
.start();
@@ -84,15 +83,28 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
itemsProperty().clear();
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
String libraryId = type.getPatchId();
String libraryVersion = analyzer.getVersion(type).orElse(null);
Consumer<InstallerItem> action = "game".equals(libraryId) || libraryVersion == null ? null : removeAction.apply(libraryId);
itemsProperty().add(new InstallerItem(libraryId, libraryVersion, () -> {
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup();
// Conventional libraries: game, fabric, forge, liteloader, optifine
for (InstallerItem installerItem : group.getLibraries()) {
String libraryId = installerItem.getLibraryId();
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
installerItem.libraryVersion.set(libraryVersion);
installerItem.upgradable.set(libraryVersion != null);
installerItem.installable.set(true);
installerItem.action.set(e -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
}, action));
});
boolean removable = !"game".equals(libraryId) && libraryVersion != null;
installerItem.removable.set(removable);
if (removable) {
Runnable action = removeAction.apply(libraryId);
installerItem.removeAction.set(e -> action.run());
}
itemsProperty().add(installerItem);
}
// other third-party libraries which are unable to manage.
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
@@ -101,14 +113,23 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
if (LibraryAnalyzer.LibraryType.fromPatchId(libraryId) != null)
continue;
Consumer<InstallerItem> action = removeAction.apply(libraryId);
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId)))
itemsProperty().add(
new InstallerItem(libraryId, libraryVersion, () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
}, action));
else
itemsProperty().add(new InstallerItem(libraryId, libraryVersion, null, action));
Runnable action = removeAction.apply(libraryId);
InstallerItem installerItem = new InstallerItem(libraryId);
installerItem.libraryVersion.set(libraryVersion);
installerItem.installable.set(false);
installerItem.upgradable.bind(installerItem.installable);
installerItem.removable.set(true);
installerItem.removeAction.set(e -> action.run());
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId))) {
installerItem.installable.set(true);
installerItem.action.set(e -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
});
}
itemsProperty().add(installerItem);
}
}, Platform::runLater);
}
@@ -122,7 +143,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
private void doInstallOffline(File file) {
Task<?> task = profile.getDependency().installLibraryAsync(version, file.toPath())
.thenComposeAsync(profile.getRepository()::save)
.thenComposeAsync(profile.getRepository()::saveAsync)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
task.setName(i18n("install.installer.install_offline"));
TaskExecutor executor = task.executor(new TaskListener() {
@@ -0,0 +1,322 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadListPage extends Control implements DecoratorPage {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final ListProperty<CurseAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final ModDownloadPage.DownloadCallback callback;
/**
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MODPACK
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MOD
*/
private final int section;
public ModDownloadListPage(int section, ModDownloadPage.DownloadCallback callback) {
this.section = section;
this.callback = callback;
}
public void loadVersion(Profile profile, String version) {
this.version.set(new Profile.ProfileVersion(profile, version));
setLoading(false);
setFailed(false);
}
public boolean isFailed() {
return failed.get();
}
public BooleanProperty failedProperty() {
return failed;
}
public void setFailed(boolean failed) {
this.failed.set(failed);
}
public boolean isLoading() {
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public void search(String userGameVersion, int category, int pageOffset, String searchFilter, int sort) {
setLoading(true);
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
? version.get().getProfile().getRepository().getVersionJar(version.get().getVersion())
: null;
Task.supplyAsync(() -> {
String gameVersion;
if (StringUtils.isBlank(version.get().getVersion())) {
gameVersion = userGameVersion;
} else {
gameVersion = GameVersion.minecraftVersion(versionJar).orElse("");
}
return gameVersion;
}).thenApplyAsync(gameVersion -> {
return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
setLoading(false);
if (exception == null) {
items.setAll(result);
} else {
failed.set(true);
}
}).start();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override
protected Skin<?> createDefaultSkin() {
return new ModDownloadListPageSkin(this);
}
private static class ModDownloadListPageSkin extends SkinBase<ModDownloadListPage> {
protected ModDownloadListPageSkin(ModDownloadListPage control) {
super(control);
VBox pane = new VBox();
GridPane searchPane = new GridPane();
searchPane.getStyleClass().addAll("card");
VBox.setMargin(searchPane, new Insets(10, 10, 0, 10));
ColumnConstraints column1 = new ColumnConstraints();
column1.setPercentWidth(50);
column1.setHgrow(Priority.ALWAYS);
ColumnConstraints column2 = new ColumnConstraints();
column2.setHgrow(Priority.ALWAYS);
column2.setPercentWidth(50);
searchPane.getColumnConstraints().setAll(column1, column2);
searchPane.setHgap(16);
searchPane.setVgap(10);
{
JFXTextField nameField = new JFXTextField();
nameField.setPromptText(i18n("mods.name"));
searchPane.add(nameField, 0, 0);
JFXTextField gameVersionField = new JFXTextField();
gameVersionField.setPromptText(i18n("world.game_version"));
searchPane.add(gameVersionField, 1, 0);
FXUtils.onChangeAndOperate(getSkinnable().version, version -> {
searchPane.getChildren().remove(gameVersionField);
if (StringUtils.isNotBlank(version.getVersion())) {
GridPane.setColumnSpan(nameField, 2);
} else {
searchPane.add(gameVersionField, 1, 0);
GridPane.setColumnSpan(nameField, 1);
}
});
StackPane categoryStackPane = new StackPane();
JFXComboBox<CategoryIndented> categoryComboBox = new JFXComboBox<>();
categoryComboBox.getItems().setAll(new CategoryIndented(0, 0));
categoryStackPane.getChildren().setAll(categoryComboBox);
categoryComboBox.prefWidthProperty().bind(categoryStackPane.widthProperty());
categoryComboBox.getStyleClass().add("fit-width");
categoryComboBox.setPromptText(i18n("mods.category"));
categoryComboBox.getSelectionModel().select(0);
Task.supplyAsync(() -> CurseModManager.getCategories(getSkinnable().section))
.thenAcceptAsync(Schedulers.javafx(), categories -> {
List<CategoryIndented> result = new ArrayList<>();
result.add(new CategoryIndented(0, 0));
for (CurseModManager.Category category : categories) {
resolveCategory(category, 0, result);
}
categoryComboBox.getItems().setAll(result);
}).start();
searchPane.add(categoryStackPane, 0, 1);
StackPane sortStackPane = new StackPane();
JFXComboBox<String> sortComboBox = new JFXComboBox<>();
sortStackPane.getChildren().setAll(sortComboBox);
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
sortComboBox.getStyleClass().add("fit-width");
sortComboBox.setPromptText(i18n("search.sort"));
sortComboBox.getItems().setAll(
i18n("curse.sort.date_created"),
i18n("curse.sort.popularity"),
i18n("curse.sort.last_updated"),
i18n("curse.sort.name"),
i18n("curse.sort.author"),
i18n("curse.sort.total_downloads"));
sortComboBox.getSelectionModel().select(0);
searchPane.add(sortStackPane, 1, 1);
VBox vbox = new VBox();
vbox.setAlignment(Pos.CENTER_RIGHT);
searchPane.add(vbox, 0, 2, 2, 1);
JFXButton searchButton = new JFXButton();
searchButton.setText(i18n("search"));
searchButton.setOnAction(e -> {
getSkinnable().search(gameVersionField.getText(),
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
.map(CategoryIndented::getCategoryId)
.orElse(0),
0,
nameField.getText(),
sortComboBox.getSelectionModel().getSelectedIndex());
});
searchPane.add(searchButton, 0, 2);
vbox.getChildren().setAll(searchButton);
}
SpinnerPane spinnerPane = new SpinnerPane();
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
JFXListView<CurseAddon> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
CurseAddon selectedItem = listView.getSelectionModel().getSelectedItem();
Controllers.navigate(new ModDownloadPage(selectedItem, getSkinnable().version.get(), getSkinnable().callback));
});
listView.setCellFactory(x -> new FloatListCell<CurseAddon>() {
TwoLineListItem content = new TwoLineListItem();
ImageView imageView = new ImageView();
{
Region clippedContainer = (Region) listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
}
@Override
protected void updateControl(CurseAddon dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getName());
content.setSubtitle(dataItem.getSummary());
content.getTags().setAll(dataItem.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
for (CurseAddon.Attachment attachment : dataItem.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
}
});
}
pane.getChildren().setAll(searchPane, spinnerPane);
getChildren().setAll(pane);
}
private static class CategoryIndented {
private final int indent;
private final int categoryId;
public CategoryIndented(int indent, int categoryId) {
this.indent = indent;
this.categoryId = categoryId;
}
public int getIndent() {
return indent;
}
public int getCategoryId() {
return categoryId;
}
@Override
public String toString() {
return StringUtils.repeats(' ', indent) + i18n("curse.category." + categoryId);
}
}
private static void resolveCategory(CurseModManager.Category category, int indent, List<CategoryIndented> result) {
result.add(new CategoryIndented(indent, category.getId()));
for (CurseModManager.Category subcategory : category.getSubcategories()) {
resolveCategory(subcategory, indent + 1, result);
}
}
}
}
@@ -0,0 +1,245 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadPage extends Control implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final ListProperty<CurseAddon.LatestFile> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final CurseAddon addon;
private final Profile.ProfileVersion version;
private final DownloadCallback callback;
public ModDownloadPage(CurseAddon addon, Profile.ProfileVersion version, DownloadCallback callback) {
this.addon = addon;
this.version = version;
this.callback = callback;
File versionJar = StringUtils.isNotBlank(version.getVersion())
? version.getProfile().getRepository().getVersionJar(version.getVersion())
: null;
Task.runAsync(() -> {
if (StringUtils.isNotBlank(version.getVersion())) {
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
if (gameVersion.isPresent()) {
List<CurseAddon.LatestFile> files = CurseModManager.getFiles(addon);
items.setAll(files.stream()
.filter(file -> file.getGameVersion().contains(gameVersion.get()))
.collect(Collectors.toList()));
return;
}
}
List<CurseAddon.LatestFile> files = CurseModManager.getFiles(addon);
items.setAll(files);
}).start();
this.state.set(State.fromTitle(i18n("mods.download.title", addon.getName())));
}
public CurseAddon getAddon() {
return addon;
}
public Profile.ProfileVersion getVersion() {
return version;
}
public boolean isLoading() {
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
}
public boolean isFailed() {
return failed.get();
}
public BooleanProperty failedProperty() {
return failed;
}
public void setFailed(boolean failed) {
this.failed.set(failed);
}
public void download(CurseAddon.LatestFile file) {
this.callback.download(version.getProfile(), version.getVersion(), file);
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override
protected Skin<?> createDefaultSkin() {
return new ModDownloadPageSkin(this);
}
private static class ModDownloadPageSkin extends SkinBase<ModDownloadPage> {
protected ModDownloadPageSkin(ModDownloadPage control) {
super(control);
BorderPane pane = new BorderPane();
HBox descriptionPane = new HBox(8);
descriptionPane.setAlignment(Pos.CENTER);
pane.setTop(descriptionPane);
descriptionPane.getStyleClass().add("card");
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
content.setTitle(getSkinnable().addon.getName());
content.setSubtitle(getSkinnable().addon.getSummary());
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
ImageView imageView = new ImageView();
for (CurseAddon.Attachment attachment : getSkinnable().addon.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
JFXButton openUrlButton = new JFXButton();
openUrlButton.getStyleClass().add("toggle-icon4");
openUrlButton.setGraphic(SVG.launchOutline(Theme.blackFillBinding(), -1, -1));
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getWebsiteUrl()));
descriptionPane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content, openUrlButton);
SpinnerPane spinnerPane = new SpinnerPane();
pane.setCenter(spinnerPane);
{
spinnerPane.loadingProperty().bind(getSkinnable().loadingProperty());
spinnerPane.failedReasonProperty().bind(Bindings.createStringBinding(() -> {
if (getSkinnable().isFailed()) {
return i18n("download.failed.refresh");
} else {
return null;
}
}, getSkinnable().failedProperty()));
JFXListView<CurseAddon.LatestFile> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
listView.setCellFactory(x -> new FloatListCell<CurseAddon.LatestFile>() {
TwoLineListItem content = new TwoLineListItem();
StackPane graphicPane = new StackPane();
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
}
container.getChildren().setAll(graphicPane, content);
}
@Override
protected void updateControl(CurseAddon.LatestFile dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getDisplayName());
content.getTags().setAll(dataItem.getGameVersion());
switch (dataItem.getReleaseType()) {
case 1: // release
graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.release"));
break;
case 2: // beta
graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.snapshot"));
break;
case 3: // alpha
graphicPane.getChildren().setAll(SVG.alphaCircleOutline(Theme.blackFillBinding(), 24, 24));
content.getTags().add(i18n("version.game.snapshot"));
break;
}
}
});
listView.setOnMouseClicked(e -> {
if (listView.getSelectionModel().getSelectedIndex() < 0)
return;
CurseAddon.LatestFile selectedItem = listView.getSelectionModel().getSelectedItem();
getSkinnable().download(selectedItem);
});
}
getChildren().setAll(pane);
}
}
public interface DownloadCallback {
void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file);
}
}
@@ -23,7 +23,6 @@ import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
@@ -90,10 +89,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
Region clippedContainer = (Region)listView.lookup(".clipped-container");
setPrefWidth(0);
HBox container = new HBox(8);
container.setPadding(new Insets(0, 0, 0, 6));
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
pane.setPadding(new Insets(8, 8, 8, 0));
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
@@ -17,22 +17,24 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -40,27 +42,25 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionPage extends Control implements DecoratorPage {
public class VersionPage extends Control implements DecoratorPage, ModDownloadPage.DownloadCallback {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty();
private final JFXListView<String> listView = new JFXListView<>();
private final TabHeader.Tab versionSettingsTab = new TabHeader.Tab("versionSettingsTab");
private final VersionSettingsPage versionSettingsPage = new VersionSettingsPage();
private final TabHeader.Tab modListTab = new TabHeader.Tab("modListTab");
private final ModListPage modListPage = new ModListPage(modListTab);
private final TabHeader.Tab curseModListTab = new TabHeader.Tab("modListTab");
private final ModDownloadListPage curseModListPage = new ModDownloadListPage(CurseModManager.SECTION_MOD, this);
private final TabHeader.Tab installerListTab = new TabHeader.Tab("installerListTab");
private final InstallerListPage installerListPage = new InstallerListPage();
private final TabHeader.Tab worldListTab = new TabHeader.Tab("worldList");
@@ -68,63 +68,43 @@ public class VersionPage extends Control implements DecoratorPage {
private final TransitionPane transitionPane = new TransitionPane();
private final ObjectProperty<TabHeader.Tab> selectedTab = new SimpleObjectProperty<>();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private Profile profile;
private String version;
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private String preferredVersionName = null;
{
Profiles.registerVersionsListener(this::loadVersions);
listView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null && !Objects.equals(oldValue, newValue))
loadVersion(newValue, profile);
if (newValue == null && !Objects.equals(oldValue, newValue)) {
if (listView.getItems().contains(preferredVersionName)) {
loadVersion(preferredVersionName, profile);
} else if (!listView.getItems().isEmpty()) {
loadVersion(listView.getItems().get(0), profile);
}
}
});
versionSettingsTab.setNode(versionSettingsPage);
modListTab.setNode(modListPage);
curseModListTab.setNode(curseModListPage);
installerListTab.setNode(installerListPage);
worldListTab.setNode(worldListPage);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
}
private void loadVersions(Profile profile) {
HMCLGameRepository repository = profile.getRepository();
List<String> 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::getId)
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
this.profile = profile;
loading.set(false);
listView.getItems().setAll(children);
}
selectedTab.set(versionSettingsTab);
FXUtils.onChangeAndOperate(selectedTab, newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
}
public void setVersion(String version, Profile profile) {
this.version = version;
this.profile = profile;
this.version.set(new Profile.ProfileVersion(profile, version));
}
public void loadVersion(String version, Profile profile) {
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (this.version.get() != null && (!getProfile().getRepository().isLoaded() ||
!getProfile().getRepository().hasVersion(version))) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
setVersion(version, profile);
preferredVersionName = version;
listView.getSelectionModel().select(version);
versionSettingsPage.loadVersion(profile, version);
curseModListPage.loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
CompletableFuture.allOf(
@@ -134,63 +114,71 @@ public class VersionPage extends Control implements DecoratorPage {
}
private void onNavigated(Navigator.NavigationEvent event) {
if (this.version == null || this.profile == null)
if (this.version.get() == null)
throw new IllegalStateException();
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (!this.profile.getRepository().isLoaded() ||
!this.profile.getRepository().hasVersion(version)) {
if (!getProfile().getRepository().isLoaded() ||
!getProfile().getRepository().hasVersion(getVersion())) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
loadVersion(this.version, this.profile);
loadVersion(getVersion(), getProfile());
}
private void onBrowse(String sub) {
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
FXUtils.openFolder(new File(getProfile().getRepository().getRunDirectory(getVersion()), sub));
}
private void redownloadAssetIndex() {
Versions.updateGameAssets(profile, version);
Versions.updateGameAssets(getProfile(), getVersion());
}
private void clearLibraries() {
FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries"));
FileUtils.deleteDirectoryQuietly(new File(getProfile().getRepository().getBaseDirectory(), "libraries"));
}
private void clearJunkFiles() {
Versions.cleanVersion(profile, version);
Versions.cleanVersion(getProfile(), getVersion());
}
private void testGame() {
Versions.testGame(profile, version);
Versions.testGame(getProfile(), getVersion());
}
private void updateGame() {
Versions.updateVersion(profile, version);
Versions.updateVersion(getProfile(), getVersion());
}
private void generateLaunchScript() {
Versions.generateLaunchScript(profile, version);
Versions.generateLaunchScript(getProfile(), getVersion());
}
private void export() {
Versions.exportVersion(profile, version);
Versions.exportVersion(getProfile(), getVersion());
}
private void rename() {
Versions.renameVersion(profile, version)
.thenApply(newVersionName -> this.preferredVersionName = newVersionName);
Versions.renameVersion(getProfile(), getVersion())
.thenApply(newVersionName -> this.preferredVersionName = newVersionName);
}
private void remove() {
Versions.deleteVersion(profile, version);
Versions.deleteVersion(getProfile(), getVersion());
}
private void duplicate() {
Versions.duplicateVersion(profile, version);
Versions.duplicateVersion(getProfile(), getVersion());
}
public Profile getProfile() {
return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::getProfile).orElse(null);
}
public String getVersion() {
return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::getVersion).orElse(null);
}
@Override
@@ -203,9 +191,30 @@ public class VersionPage extends Control implements DecoratorPage {
return state.getReadOnlyProperty();
}
@Override
public void download(Profile profile, @Nullable String version, CurseAddon.LatestFile file) {
if (version == null) {
throw new InternalError();
}
Path dest = profile.getRepository().getRunDirectory(version).toPath().resolve("mods").resolve(file.getFileName());
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getDownloadUrl()), dest.toFile());
task.setName(file.getDisplayName());
return task;
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}
public static class Skin extends SkinBase<VersionPage> {
String currentVersion;
private JFXPopup listViewItemPopup;
/**
@@ -220,47 +229,30 @@ public class VersionPage extends Control implements DecoratorPage {
listViewItemPopup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(() -> {
Versions.testGame(getSkinnable().profile, currentVersion);
Versions.testGame(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(() -> {
Versions.generateLaunchScript(getSkinnable().profile, currentVersion);
Versions.generateLaunchScript(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> {
Versions.renameVersion(getSkinnable().profile, currentVersion).thenApply(name -> getSkinnable().preferredVersionName = name);
Versions.renameVersion(getSkinnable().getProfile(), getSkinnable().getVersion()).thenApply(name -> getSkinnable().preferredVersionName = name);
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> {
Versions.duplicateVersion(getSkinnable().profile, currentVersion);
Versions.duplicateVersion(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> {
Versions.deleteVersion(getSkinnable().profile, currentVersion);
Versions.deleteVersion(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> {
Versions.exportVersion(getSkinnable().profile, currentVersion);
Versions.exportVersion(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> {
Versions.openFolder(getSkinnable().profile, currentVersion);
Versions.openFolder(getSkinnable().getProfile(), getSkinnable().getVersion());
}, listViewItemPopup))
);
FXUtils.smoothScrolling(control.listView);
control.listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
control.listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
@Override
public ListCell<String> call(ListView<String> param) {
JFXListCell<String> cell = new JFXListCell<>();
cell.setOnMouseClicked(e -> {
if (cell.getItem() == null) return;
currentVersion = cell.getItem();
if (e.getButton() == MouseButton.SECONDARY) {
listViewItemPopup.show(cell, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
}
});
return cell;
}
});
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
@@ -268,58 +260,79 @@ public class VersionPage extends Control implements DecoratorPage {
BorderPane root = new BorderPane();
{
StackPane drawerContainer = new StackPane();
FXUtils.setLimitWidth(drawerContainer, 200);
drawerContainer.getStyleClass().add("gray-background");
drawerContainer.getChildren().setAll(control.listView);
FXUtils.setOverflowHidden(drawerContainer, 8);
BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200);
root.setLeft(left);
StackPane wrapper = new StackPane(drawerContainer);
wrapper.setPadding(new Insets(4, 0, 4, 4));
root.setLeft(wrapper);
}
AdvancedListItem versionSettingsItem = new AdvancedListItem();
versionSettingsItem.getStyleClass().add("navigation-drawer-item");
versionSettingsItem.setTitle(i18n("settings"));
versionSettingsItem.setLeftGraphic(wrap(SVG.gearOutline(null, 20, 20)));
versionSettingsItem.setActionButtonVisible(false);
versionSettingsItem.activeProperty().bind(control.selectedTab.isEqualTo(control.versionSettingsTab));
versionSettingsItem.setOnAction(e -> control.selectedTab.set(control.versionSettingsTab));
AdvancedListItem modListItem = new AdvancedListItem();
modListItem.getStyleClass().add("navigation-drawer-item");
modListItem.setTitle(i18n("mods"));
modListItem.setLeftGraphic(wrap(SVG.puzzle(null, 20, 20)));
modListItem.setActionButtonVisible(false);
modListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.modListTab));
modListItem.setOnAction(e -> control.selectedTab.set(control.modListTab));
AdvancedListItem curseModListItem = new AdvancedListItem();
curseModListItem.getStyleClass().add("navigation-drawer-item");
curseModListItem.setTitle(i18n("mods.download"));
curseModListItem.setLeftGraphic(wrap(SVG.fire(null, 20, 20)));
curseModListItem.setActionButtonVisible(false);
curseModListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.curseModListTab));
curseModListItem.setOnAction(e -> control.selectedTab.set(control.curseModListTab));
AdvancedListItem installerListItem = new AdvancedListItem();
installerListItem.getStyleClass().add("navigation-drawer-item");
installerListItem.setTitle(i18n("settings.tabs.installers"));
installerListItem.setLeftGraphic(wrap(SVG.cube(null, 20, 20)));
installerListItem.setActionButtonVisible(false);
installerListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.installerListTab));
installerListItem.setOnAction(e -> control.selectedTab.set(control.installerListTab));
AdvancedListItem worldListItem = new AdvancedListItem();
worldListItem.getStyleClass().add("navigation-drawer-item");
worldListItem.setTitle(i18n("world"));
worldListItem.setLeftGraphic(wrap(SVG.gamepad(null, 20, 20)));
worldListItem.setActionButtonVisible(false);
worldListItem.activeProperty().bind(control.selectedTab.isEqualTo(control.worldListTab));
worldListItem.setOnAction(e -> control.selectedTab.set(control.worldListTab));
AdvancedListBox sideBar = new AdvancedListBox()
.add(versionSettingsItem)
.add(modListItem)
.add(curseModListItem)
.add(installerListItem)
.add(worldListItem);
left.setCenter(sideBar);
TabHeader tabPane = new TabHeader();
tabPane.setPickOnBounds(false);
tabPane.getStyleClass().add("jfx-decorator-tab");
control.versionSettingsTab.setText(i18n("settings"));
control.modListTab.setText(i18n("mods"));
control.installerListTab.setText(i18n("settings.tabs.installers"));
control.worldListTab.setText(i18n("world"));
tabPane.getTabs().setAll(
control.versionSettingsTab,
control.modListTab,
control.installerListTab,
control.worldListTab);
control.selectedTab.bind(tabPane.getSelectionModel().selectedItemProperty());
FXUtils.onChangeAndOperate(tabPane.getSelectionModel().selectedItemProperty(), newValue -> {
control.transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
HBox toolBar = new HBox();
toolBar.setAlignment(Pos.TOP_RIGHT);
toolBar.setPickOnBounds(false);
{
PopupMenu browseList = new PopupMenu();
JFXPopup browsePopup = new JFXPopup(browseList);
browseList.getContent().setAll(
new IconedMenuItem(null, i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> control.onBrowse(""), browsePopup)),
new IconedMenuItem(null, i18n("folder.mod"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("mods"), browsePopup)),
new IconedMenuItem(null, i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("config"), browsePopup)),
new IconedMenuItem(null, i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("resourcepacks"), browsePopup)),
new IconedMenuItem(null, i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("screenshots"), browsePopup)),
new IconedMenuItem(null, i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("saves"), browsePopup))
new IconedMenuItem(FXUtils.limitingSize(SVG.gamepad(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(() -> control.onBrowse(""), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.puzzle(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.mod"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("mods"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.gearOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.config"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("config"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.texture(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.resourcepacks"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("resourcepacks"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.monitorScreenshot(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.screenshots"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("screenshots"), browsePopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.gamepad(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.saves"), FXUtils.withJFXPopupClosing(() -> control.onBrowse("saves"), browsePopup))
);
PopupMenu managementList = new PopupMenu();
JFXPopup managementPopup = new JFXPopup(managementList);
managementList.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(control::testGame, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.launchOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(control::testGame, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(control::generateLaunchScript, managementPopup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(control::rename, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(control::duplicate, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.deleteOutline(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(control::export, managementPopup)),
new MenuSeparator(),
new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), FXUtils.withJFXPopupClosing(control::redownloadAssetIndex, managementPopup)),
@@ -327,60 +340,67 @@ public class VersionPage extends Control implements DecoratorPage {
new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(control::clearJunkFiles, managementPopup)).addTooltip(i18n("version.manage.clean.tooltip"))
);
JFXButton upgradeButton = new JFXButton();
FXUtils.setLimitWidth(upgradeButton, 40);
FXUtils.setLimitHeight(upgradeButton, 40);
upgradeButton.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
upgradeButton.getStyleClass().add("jfx-decorator-button");
upgradeButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
upgradeButton.setOnAction(event -> control.updateGame());
upgradeButton.visibleProperty().bind(control.currentVersionUpgradable);
FXUtils.installFastTooltip(upgradeButton, i18n("version.update"));
AdvancedListItem upgradeItem = new AdvancedListItem();
upgradeItem.getStyleClass().add("navigation-drawer-item");
upgradeItem.setTitle(i18n("version.update"));
upgradeItem.setLeftGraphic(wrap(SVG.update(Theme.blackFillBinding(), 20, 20)));
upgradeItem.setActionButtonVisible(false);
upgradeItem.visibleProperty().bind(control.currentVersionUpgradable);
upgradeItem.setOnAction(e -> control.updateGame());
JFXButton testGameButton = new JFXButton();
FXUtils.setLimitWidth(testGameButton, 40);
FXUtils.setLimitHeight(testGameButton, 40);
testGameButton.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
testGameButton.getStyleClass().add("jfx-decorator-button");
testGameButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
testGameButton.setOnAction(event -> control.testGame());
FXUtils.installFastTooltip(testGameButton, i18n("version.launch.test"));
AdvancedListItem testGameItem = new AdvancedListItem();
testGameItem.getStyleClass().add("navigation-drawer-item");
testGameItem.setTitle(i18n("version.launch.test"));
testGameItem.setLeftGraphic(wrap(SVG.launchOutline(Theme.blackFillBinding(), 20, 20)));
testGameItem.setActionButtonVisible(false);
testGameItem.setOnAction(e -> control.testGame());
JFXButton browseMenuButton = new JFXButton();
FXUtils.setLimitWidth(browseMenuButton, 40);
FXUtils.setLimitHeight(browseMenuButton, 40);
browseMenuButton.setGraphic(SVG.folderOpen(Theme.whiteFillBinding(), 20, 20));
browseMenuButton.getStyleClass().add("jfx-decorator-button");
browseMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
browseMenuButton.setOnAction(event -> browsePopup.show(browseMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, browseMenuButton.getHeight()));
FXUtils.installFastTooltip(browseMenuButton, i18n("settings.game.exploration"));
AdvancedListItem browseMenuItem = new AdvancedListItem();
browseMenuItem.getStyleClass().add("navigation-drawer-item");
browseMenuItem.setTitle(i18n("settings.game.exploration"));
browseMenuItem.setLeftGraphic(wrap(SVG.folderOutline(Theme.blackFillBinding(), 20, 20)));
browseMenuItem.setActionButtonVisible(false);
browseMenuItem.setOnAction(e -> browsePopup.show(browseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, browseMenuItem.getWidth(), 0));
JFXButton managementMenuButton = new JFXButton();
FXUtils.setLimitWidth(managementMenuButton, 40);
FXUtils.setLimitHeight(managementMenuButton, 40);;
managementMenuButton.setGraphic(SVG.wrench(Theme.whiteFillBinding(), 20, 20));
managementMenuButton.getStyleClass().add("jfx-decorator-button");
managementMenuButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
managementMenuButton.setOnAction(event -> managementPopup.show(managementMenuButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, managementMenuButton.getHeight()));
FXUtils.installFastTooltip(managementMenuButton, i18n("settings.game.management"));
AdvancedListItem managementItem = new AdvancedListItem();
managementItem.getStyleClass().add("navigation-drawer-item");
managementItem.setTitle(i18n("settings.game.management"));
managementItem.setLeftGraphic(wrap(SVG.wrenchOutline(Theme.blackFillBinding(), 20, 20)));
managementItem.setActionButtonVisible(false);
managementItem.setOnAction(e -> managementPopup.show(managementItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, managementItem.getWidth(), 0));
toolBar.getChildren().setAll(upgradeButton, testGameButton, browseMenuButton, managementMenuButton);
AdvancedListBox toolbar = new AdvancedListBox()
.add(upgradeItem)
.add(testGameItem)
.add(browseMenuItem)
.add(managementItem);
toolbar.getStyleClass().add("advanced-list-box-clear-padding");
FXUtils.setLimitHeight(toolbar, 40 * 4 + 12 * 2);
left.setBottom(toolbar);
}
BorderPane titleBar = new BorderPane();
titleBar.setLeft(tabPane);
titleBar.setRight(toolBar);
control.state.set(new State(i18n("version.manage.manage"), titleBar, true, false, true));
control.state.bind(Bindings.createObjectBinding(() ->
new State(i18n("version.manage.manage.title", getSkinnable().getVersion()), null, true, false, true, false, 200),
getSkinnable().version));
control.transitionPane.getStyleClass().add("gray-background");
FXUtils.setOverflowHidden(control.transitionPane, 8);
StackPane wrapper = new StackPane(control.transitionPane);
wrapper.setPadding(new Insets(4));
root.setCenter(wrapper);
//control.transitionPane.getStyleClass().add("gray-background");
//FXUtils.setOverflowHidden(control.transitionPane, 8);
root.setCenter(control.transitionPane);
spinnerPane.loadingProperty().bind(control.loading);
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
}
}
public static Node wrap(Node node) {
StackPane stackPane = new StackPane();
FXUtils.setLimitWidth(stackPane, 30);
FXUtils.setLimitHeight(stackPane, 20);
stackPane.setPadding(new Insets(0, 10, 0, 0));
stackPane.getChildren().setAll(node);
return stackPane;
}
}
Loaded 100 of 326 files, more files were not shown because too many files have changed in this diff. Show more