Compare commits

...
Author SHA1 Message Date
Glavo 0a230ef6b5 优化 Release Note (#3472) 2024-11-23 19:48:46 +08:00
Burning_TNT c12ae9fdf8 Fix #3460 无法识别奇异的 Forge (Like) 模组 Metadata (#3470) 2024-11-21 21:22:20 +08:00
Glavo 773ef4282c Bump action-gh-release to v2 (#3469) 2024-11-17 22:09:29 +08:00
Burning_TNTandYELANDAOKONG 9edfe29659 实现 #2969: 启动前日志列出mods目录的jar文件 (#3125)
* Support #2969: 启动前日志列出mods目录的jar文件

* Support #2969: 启动前日志列出mods目录jar文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 日志列出mods目录时不再过滤文件后缀

* Support #2969: 启动时日志列出mods目录(代码优化)

* Support #2969: 启动时日志列出mods目录(不再列出Zip内容)

* Support #2969

* Fix: checkstyle

---------

Co-authored-by: YELANDAOKONG <yelandaokong@yldk.xyz>
2024-11-16 23:29:53 +08:00
Burning_TNT 5591d92e25 Fix #2991: 导入 PrismLauncher 格式整合包时 $.jvmArgs 参数未正确移除两侧引号 (#3030) 2024-11-15 22:49:30 +08:00
Glavo 65bdb29e71 在 macOS 上使用 JavaFX 19.0.2.1 (#3461) 2024-11-15 18:02:59 +08:00
Glavo 355df211db 在游戏下载列表中添加 Minecraft Wiki 链接 (#3435) 2024-11-14 17:49:27 +08:00
3gf8jv4dv fa66549f53 Revise English (#3453)
- Adjust some translation styles again
- Fix some missed mistakes
2024-11-14 09:59:15 +08:00
3gf8jv4dv 6e48dbd6c6 Fix dialog title when modpack download fails (#3451)
* Fix dialog title when modpack download fails

* Update
2024-11-14 09:58:14 +08:00
Zkitefly e3c7278759 Update check-update.yml (#3452) 2024-11-14 09:42:43 +08:00
3gf8jv4dv dd1829d8ca Check word 其它 → 其他 when checking translation (#3455)
There is some controversy over the use of this word.
In order to ensure consistency in formal texts, we
should unify them to more tried-and-true and commonly
used usages.

By checking at build time, it lets the translator
or proofreader correct the word on his/her own without
any special attention from the reviewer.
2024-11-14 09:27:45 +08:00
3gf8jv4dv 60ac16a3ea Revise Chinese (#3454)
* Revise Chinese

- Simplified Chinese only
- Minor revision

* oops...
2024-11-13 22:14:55 +08:00
Glavo b400a9a3ef 使最小化按钮居中 (#3450)
* 修复最小化按钮未居中的问题

* update
2024-11-12 14:24:15 +08:00
3gf8jv4dv c7cc2d0733 Fix missing i18n when checking for updates fails (#3444)
* Fix missing i18n when checking for updates fails

* Revise translation
2024-11-11 19:20:28 +08:00
Glavo 861223e909 清理 GameJavaVersion (#3441)
* 清理 GameJavaVersion

* update
2024-11-11 02:30:41 +08:00
Glavo 70ecbc734e Fix #3438: 修复 Minecraft 1.20.2+Forge 默认图标错误的问题 (#3439) 2024-11-10 23:19:14 +08:00
ZkiteflyandGlavo 2ca7e7c830 Fix #3200: 启动时不应补全 Modrinth 整合包中已禁用的 mod (#3308)
* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* update

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-11-10 21:27:36 +08:00
Wulian 9a0db56ac3 修复 Copyright © 显示为“漏”的bug (#3437)
Fixes #3436

Specify UTF-8 charset when reading `deps.json` file in `AboutPage.java`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/HMCL-dev/HMCL/issues/3436?shareId=XXXX-XXXX-XXXX-XXXX).
2024-11-10 19:02:59 +08:00
Glavo c1ae5875ba 将 VERSION_ROOT 更新至 3.6 (#3431) 2024-11-10 13:01:50 +08:00
Glavo 1d0ae427bf Update JavaVersionConstraint: 1.20.5 requires Java 21 (#3434) 2024-11-09 19:39:09 +08:00
3gf8jv4dv 3dde2d19e2 Fix extra "=" in zh_CN launch.advice translation (#3430)
Closes #3429
2024-11-08 05:59:02 +08:00
3gf8jv4dv d5ec6d7b85 Revise i18n translation before releases (#3428)
- Fix some English grammar and text style
- Revise `Allow Cheats` to `Allow Commands/Cheats`
  - Due to the change from `Allow Cheats` to `Allow Commands` by Mojang in 24w04a.
  - https://minecraft.wiki/w/Java_Edition_24w04a
2024-11-07 21:37:59 +08:00
Glavo 13856b71a0 更新 HMCLauncher.exe (#3427) 2024-11-05 03:43:18 +08:00
Glavo c6658771ca 修复 HMCLauncher 无法正常识别 Windows on Arm 平台的问题 (#3425)
* update

* update

* update
2024-11-05 00:20:42 +08:00
Glavo 406f36584d 设置 FontComboBox 的最小宽度 (#3424) 2024-11-04 18:55:04 +08:00
Glavo 898750d058 修复对话框按钮自行移动的问题 (#3423)
* 修复对话框按钮自行移动的问题

* update

* update

* update
2024-11-04 18:41:14 +08:00
Glavo 09e9e934d3 Fix #3412: 修复点击前置模组时,模组下载对话框不会关闭的问题 (#3422) 2024-11-04 06:00:45 +08:00
Glavo 3d170267b7 Fix #3416: 修复打开游戏设置页时崩溃的问题 (#3421) 2024-11-04 05:34:28 +08:00
3gf8jv4dv 3ca7e3cf88 Fix incorrect i18n key and line breaks in es & ja (#3404)
* Fix incorrect i18n key and line breaks in es & ja

* fix
2024-11-03 17:03:41 +08:00
3gf8jv4dv 1378d1445a README & PLATFORM docs post-fixes (#3417) 2024-11-02 17:36:41 +08:00
3gf8jv4dv 16d304dbb4 en_US Post-fix (#3413)
* en_US Post-fix

Some minor fixes.

* fix `game.crash.feedback`

* oops...

* oops...

* Bunch of grammar fixes :/

* Another bunch fixes

* Minor fixes

* fix

* fix

* fix
2024-11-02 17:36:20 +08:00
3gf8jv4dv aff5db7759 Change the terminology %AppData% to %APPDATA% (#3420) 2024-11-02 02:27:54 +08:00
Glavo 0697aa9755 重命名包 moe.mickey.minecraft.skin.fx (#3419) 2024-11-02 01:05:52 +08:00
Glavo 995d3bd4b9 不再生成 bss 文件 (#3418) 2024-11-01 20:59:33 +08:00
Glavo 3bf5204e79 优化 Theme (#3414) 2024-11-01 02:12:16 +08:00
3gf8jv4dv 0b1ce6e94a Adjust language selection order (#3411)
Sort alphabetically by language code.
2024-10-31 23:30:44 +08:00
3gf8jv4dv a66ccd7385 Adjust Microsoft login related i18n (#3393)
* Adjust Microsoft login related i18n

- Adjust English and Chinese
- Fix typos and grammatical errors

* fix
2024-10-30 02:26:55 +08:00
3gf8jv4dv 7a3f125802 Adjust custom command hint (zh_CN only) (#3394)
I forgot to change the zh_CN part.
A half-width colon is better.
2024-10-30 02:22:34 +08:00
3gf8jv4dv 4a8dcce3d4 Adjust about page i18n (#3398)
* Adjust about page i18n

- Adjust all languages
- Add missing translations

* fix

* fix
2024-10-30 02:21:54 +08:00
3gf8jv4dv adcc3eb43f Adjust buttons, chat & crash i18n (#3401)
* Adjust buttons, chat & crash i18n

- Add missing translations

* fix

* fix

* fix

* fix ja

* fix es & ja
2024-10-30 02:20:39 +08:00
3gf8jv4dv 8bbd1a7cf4 Remove unused i18n keys in es, ja & ru (#3405) 2024-10-30 02:19:45 +08:00
Zkitefly 467ff26278 模组管理添加右键查看模组详情 (#3232)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java
2024-10-30 02:19:16 +08:00
Glavo d6a267afa4 优化 FXUtils.installTooltip (#3407)
* update

* Fix checkstyle

* update

* update

* update

* update

* update
2024-10-28 01:52:38 +08:00
Glavo 743e934436 优化“复制”图标 (#3406) 2024-10-28 00:29:29 +08:00
3gf8jv4dv f89ac2231a Adjust Java management i18n (#3402)
- Add missing languages.
2024-10-27 22:07:25 +08:00
3gf8jv4dv 1877a18ebb Fix en language missing a line break (#3403) 2024-10-27 22:07:03 +08:00
Glavo c99f85d9d5 缩小复制图标 (#3397)
* update

* update
2024-10-27 00:21:31 +08:00
Glavo 228942c60a 优化删除账号按钮图标 (#3396) 2024-10-26 23:49:43 +08:00
Glavo fecd567eab 简化 ContainerAnimations (#3395) 2024-10-26 23:34:41 +08:00
3gf8jv4dv 602840519b Change forgot password link (#3392) 2024-10-26 23:32:12 +08:00
959adf1075 更改主页 UI (#3129)
* Change root page UI

* Update HMCL/src/main/resources/assets/lang/I18N.properties

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>

* Shorten I18N

* update

---------

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-26 21:02:34 +08:00
Glavo 422f681fe6 Bump XZ to 1.10 (#3391) 2024-10-26 20:54:48 +08:00
Glavo 474d298fd3 Bump Gradle to 8.10.2 (#3390) 2024-10-26 13:52:49 +08:00
3gf8jv4dv 4052136217 Adjust sponsor hint i18n (#3387)
- Adjust for all languages.
- For BMCLAPI: Make it clear that this is a service
  for users in Chinese Mainland.
- For HMCL: Since the page that was opened was not
  Afdian but the GitHub repo page, I made adjustments
  based on the actual situation.
- Fix some typos.
2024-10-26 02:51:19 +08:00
3gf8jv4dv 3193e02867 Adjust feedback page i18n (#3389)
- Adjust for all languages.
- Add missing translations to some languages.
2024-10-26 02:49:58 +08:00
Glavo c7122b3fff 优化首页公告 (#3378)
* update

* 添加公告隐藏动画

* update

* update
2024-10-26 02:45:58 +08:00
3gf8jv4dv 8775194d26 Adjust i18n for Linux/FreeBSD-specific settings in Advanced Settings (#3388)
I checked the code and it looked like this was a
Linux/FreeBSD-specific feature, so I adjusted the
translation for that.
2024-10-26 01:14:28 +08:00
3gf8jv4dv b3f27cc96e Adjust homepage hint typesetting (#3382)
- Adjustments made for all languages.
- Remove outdated hidden hint links.
2024-10-25 22:30:12 +08:00
3gf8jv4dv 3313a1ed27 For language selection, remove all region descriptions (#3383)
Currently, HMCL does not provide variants for
languages ​​(e.g. US, UK, etc.). For Chinese, there
are already descriptions of Simplified (简体) and
Traditional (繁體) to distinguish them, so keep them.
2024-10-25 22:05:19 +08:00
3gf8jv4dv da66622994 Adjust Microsoft login hint (zh_CN only) (#3384) 2024-10-25 22:04:48 +08:00
3gf8jv4dv 446cdc32d7 Adjust language file header comments (#3385)
- Update copyright year
- Unify typesetting style
2024-10-25 22:03:52 +08:00
3gf8jv4dv b08d2b0dc0 Remove some unnecessary tooltips (#3386)
* Remove some unnecessary tooltips

These texts are basically fixed, and the problem of
too long texts can be solved by revising the translation
for different languages.

Currently, for both Chinese and English, the i18n
keys involved in this PR will not be partially obscured
due to long text, so their tooltips are removed.

* fix checkstyle
2024-10-25 16:11:16 +08:00
Glavo e051c212d1 支持使用鼠标返回键返回上级 (#3381)
* 支持使用鼠标返回键返回上级

* fix checkstyle
2024-10-25 12:36:07 +08:00
Glavo 7645a600ef 修复部分控件响应任意鼠标按键点击事件的问题 (#3380)
* update

* update

* update

* update

* update

* update

* update

* update

* update
2024-10-25 12:24:57 +08:00
Glavo 9e56b5cd15 清理对 setOnMouseClicked 的误用 (#3379) 2024-10-25 04:08:40 +08:00
3gf8jv4dvandGlavo dcfcd1d8b2 Revise zh_TW part 3 (#3358)
* Revise zh_TW part 3

* Update

* Update

* fix typo

* fix

* fix

* oops...

* fix

* fix main page hint

* Update HMCL/src/main/resources/assets/lang/I18N_zh.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-25 02:25:26 +08:00
3gf8jv4dv b2dc4b45c2 Revise zh_TW part 2 (#3357)
* Revise zh_TW part 2

* Update

* fix typo

* fix

* fix

* fix

* fix

When choosing a download source, the commas in the BMCLAPI item are changed to half-width.
2024-10-25 01:47:22 +08:00
0314921728 Revise en_US (#3367)
* Revise en_US

* fix

* fix

* Revise

Co-authored-by: Zkitefly <2573874409@qq.com>

* fix some English grammar

* another English grammar fixes

* fix fix fix...

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: Zkitefly <2573874409@qq.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-25 01:28:02 +08:00
Glavo 7d339677e4 调整公告中超链接的风格 (#3377)
* 调整 MessageDialogPane 中超链接的风格

* Fix checkstyle

* fix
2024-10-24 21:50:05 +08:00
3gf8jv4dv c9d259d7f1 zh_CN post-fix (#3376)
- When choosing a download source, the commas in the BMCLAPI item are changed to half-width.
- Remove extra line breaks in the home page hint.
2024-10-24 21:32:37 +08:00
Glavo eae2670855 支持在启动器内上传微软账号皮肤 (#3375)
* update

* Fix clearCache
2024-10-24 20:03:21 +08:00
3gf8jv4dvandGlavo e857f19b18 Revise zh_CN part 4 (#3349)
* Revise zh_CN part 4

* Update

* fix some typo

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-24 14:25:37 +08:00
Glavo a11816884c 创建 @FXThread 注解 (#3374) 2024-10-24 01:01:31 +08:00
3gf8jv4dv f686cb0ceb Update versions.txt (#3373) 2024-10-23 23:00:02 +08:00
Glavo 5aac4b82fc 优化 Java 下载功能 (#3371)
* update

* update

* update

* update
2024-10-23 21:49:35 +08:00
Glavo 99c3ef8a57 优化 Java 下载功能 (#3369)
* update

* update

* update
2024-10-20 22:26:38 +08:00
3gf8jv4dv 0a17aad9d6 Revise readme (#3362) 2024-10-20 21:36:01 +08:00
lijiaxuanand3gf8jv4dv 965995ad20 略微修改了一下英文版的readme.md (#3354)
* Updated English version of readme.md

修改了英文版的readme.md,并未增加或删除内容,只是让一些句子更加自然了一些

* Update README.md

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-20 21:35:27 +08:00
Glavo a5067cb1dd Bump JavaFX to 17.0.13 (#3364) 2024-10-20 21:33:55 +08:00
3gf8jv4dv 1f9c0736aa Capitalize choose player dialog title text (#3368) 2024-10-20 21:33:35 +08:00
Glavo a27671560d 支持下载 OpenJ9 (#3363)
* 支持下载 OpenJ9

* update
2024-10-19 22:59:40 +08:00
Glavo 5c767deaf3 优化 WorldInfoPage (#3361)
* update

* update

* update
2024-10-19 22:25:47 +08:00
3gf8jv4dv c157d4e0d1 Revise zh_TW part 4 (#3359) 2024-10-19 21:48:30 +08:00
3gf8jv4dv 655f42734a Revise zh_TW part 1 (#3356)
* Revise zh_TW part 1

* Update

* Update

* Update
2024-10-19 21:41:54 +08:00
3gf8jv4dv 55b01ab101 Revise zh_CN part 3 (#3348)
* Revise zh_CN part 3

* Update
2024-10-19 21:35:57 +08:00
3gf8jv4dv c6ed42d3e6 Revise zh_CN part 2 (#3346)
* Revise zh_CN part 2

* Update

* Oops...
2024-10-19 21:35:22 +08:00
0562051989 Fix mod update channel i18n (#3331)
* Fix mod update channel i18n

* Update

Co-authored-by: Burning_TNT <pangyl08@163.com>

* update

---------

Co-authored-by: Burning_TNT <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-19 21:34:11 +08:00
3gf8jv4dvandBurning_TNT f68e056e47 Revise zh_CN part 1 (#3343)
* Revise zh_CN part 1

* Update

Co-authored-by: Burning_TNT <pangyl08@163.com>

---------

Co-authored-by: Burning_TNT <pangyl08@163.com>
2024-10-19 21:28:03 +08:00
Burning_TNTandGlavo 457490276c Update CallerFinder (#3350)
* Update CallerFinder

* Revert "Fix #2687 (#2688)"

This reverts commit 207b4d0033.

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-18 02:17:47 +08:00
Glavo 49ea9e5da0 fix checkstyle (#3353) 2024-10-17 12:48:17 +08:00
Glavo 9f3fb7aff6 修复无法安装 zip 格式的 Java 压缩包的问题 (#3352) 2024-10-17 04:20:09 +08:00
Glavo 1033389c38 Revert "Bump commons-compress to 1.27.1 (#3344)" (#3351)
This reverts commit fab32a6d51.
2024-10-16 22:15:13 +08:00
Glavo 58e32c4459 当整合包没有描述时,隐藏查看整合包描述按钮 (#3347) 2024-10-15 19:56:32 +08:00
Glavo 936285847c Bump Gson to 2.11.0 (#3345) 2024-10-15 19:10:02 +08:00
Glavo fab32a6d51 Bump commons-compress to 1.27.1 (#3344) 2024-10-15 19:05:22 +08:00
Glavo 5b345affec 优化 Java 下载功能 (#3342)
* update

* update
2024-10-15 18:47:57 +08:00
3gf8jv4dv 32c3b694b3 Revise readme and platform docs [skip ci] (#3054)
* Revise [skip ci]

Squash previous commits.

* Revise [skip ci]

Unify badge style.

* Revise [skip ci]

Change the badge style of QQ group

* Revise [skip ci]

- Add Chinese variants (Traditional, Taiwan)
- Compilation requirements for Java bump to 11+
- Various revisions

* Revise [skip ci]

Fix a link

* Revise [skip ci]

Revert a change

* Revise [skip ci]

* Revise [skip ci]

* Revise [skip ci]
2024-10-15 18:47:38 +08:00
3gf8jv4dv 4dce13855a Fix some i18n key mistake for es, ja & ru (#3341) 2024-10-15 18:26:12 +08:00
Burning_TNTandGlavo 1ce9858893 Support #2992: 检查模组更新界面加入全选/取消全选复选框 (#3032)
Support #2992

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-15 04:06:40 +08:00
Glavo 71c9632d61 GitHub Action 使用 Java 8 构建 HMCL (#3340)
* update

* update
2024-10-15 03:13:35 +08:00
3gf8jv4dv cce17edaa6 Capitalize text above the sidebar line (#3336) 2024-10-15 01:24:44 +08:00
3gf8jv4dvandGlavo deeef8f67a Download source and thread setting titles i18n key (#3338)
* Change the download source and thread setting titles to the appropriate i18n key

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-15 01:19:11 +08:00
3gf8jv4dv 25c76a748f Fix alignment of host and port in proxy settings (#3332) 2024-10-15 01:06:21 +08:00
3gf8jv4dv 2109df8145 Fix some link (#3334)
* Fix some link

* Revise
2024-10-15 00:58:40 +08:00
3gf8jv4dv ae01a49cc4 Fix some error message wrap (#3333)
* Fix some error message wrap

* Fix checkstyle
2024-10-15 00:57:23 +08:00
Glavo a4f5e013cb 修复使用 Java 8 构建失败的问题 (#3326) 2024-10-14 01:24:14 +08:00
Glavo d4579852bb 优化 AboutPage (#3325)
* 优化关于页面

* update
2024-10-14 01:08:06 +08:00
Zkitefly edd95ad626 模组管理搜索操作更改为文本更新后触发 (#3221)
* 更改为文本更新后触发

* 添加防抖
2024-10-13 21:57:32 +08:00
ZkiteflyandGlavo dec307b531 安装新版本的版本名输入框显示已选择加载器名称 (#3234)
* Update InstallersPage.java

* 优化代码

* Update InstallersPage.java

* Update InstallersPage.java

* fix: checkstyle

* 给 LibraryAnalyzer.LibraryType.fromPatchId 稍微优化一下,把她里面的实现改为一个预先生成的 map,

相信你知道怎么把这个函数改成一个 lookupMap.get(patchId) 的形式

* Update InstallersPage.java

* Update InstallersPage.java

* Fix

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-13 21:46:48 +08:00
Glavo 00b7085370 更新 Linux LoongArch64 支持 (#3264) 2024-10-13 20:54:15 +08:00
Glavo 17c1998119 修复对话框标题和按钮未使用自定义字体的问题 (#3324) 2024-10-13 20:39:55 +08:00
Glavo 1c05cd981f 使用 ./gradlew run 启动 HMCL 时解析 HMCL_JAVA_OPTS 环境变量 (#3323) 2024-10-13 20:20:05 +08:00
Glavo e9c3795ffc 预生成 openjfx-dependencies.json (#3322) 2024-10-13 19:52:00 +08:00
Zkiteflyand3gf8jv4dv c3917082ee 添加打开启动器日志文件夹按钮 (#3237)
* 添加打开启动器日志文件夹

* Update HMCL/src/main/resources/assets/lang/I18N.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-13 19:37:35 +08:00
3gf8jv4dv 6265580d75 Adjust the order of sidebars in instance settings (#3278) 2024-10-13 19:35:24 +08:00
YLDK 65fc3d0c4f Support #3087: 隐藏整合包文件位置显示 (#3088)
* Support #3087: 隐藏整合包文件位置显示

* Support #3087: 移除整合包文件位置显示代码

* Support #3087: 移除整合包文件位置显示代码
2024-10-13 19:29:02 +08:00
Glavo 1c864ba072 使用 Jsoup 解析并渲染 HTML 页面 (#3321) 2024-10-13 19:19:52 +08:00
Glavo ef9159666c 使用 TypeToken 替代 Type (#3320) 2024-10-11 20:50:58 +08:00
Glavo 541f2e2619 清理不再使用的 i18n 条目 (#3318) 2024-10-09 02:00:53 +08:00
Glavo c7f248a092 优化更新窗口 (#3317)
* 优化更新窗口

* fix NPE
2024-10-09 01:40:26 +08:00
Zkitefly 7937f8ddf5 发现更新弹窗添加取消按钮 (#3282) 2024-10-09 01:03:56 +08:00
Glavo 74926704bd Close #3209: 搜索 IntelliJ IDEA 安装的 Java (#3316) 2024-10-09 00:35:07 +08:00
Glavo 9196bda537 优化 FXUtils.testLinuxCommand (#3315) 2024-10-09 00:19:06 +08:00
Glavo 4c1e607b8e 在 Linux/FreeBSD 平台读取 os-release (#3314)
* 简化 OperatingSystem.getPhysicalMemoryStatus

* Read os-release

* update

* update
2024-10-07 00:57:35 +08:00
Zkitefly 26224ae366 删除账户和删除认证服务器时弹出提示确认删除 (#3280) 2024-10-06 23:38:48 +08:00
Glavo d3e9511aca 删除 FractureiserDetector (#3313) 2024-10-06 23:31:19 +08:00
ShulkerSakura b624cebc08 修改下载页面按钮名称 (#3262) 2024-10-06 23:21:12 +08:00
Glavo 7e4d437a1d 重构 Java 管理 (#2988)
* update

* update

* Update task name

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* Update logo
2024-10-05 23:50:14 +08:00
Zkitefly 37d6857b82 根据版本类型调整下载项图标 (#3306) 2024-10-03 00:31:06 +08:00
Zkitefly f8d7c92d51 Close #3208: 隐藏版本更新界面的Fabric/Quilt API选项 (#3271) 2024-09-24 03:32:02 +08:00
YSXX1013andGlavo ea9999a1f9 支持使用 CMake 与 MinGW 构建 HMCLauncher (#3011)
* 尝试性的为HMCLauncher添加cmake及gcc支持

* 完善cmake及gcc支持

* 尝试支持Github Actions

* 尝试支持Github Actions-2

* 尝试支持Github Actions-3

* 尝试支持Github Actions-4

* 尝试支持Github Actions-5

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-24 02:51:44 +08:00
3gf8jv4dv d969fab284 Fix checkstyle (#3292) 2024-09-19 17:42:58 +08:00
vochant 97fe52c2a7 feat: Supported JRE from SDKMAN! on Linux (#3062) 2024-09-19 03:09:05 +08:00
Glavo 8a816f7f35 优化 LogWindow (#3274)
* update

* update

* update

* update

* update
2024-09-02 20:54:33 +08:00
6409841cca Fix #3138: HMCL 错误地将 Authlib 日志识别为 ERROR (#3142)
* Close#3138

* 恢复gradle-wrapper.properties

* 恢复gradle-wrapper.properties

* 添加空格

* 修复控制台输出的日志

* Merge if statement

* update

* update

---------

Co-authored-by: CiLuQwQ <a@>
Co-authored-by: burningtnt <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-02 01:51:53 +08:00
ZkiteflyandGlavo 6348166dac 支持在 Linux/BSD 平台使用 showFileInExplorer (#3233)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update FXUtils.java

* Update ModListPageSkin.java

* 使用 dbus-send

* Update FXUtils.java

* Update FXUtils.java

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-01 00:27:06 +08:00
ZkiteflyandGlavo 48417b9b7b 显示 Forge 发版时间 (#3256)
* 显示时间

* Fix

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-31 00:44:43 +08:00
Burning_TNT 738000ff39 移除无用类与方法 (#3132)
* Remove useless classes.

* Remove useless methods
2024-08-25 03:03:48 +08:00
ZkiteflyandGlavo 9b5445b686 修复模组详情页图标问题 (#3227)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* update

* 调整判断逻辑

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-25 02:55:55 +08:00
Haowei Wen abf58b2c20 Fix missing i18n key curse.category.7418 (#3260) 2024-08-24 23:44:13 +08:00
Zkitefly fd97f6c321 支持 Forge 官方源 (#3259)
* Revert "Revert "支持 Forge 官方源 (#3251)" (#3258)"

This reverts commit abd37a093a.

* Update ForgeVersionList.java
2024-08-24 23:43:50 +08:00
Glavo abd37a093a Revert "支持 Forge 官方源 (#3251)" (#3258)
This reverts commit 0f0cf55509.
2024-08-24 14:50:41 +08:00
Zkitefly 0f0cf55509 支持 Forge 官方源 (#3251)
* 支持 Forge 官方源

* 更新 ForgeVersionList.java

* 更新 ForgeVersionList.java
2024-08-24 03:32:45 +08:00
ZkiteflyandGlavo 95a1496389 Fix #3223: 正确解析包含 hash 的版本号 (#3224)
* Fix #3223

* 使用正则表达式删除内容

* 添加日志

添加日志

* Update GameVersion.java

* Update GameVersion.java

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-20 01:08:49 +08:00
Glavo 47cc9ebe2b Fix #3199: 修复运行在 Java 8 上时游戏安装界面卡片布局的问题 (#3201) 2024-07-29 15:46:11 +08:00
Glavo 3ebbdb60ef Fix #3191: 修复无法同时安装 LiteLoader 与 Forge 的问题 (#3198) 2024-07-28 02:47:52 +08:00
Burning_TNTandGlavo e8306ea59a 修复 #3099 自动安装选项卡重新选择版本(Minecraft版本和模组加载器版本)的按钮消失的问题 (#3117)
* Fix #3099

* Fix: checkstyle

* Fix.

* Fix #3144

* Update

* Fix bugs.

* Change I18N

* Fix #3164

* Delete State.SEALED

* fix typo

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-07-28 02:41:08 +08:00
Glavo 6f7c922ac0 更新多平台支持 (#3197)
* update

* Update PLATFORM.md

* Update NativePatcher

* Update NativePatcher

* Update NativePatcher
2024-07-27 09:16:33 +08:00
Burning_TNT 74abecaae7 实现 #3095 微软登录界面对 XBox 400 错误给予提示 (#3194) 2024-07-22 23:06:20 +08:00
Burning_TNTandGlavo ad5b74d8c7 修复 2830 本地模组查看信息时标签错误 (#2838)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes.

* Render the raw category when no i18n is provided.

* Fix #2830.

* Fix checkstyle.

* format file

* Remove the redundant lock.

* Update ModListPageSkin

* Update ModListPageSkin

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-07-21 00:58:33 +08:00
Burning_TNT 5f84e8cfeb 修复 1.8 及 1.9 安装 OptiFine 的问题 (#3130)
* Fix: 1.8 & 1.9 OptiFine Install

* Fix: checkstyle
2024-07-20 20:09:36 +08:00
Burning_TNT 519ed0ad5f 清理 RemoteModRepository (#2798)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes.

* Render the raw category when no i18n is provided.

* Fix checkstyle.
2024-07-20 05:29:30 +08:00
Burning_TNT 61096142d5 Fix #3065: 修复 CurseForge 搜索 API 对翻页总量计算不正确的 Bug (#3066)
* Fix #3065

* Fix

* Fix.
2024-07-20 05:03:08 +08:00
Burning_TNT 7c7c36f8aa 修复 #3112 模组详情中若没有这个游戏版本,仍然会出现该下拉栏的问题 (#3114) 2024-07-20 05:00:19 +08:00
Glavo dbb614dce1 Fix #3051: 支持解析更多版本号 (#3058)
* Fix #3051: 支持解析更多版本号

* Update GameVersionNumberTest

* Update versions.txt

* Update GameVersionNumber

* Update GameVersionNumberTest

* Update GameVersionNumberTest

* Update GameVersionNumberTest
2024-07-20 04:58:51 +08:00
Burning_TNT 5a4825d419 修复 #3080 微软账户无法正确读取 Token 过期时间的问题 (#3126) 2024-07-20 04:05:58 +08:00
Glavo d87cf91cdf 删除 I18N 文件中重复的条目 (#3190)
* 删除 I18N 文件中重复的项目

* update

* update
2024-07-20 02:18:41 +08:00
Burning_TNT 0782f40fe0 修复 #3127 1.21 NeoForge 下载问题 (#3128) 2024-07-20 01:57:04 +08:00
Burning_TNTandGlavo b7a5b484b5 [重要] 实现 #3095 微软登录界面对 XBox 400 错误给予提示 (#3121)
* Fix #3095

* Fix

* Update I18N_zh_CN.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-07-20 01:56:13 +08:00
Burning_TNT 1e472ea463 Fix: NPE in LauncherHelper. (#3160) 2024-07-20 01:44:26 +08:00
Burning_TNT 816a8539ea 修复 bug-report.yml 中的无效链接 (#3159) 2024-07-18 00:40:34 +08:00
Glavo c81e510bfa 支持使用环境变量指定字体 (#3181) 2024-07-15 12:33:53 +08:00
Haowei Wen 85b68ad135 Fix findMinecraftRuntimeDirs() is broken on Linux/FreeBSD 2024-06-30 13:28:49 +08:00
Burning_TNTandGlavo 7d5d74d0c6 修复 #3135 Forge 1.7.10-pre4 安装问题 (#3136)
* Fix #3135

* Update ForgeBMCLVersionList.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-06-21 23:24:16 +08:00
Burning_TNT e0ecd5e695 修复 #3131 1.7.10-pre4 版本 Forge 安装问题 (#3133)
* Fix #3131

* Fix.
2024-06-17 20:16:55 +08:00
z0z0r4 5e0687c138 Update CurseForgeRemoteModRepository.java (#3022)
432 is gameid for minecraft

https://docs.curseforge.com/#get-fingerprints-matches-by-game-id

https://github.com/Hex-Dragon/PCL2/blob/d682b598032bc4c7e3b1367cae4d51d037007ae9/Plain%20Craft%20Launcher%202/Modules/Minecraft/ModMod.vb#L1029-L1030
2024-06-15 22:31:49 +08:00
Glavo b827167a3d 将 1.21 添加至 versions.txt (#3118) 2024-06-14 00:45:48 +08:00
Burning_TNT 20711c9d10 Fix #3107 修复 HMCL 3.5.8.248 无法安装启动带 forge 的 1.11.2 版本的问题 (#3113) 2024-06-14 00:20:25 +08:00
Burning_TNT 15c1042991 Fix #3086 修复模组下载界面版本推荐功能 (#3089)
* Fix #3086

* Update
2024-05-31 23:12:33 +08:00
Burning_TNT 1275e38995 Fix #3006: 全局游戏设置中版本隔离开启后的显示的路径的版本号为”null“ (#3028) 2024-05-31 23:10:02 +08:00
Glavo 312a2e0cde 更新 Linux ARM64 支持 (#3097) 2024-05-31 23:06:38 +08:00
Burning_TNT 35ce95857a Fix #3026: resources 目录应该跟随游戏目录 (#3027) 2024-05-31 22:48:19 +08:00
Burning_TNT 6b78f56298 修复自动安装在 1.20.4 的严重问题 (#3023)
* Fix: OptiFine and Forge has compatible problem on 1.20.4 . Fix: Forge changes it's main class on 1.20.4 .

* Fix: HMCL cannot display the analyed versions installed by external launchers.

* Refuse to configure installable widgets when it's from external process.

* Add javadoc about LibraryStatus

* Adapt to the new change of Forge: https://github.com/MinecraftForge/MinecraftForge/commit/b8270cd9ff71678bd71f93938b35e5904b5f507a

* Re-arrange entries.

* Fix.
2024-05-31 22:02:56 +08:00
Glavo b43c8ceae5 移除 KOOK 入口 (#3092)
* 移除 KOOK 入口

* update
2024-05-31 21:41:41 +08:00
Burning_TNT 704d4e9cc0 Fix #3079 修复 NeoForge 整合包版本识别不正确的问题 (#3082)
* Fix #3079

* Fix.

* Fix: checkstyle.
2024-05-28 08:28:35 +08:00
CalbootandGlavo 96471e3a92 Fix #3071: 在 HMCL 被终结时停止日志线程 (#3081)
* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-05-28 08:17:55 +08:00
Burning_TNT af3719403d Support #3000: Support neoforge.mods.toml (#3031)
* Support #3000

* Fix #2371

* Rename references.

* Code cleanup.
2024-05-15 22:24:48 +08:00
Liyan Zhao e0764d6f0c support java from homebrew (#3035)
* fix: support java from homebrew

* fix: support java from homebrew

* fix: add ~/Library/Java/JavaVirtualMachine
2024-05-14 00:15:07 +08:00
zkitefly 908f47f75e Update Copyright (#3010) 2024-05-13 23:45:46 +08:00
Glavo a3af719087 支持自动下载 Java 21 (#3045) 2024-05-13 23:41:23 +08:00
zkitefly 7f13d6646b Update ExitWaiter.java (#3042) 2024-05-12 20:36:21 +08:00
Kieran Moy e8b4c38451 Add cinnamon desktop support (#3017) 2024-05-07 23:40:18 +08:00
Glavo 38ba6802a0 Bump Hub Mirror Action to 1.4 (#3034) 2024-05-07 23:36:28 +08:00
Glavo 381eead7b6 将 1.20.5 和 1.20.6 添加至 versions.txt (#3008)
* 更新 versions.txt

* update
2024-05-07 23:27:22 +08:00
3gf8jv4dv df953e5ea7 Fix NeoForge 2x icon not working (#2994) 2024-04-17 22:00:28 +08:00
Glavo 2fd2954a06 更新指向 XZ 主页的链接 (#2990) 2024-04-15 05:37:04 +08:00
Glavo a8bb5c1570 修复导入部分整合包失败的问题 (#2989)
* 修复导入部分整合包失败的问题

* update

* update
2024-04-15 04:02:24 +08:00
zkitefly a33377d1f2 add macos_failed_to_find_service_port_for_display (#2797)
* add macos_failed_to_find_service_port_for_display

日志来自 HMCL KOOK

* updata i18n

* Update I18N_zh.properties
2024-04-15 02:38:59 +08:00
zkiteflyandGlavo e515542add 还原 MCBBS 的鸣谢 (#2958)
* Update AboutPage.java

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-04-15 02:35:20 +08:00
zkitefly 48b6bd4ea1 add NEOFORGE_FOREST_OPTIFINE (#2976)
* add NEOFORGE_FOREST_OPTIFINE

* fix
2024-04-15 02:10:02 +08:00
Liyan Zhao 9cf23f69a6 Fix Microsoft authentication (#2986)
* fix: microsoft login #2979

* fix: typo
2024-04-15 02:07:47 +08:00
Glavo 4104ca082d 修复日志文件夹不存在时无法将日志写入文件的问题 (#2984) 2024-04-11 19:43:18 +08:00
Glavo 32f8ffad46 修复整合包下载失败提示窗标题未正确格式化的问题 (#2980)
* 修复 i18n 问题

* update

* update
2024-04-08 21:49:54 +08:00
Glavo a5e04a7b81 将 24w14potato 添加至 versions.txt (#2962)
* 将 24w14potato 添加至 versions.txt

* Update test
2024-04-02 13:55:52 +08:00
Glavo 57018bef47 重新实现日志记录 (#2951)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
2024-03-28 12:16:52 +08:00
Glavo 0b65431044 清理残余 MCBBS 相关内容 (#2953)
* 清理残余 MCBBS 相关内容

* update
2024-03-25 21:44:27 +08:00
Glavo 991729b684 支持从当前路径加载字体 (#2950) 2024-03-24 19:59:38 +08:00
MythandGlavo 1605eb1775 fix: add locale to avoid wrong yggdrasil server name (#2521)
* fix: add locale to avoid wrong yggdrasil server name

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-23 14:19:16 +08:00
Glavo 94ccee0b76 优化 I18n (#2948)
* update

* update

* update

* update

* update
2024-03-23 14:11:52 +08:00
Glavo 8b94fe5b0b 更新模组和整合包信息 (#2947)
* update

* update
2024-03-23 11:31:09 +08:00
Glavo abec1abcff 更新默认皮肤 (#2943)
* 更新默认皮肤

* update

* update

* update

* update

* update

* update
2024-03-22 16:09:25 +08:00
Glavo 127938fa38 支持更多格式的版本图标 (#2942) 2024-03-21 20:01:21 +08:00
Glavo 1187ae4f04 Fix #2938: 修复 DefaultLauncher 会多次添加包装命令的问题 (#2939) 2024-03-16 11:08:17 +08:00
Glavo 8efdc536ee Fix #2934: 修复无法读取远古版本版本号的问题 (#2936)
* update

* update
2024-03-15 20:57:45 +08:00
Glavo 78f080b6cc Fix #2784: 离线账户选择 Steve/Alex 皮肤时强制使用本地 Yggdrasil 服务器 (#2933) 2024-03-15 13:39:50 +08:00
Glavo cc8bcfd924 Fix i18n (#2931)
* Fix i18n

* update
2024-03-15 12:56:29 +08:00
Glavo 806ca3f64a 更新 Modrinth 翻译 (#2929)
* update

* update

* update

* update

* update

* update
2024-03-15 12:24:43 +08:00
lyuxc_ 4a16fa13ee Update ModpackInfoPage.java (#2928) 2024-03-14 21:23:06 +08:00
Glavo b88d7c4fd2 修复 saveETagIndex 可能写入不全的问题 (#2926) 2024-03-14 19:24:30 +08:00
Burning_TNTandGlavo c0838468b1 Close #2911: 下载失败时打印重定向链 (#2912)
* Support #2911.

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-14 13:50:25 +08:00
Glavo 90cfe3d0b7 加载网络背景时设置超时 (#2856) 2024-03-13 16:25:55 +08:00
Burning_TNT baf1d22bc4 Fix bugs. (#2920)
* Always sort the results while searching in CurseForge.

* Fix bugs.
2024-03-12 22:14:32 +08:00
Glavo 7ca5744559 允许使用系统属性禁用系统代理 (#2921) 2024-03-12 21:44:08 +08:00
Glavo 57e728991b Fix #1938: 正确处理 map_to_resources (#2919)
* Fix #1938: 正确处理 map_to_resources

* fix checkstyle
2024-03-12 20:01:30 +08:00
Glavo e08ba4d421 不再尝试下载 .pack.xz 文件 (#2917)
* 不再尝试下载 .pack.xz 文件

* update
2024-03-11 20:21:17 +08:00
Burning_TNT ba91c60e95 Always sort the results while searching in CurseForge. (#2916) 2024-03-10 22:45:34 +08:00
389b93178e fix #2796 (#2804)
* fix https://github.com/HMCL-dev/HMCL/issues/2796

* 调整 game.crash.feedback 显示位置

* fix i18n

* 添加导出崩溃信息失败的弹窗

* updata

* 在崩溃窗口的日志窗口添加更多信息

* fix i18n

* Update HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Co-authored-by: 何杰豪 <hejiehao12@126.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Co-authored-by: 何杰豪 <hejiehao12@126.com>

* Revert "在崩溃窗口的日志窗口添加更多信息"

This reverts commit e8ed43291e.

* updata

* add log

* updat log

* Update GameCrashWindow.java

* 添加更多日志输出

格式:Processing log file: /home/zkitefly/.minecraft/hs_err_pid20074.log, processStartTime: 1709952457315, lastModifiedTime: 1709952504054

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: 何杰豪 <hejiehao12@126.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-10 22:02:56 +08:00
Glavo 37aaf153f8 更新文本和链接 (#2915)
* update

* update

* update

* update

* update

* update
2024-03-10 20:29:44 +08:00
ShulkerSakuraandGlavo 2249f4ee0e 在未知桌面环境禁用 moveToTrash (#2885)
* Added Optifine Icon

* Updated Optifine Icons

* Updated Optifine Icons

* Added new HMCL logo

* update

* Add ChromeOS Detection

* ChromeOS Detection

* Removed useless import

* Style Checked

* Add private constructor

* Style Check

* Simplification

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-09 20:38:48 +08:00
Glavo eadfff349a Revert "auth: Always check token.exp when launch game (#2642)" (#2908)
This reverts commit 8678c0dcf2.
2024-03-09 20:25:59 +08:00
Glavo cb4cd696a0 修复未初始化 fileDownloadProvider 的问题 (#2906) 2024-03-08 16:26:37 +08:00
Glavo c85c074eee 更改语言应在重启后生效 (#2905) 2024-03-08 15:04:40 +08:00
Glavo b26c478d4a 更新 I18N (#2904)
* 更新 I18N

* 更新 I18N
2024-03-08 14:21:05 +08:00
Glavo 2d83de7c31 简化 InvocationDispatcher (#2674)
* 优化 InvocationDispatcher

* update

* Rename type parameter

* update
2024-03-08 10:06:08 +08:00
Glavo 20d93dd085 清理 GlobalConfig (#2903) 2024-03-08 10:01:01 +08:00
Glavo 24fc436870 将首页联机入口替换为入群链接 (#2900) 2024-03-06 18:29:51 +08:00
Glavo 3a6c23aa9d 将部分 KOOK 入口替换为 QQ 群 (#2902) 2024-03-06 14:38:13 +08:00
Glavo 0ee8b2dd82 指引用户加入 QQ 群 (#2897) 2024-03-05 23:57:25 +08:00
Glavo 7cce90bed8 删除 Mojang 登录支持 (#2889)
* 删除 Mojang 登录支持

* update

* update
2024-03-05 15:07:15 +08:00
Glavo c806a5d412 Update gradle.yml (#2895) 2024-03-05 14:22:45 +08:00
Glavo 14e8b9212f 更新 HMCLauncher 图标 (#2887) 2024-03-05 13:19:29 +08:00
Glavo 67e96fb84f 使用 GitHub 构建 HMCLauncher 后构建 HMCL (#2893)
* update

* update

* test

* update

* update

* update
2024-03-05 06:28:51 +08:00
Glavo c39c19f215 仅在必要时运行 GitHub Action (#2892)
* 仅在必要时运行 GitHub Action

* test

* test

* test

* update
2024-03-05 05:23:34 +08:00
Glavo 88d7493870 更新 GitHub Action 脚本 (#2891)
* update

* update
2024-03-05 05:11:21 +08:00
Glavo 404bc88ec8 使用 GitHub Action 构建 HMCLauncher (#2890)
* 使用 GitHub Action 构建 HMCLauncher

* test

* update

* update

* update

* update
2024-03-05 04:59:30 +08:00
Glavo e2355ca2ff 暂时移除赞助页面 (#2888) 2024-03-05 02:27:16 +08:00
Glavo aab271ac75 添加 GameJavaVersion.JAVA_21 (#2886)
* 将 GameJavaVersion 转换为 enum

* update
2024-03-05 00:35:29 +08:00
Glavo 0da0a1f386 默认不进行降级更新 (#2862)
* 默认不进行降级更新

* update
2024-03-04 16:19:30 +08:00
Glavo 59316a866d Fix #2880: 支持安装带有 NeoForge 的 Modrinth 整合包 (#2881) 2024-03-03 20:37:11 +08:00
zkitefly d3087bd763 更新 README (#2877) 2024-03-03 08:53:20 +08:00
Glavo df7b63c491 更新 README (#2872)
* 更新 README

* 更新 README

* update
2024-03-02 01:29:49 +08:00
Glavo dff893ba4b Fix #2870: 导出脚本时预解析 PreLaunchCommand 与 PostExitCommand (#2871)
* Fix #2870: 导出脚本时预解析 PreLaunchCommand 与 PostExitCommand

* fix checkstyle
2024-03-02 00:01:02 +08:00
Glavo c539702a1b 修复下载 Java 不会尝试备用下载源的问题 (#2869)
* 修复下载 Java 不会尝试备用下载源的问题

* Fix checkstyle

* Fix checkstyle
2024-03-01 21:38:04 +08:00
Glavo 6fad149410 更新 Release Note (#2868)
* 更新 Release Note

* update
2024-03-01 17:01:06 +08:00
Glavo a072fb7bea 改进任务栏图标质量 (#2864)
* 改进任务栏图标质量

* update
2024-02-29 02:03:42 +08:00
Glavo 08a3850dcf Fix #2861: MaintainTask 中不应调用 getGameVersion (#2863)
* Fix #2861: MaintainTask 中不应调用 getGameVersion

* update

* fix checkstyle
2024-02-29 00:06:50 +08:00
Glavo 030fba4630 Fix #2855: tokenize 应支持变量插值 (#2858)
* Fix #2855: tokenize 应支持变量插值

* update

* update

* update

* update

* update
2024-02-27 21:12:51 +08:00
Glavo 559aefd6b6 Fix #2854: 修复 LibraryAnalyzer.analyze 无法获得游戏版本的问题 (#2859)
* Fix #2854: 修复 LibraryAnalyzer.analyze 无法获得游戏版本的问题

* delete debug code

* update
2024-02-27 21:12:35 +08:00
Burning_TNT 2ee5d9ceed Fix #2831. (#2857) 2024-02-27 16:35:00 +08:00
zkitefly 20ed42ab8a 移除残留的 MCBBS 按钮 (#2849)
* remove

* fix
2024-02-25 01:20:42 +08:00
Glavo 815b90845a Fix #2850: fix i18n (#2852)
* Fix #2850: fix i18n

* update

* update

* update

* update
2024-02-25 00:56:29 +08:00
zkitefly e9846ea644 zkitefly-add-thanks-to-@ZekerZhayard (#2807)
* zkitefly-add-thanks-to-@ZekerZhayard

* fix

* add image
2024-02-25 00:25:15 +08:00
Glavo 02065094ca 修复使用 Java 19+ 启动游戏时控制台乱码的问题 (#2846) 2024-02-22 23:20:30 +08:00
Burning_TNTandGlavo 9f6124d617 Remove the link of MCBBS in README.md. (#2844)
* Remove the link of MCBBS in README.md.

* Remove MCBBS in sponsor.bmclapi

* Update I18N_zh.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-21 21:54:50 +08:00
Glavo 76229a894c Fix Linux RISC-V 64 Support (#2840) 2024-02-20 02:41:24 +08:00
Glavo 03de1598c8 在 Jenkins CI 上使用腾讯云镜像下载 Gradle Wrapper (#2837)
* 使用腾讯云下载 Gradle Wrapper

* Create config-jenkins.sh

* update script
2024-02-20 01:42:51 +08:00
Glavo 3a13ff0e10 更新 downloadType 的默认值 (#2839) 2024-02-19 22:15:08 +08:00
Glavo 63345f52ce 更新 PUBLISH_URL (#2836)
* 更新 PUBLISH_URL

* update
2024-02-19 17:43:22 +08:00
Burning_TNT 0bff282d3d Fix #2826. (#2827) 2024-02-19 17:28:55 +08:00
Burning_TNTandGlavo a81462425d Fix #2762: 光影应当被下载至 ./shaderpacks 下 (#2770)
* Fix #2762: Shader packs should be downloaded in to the ./shaderpacks directory.

* Make the network requests parallely in order to make CurseCompletionTask faster.

* Support displaying the process.

* Fix: checkstyle

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 17:28:07 +08:00
Burning_TNT e9ae43b1f1 Fix 2774: Local NeoForge mods shouldn't be identified as Forge mods. (#2775) 2024-02-19 16:19:48 +08:00
zkitefly dc31f0d5c9 Updata slug (#2801)
* add curse.category.4736

* add

* 调整顺序
2024-02-19 16:12:33 +08:00
Burning_TNTandGlavo 76f11b6a6f Fix #2799 (#2800)
* Fix #2799

* Remove codes in ConfigUpgrader.

* delete collectDFU

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 16:07:49 +08:00
zkiteflyandGlavo d6ea2ef1f9 updata-incomplete_forge_installation (#2834)
* updata-incomplete_forge_installation

* Update CrashReportAnalyzer.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 15:54:21 +08:00
zkitefly 14db21211f updata Exitwaiter (#2813)
* 添加检测日志

参考自 https://github.com/Hex-Dragon/PCL2/blob/8c8b1066bff6476a14437a14bc16a281e25145bd/Plain%20Craft%20Launcher%202/Modules/Minecraft/ModWatcher.vb#L209-L228

* updata
2024-02-19 15:47:52 +08:00
zkiteflyandGlavo f354f65adb add-forge-found-duplicate-mods (#2828)
* add

* Update CrashReportAnalyzer.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 15:46:57 +08:00
zkitefly 83e8ea3735 把“自动选择并发数”选项默认开启 (#2805) 2024-02-19 15:41:28 +08:00
Glavo 0e9e9811eb 更新关于页面图标 (#2835) 2024-02-19 15:40:23 +08:00
huangyuhui 420d77de9f fix: remove thanks to mcbbs. 2024-02-19 15:21:10 +08:00
yuhuihuang 9361719a67 fix: remove mcbbs download source. 2024-02-19 14:48:44 +08:00
Burning_TNT b09a5868d4 Fix: Raw use of parameterized class 'VersionRange'. (#2815) 2024-02-16 17:53:33 +08:00
Burning_TNT 56c48e5595 Fix: English I18N for fatal.javafx.missing is incorrect. (#2811) 2024-02-15 19:56:50 +08:00
Burning_TNT 4d6cb547ea Fix #2809 本地模组列表搜索 (#2810) 2024-02-15 18:56:46 +08:00
Glavo 95ecd29018 Bump JUnit to 5.10.2 (#2803) 2024-02-14 13:42:23 +08:00
Glavo 5d26a106e9 使用 Instant 替代 Date (#2713)
* update

* update

* update

* update

* update

* Update

* update

* update

* Add more tests

* fix checkstyle
2024-02-13 15:29:54 +08:00
zkitefly 95707560ef fix icon (#2791) 2024-02-13 14:14:27 +08:00
zkitefly c54cb88db8 add Crash report analysis (#2790)
* fix i18n

* add

https://github.com/HMCL-dev/HMCL/discussions/1904#discussioncomment-4339947
2024-02-13 14:11:21 +08:00
Glavo 95afaa5112 更新游戏版本比较规则 (#2700)
* Create GameVersionNumber

* Update GameVersionNumber

* Rename DefaultVersionNumber

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update versions.txt

* update

* update

* update
2024-02-13 13:46:01 +08:00
Glavo fe608e32f1 Fix #2783: Alex 皮肤应为 Slim 模型 (#2789) 2024-02-13 10:26:41 +08:00
Glavo 82fc1c067d Fix #2153: 正确显示离线账户头像 (#2788)
* update

* update

* update

* update

* fix checkstyle

* update
2024-02-13 10:08:26 +08:00
Glavo 94890e2924 Update javafx.gradle.kts (#2787) 2024-02-13 00:39:39 +08:00
Glavo 9a76b1ff27 Bump Gradle to 8.6 (#2786) 2024-02-13 00:35:45 +08:00
zkiteflyandGlavo 8db8693132 updata crash report analysis (#2785)
* updata OUT_OF_MEMORY

* Update CrashReportAnalyzer.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-12 23:31:29 +08:00
Glavo 59cc5b9114 Close #2766: 不应提示 Quilt 与 Fabric API 不兼容 (#2768)
* Close #2766

* update
2024-02-08 19:27:28 +08:00
Glavo c1acd0b0b5 删除 RemoteResourceManager (#2761)
* 删除 RemoteResourceManager

* update
2024-02-08 19:19:01 +08:00
Glavo fb6538f042 更新 Linux RISC-V 64 支持 (#2760) 2024-02-08 09:36:37 +08:00
Glavo 20fe5430dd Close #2757: 更新 MultiFileItem (#2758) 2024-02-07 22:37:56 +08:00
zkiteflyandGlavo 7de07319d9 Update CacheRepository.java (#2756)
* Update CacheRepository.java

https://github.com/FCL-Team/FoldCraftLauncher/commit/e96dd55395ed8107e70d8446b9a3ac14f0a17416

* Update CacheRepository.java

* Update CacheRepository.java

* Update CacheRepository.java

* Update CacheRepository.java

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-07 22:21:48 +08:00
zkitefly 8e3c19651e Fix check-update.yml (#2750)
https://github.com/softprops/action-gh-release/issues/236#issuecomment-1147866048
2024-02-06 21:07:43 +08:00
Gamom 2740bdeb77 Fix check-update.yml (#2747)
尝试修复workflow
2024-02-06 18:33:27 +08:00
Glavo 59ab49e0c5 Fix #2740 (#2746) 2024-02-06 17:16:23 +08:00
Glavo bc4aa43834 Fix #2744: 尝试从所有下载源下载 authlib-injector (#2745) 2024-02-06 17:05:14 +08:00
zkitefly c7f3f2a229 Update Fabric warnings pattern in CrashReportAnalyzer (#2742) 2024-02-06 14:47:38 +08:00
ffeabbf1f1 修改日志分析器 (#2495)
* 小小的修改下细节

* 修改细节

* 修改链接

* add failed_to_load_a_library

* 修改一些细节

* 发现重复项(

* add NEED_JDK11 rule

minecraft-exported-crash-info-2023-08-30T18-53-01.zip!/minecraft.log: https://mclo.gs/PGKK8xD

* 修改 Java 下载地址

* add INSTALL_MIXINBOOTSTRAP

minecraft-exported-crash-info-2023-08-31T20-24-28.zip!/minecraft.log: https://mclo.gs/0gmBukK

* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N_zh_CN.properties

* d

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* add INCOMPLETE_FORGE_INSTALLATION

* update lang

* Close https://github.com/HMCL-dev/HMCL/issues/2199. add mixin_apply_mod_failed

* fix

* Add crash report analyzer for RTSS and Sodium compatibility

* Fix compatibility issue with RivaTuner Statistics Server (RTSS) and Sodium

* Update CrashReportAnalyzer.java

---------

Co-authored-by: burningtnt <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-06 02:33:42 +08:00
Glavo c9ddb7aab4 改进 ModLoader 兼容性检测 (#2730)
* 改进 ModLoader 兼容性检测

* update
2024-02-04 19:40:23 +08:00
zkitefly 997552fb7e fix (#2737) 2024-02-04 19:32:23 +08:00
zkitefly 7d9595945d Add-thanks-to-@zkitefly (#2736)
* add

* add image

* fix image
2024-02-03 22:10:18 +08:00
Pigeon0v0 70157fa8de chores: Update Copyright (#2735) 2024-02-03 21:49:52 +08:00
zkitefly 22e1e6344f fix workflows (#2733) 2024-02-03 20:37:29 +08:00
ShulkerSakuraandGlavo 3952b6e3a8 Fresh icon (#2686)
* Added Optifine Icon

* Updated Optifine Icons

* Updated Optifine Icons

* Added new HMCL logo

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-03 13:55:51 +08:00
Glavo dd2a09399e 修复 AdditionalInstallersPage (#2731) 2024-02-03 04:33:17 +08:00
Burning_TNT 7f7068a556 Fix NeoForge modpack support (#2725)
* Fix: Cannot install modpacks with NeoForge for Minecraft 1.20.1.

* Add supports for MultiMC Modpack with NeoForge.
2024-02-02 01:22:56 +08:00
Glavo a79a2014b0 在游戏安装界面隐藏不可用的模组管理器 (#2726)
* 在 Minecraft 1.13+ 的安装界面隐藏 liteloader 选项

* update

* fix checkstyle

* fix checkstyle
2024-02-02 01:11:06 +08:00
Burning_TNT 4c41f25314 Fix: Env arg INST_NEOFORGE is missing in custom commands. (#2723) 2024-02-01 22:59:17 +08:00
zkitefly c26eb2bc69 Updata DownloadListButton I18n (#2715) 2024-01-31 16:25:28 +08:00
Glavo e93e5effa6 优化 BMCLAPIDownloadProvider::injectURL (#2712) 2024-01-31 03:22:56 +08:00
Burning_TNT 7acc39154b Fix: Cannot install modpacks with NeoForge for Minecraft 1.20.1. (#2708)
* Fix: Cannot install modpacks with NeoForge for Minecraft 1.20.1.

* Add supports for MultiMC Modpack with NeoForge.
2024-01-31 01:57:38 +08:00
Glavo 19cae84a01 尝试从候选源获取 AuthlibInjectorVersionInfo (#2711)
* 尝试从候选源获取 AuthlibInjectorVersionInfo

* update

* update

* update
2024-01-31 01:56:06 +08:00
Glavo 0bcda9eb46 删除 DecoratorController 中的背景图缓存 (#2710) 2024-01-31 01:09:11 +08:00
Burning_TNT 7556ef21c4 Fix: I18N for NeoForgeOldInstallTask was missing. (#2693) 2024-01-30 22:28:24 +08:00
Burning_TNT 9290c26c0b Close #2685: Support downloading NeoForge from official maven. (#2694) 2024-01-30 21:53:03 +08:00
Burning_TNT 5388020b35 Render required dependencies first. (#2695) 2024-01-30 21:42:51 +08:00
Burning_TNTandGlavo 017d688e32 Fix: Gradle scripts aren't compatible with Java 8. (#2706)
* Fix: Gradle scripts aren't compatible with Java 8.

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-30 21:39:16 +08:00
Glavo 4102029d6d update (#2702) 2024-01-30 12:31:14 +08:00
Glavo d286f93a7e 更新链接 (#2696) 2024-01-27 19:06:28 +08:00
Glavo 207b4d0033 Fix #2687 (#2688)
* Fix #2687

* fix checkstyle
2024-01-26 21:20:29 +08:00
Glavo 351a59149f 弃用 charsetCandidates (#2683)
* update

* create test
2024-01-26 04:37:54 +08:00
Glavo 7880c2f3d9 更新 PLATFORM.md (#2682) 2024-01-26 02:55:32 +08:00
Glavo 644866a24b 支持查看 NBT 文件 (#2402)
* init

* fix checkstyle

* update RootPage

* fix build

* update

* update
2024-01-26 01:24:31 +08:00
ShulkerSakura 43fee4878f Fresh icon (#2677)
* Added Optifine Icon

* Updated Optifine Icons

* Updated Optifine Icons
2024-01-26 00:32:07 +08:00
Glavo 1d8ead2cbb Close #2460: 为 Linux/FreeBSD 提供 moveToTrash 支持 (#2678)
* Close #2460: 为 Linux/FreeBSD 提供 moveToTrash 支持

* fix checkstyle

* Ignore milliseconds
2024-01-26 00:22:46 +08:00
Glavo 4cbf5ce979 Fix #2680: Revert TwoLineListItem (#2681) 2024-01-25 21:11:21 +08:00
Glavo 1eb53b247c 清理 org.jackhuang.hmcl.util (#2673)
* 合并 Holder 与 ReferenceHolder

* 清理代码

* 清理代码

* update

* update

* update
2024-01-24 03:07:56 +08:00
Burning_TNT f53aed9a08 Fix: NeoForge Support (#2632)
* Fix

* Fix: NeoForge may successfully made HMCL think it's Forge.

* Fix #2649

* Fix #2654 partly because MultiMC hasn't support NeoForge yet.

* Get ready for HMCL to support export and read MultiMC Modpacks with NeoForge.
2024-01-24 00:19:49 +08:00
5db3c123a0 I18n (#2468)
* 更新 I18N_zh_CN.properties

* 因为 MMC 不再支持 Curse 整合包,所以 MCBBS 整合包格式将无法被识别

* https://github.com/huanghongxun/HMCL/pull/1410

* 更改一些提示

* 修改语言文件

* 修改提示和一些细节

* (

抱歉(

* Update I18N_zh_CN.properties

* 将大部分 zh_CN 中的 Mod 字样换成 模组

* Revert "Update I18N_zh_CN.properties"

This reverts commit 79d99247b6.

* Update I18N_zh_CN.properties

* 改进I18N
~~眼睛都要瞎掉~~

* 把 Optifine 改成 OptiFine ,然后还加了点空格

* 一定又是IDEA的智慧的编码问题

* 增加锚点
~~一定是IDEA偷偷加这几行字的~~

* 加了亿点点锚点

* `Optifine` -> `OptiFine`

* #2524 不小心带来的翻译错误

* `Tencent` -> `Tencent Cloud`

* Revert

This reverts commit 387935d40c.

Revert "`Tencent` -> `Tencent Cloud`"

This reverts commit a7e11b4e8d.

* 删掉了一些东西

* Update .gitignore

---------

Co-authored-by: hejiehao <hejiehao12@126.com>
Co-authored-by: 何杰豪 <98140773+hejiehao@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-24 00:11:45 +08:00
Glavo e1e9215f2c 清理 CacheRepository (#2672) 2024-01-23 21:46:01 +08:00
Glavo 23f58e63aa Delete VersionNumber.VERSION_COMPARATOR (#2671) 2024-01-23 14:34:29 +08:00
Glavo 2cad6f33f5 Fix #2552: 对于 Minecraft 1.19+ 不应特殊处理中文路径 (#2670) 2024-01-23 11:22:54 +08:00
Glavo 308086b876 简化 VersionIconType 相关代码 (#2669) 2024-01-23 11:03:25 +08:00
Glavo 9bdf08af1e Fix #2665: newBuiltinImage 不应缓存经缩放的图像 (#2668) 2024-01-22 21:09:26 +08:00
ddcead7c85 Fix: version::compareTo compares the name, instead of the game version. (#2664)
* Fix: version.compareTo compares the name, but not the game version.

* Fix more.

* update

* update

Co-authored-by: zkitefly <z18344203426@qq.com>

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: zkitefly <z18344203426@qq.com>
2024-01-21 00:29:06 +08:00
Glavo 89b5b64943 放弃 HMCLauncher 与 Windows XP 的兼容性 (#2644)
* 放弃 HMCLauncher 与 Windows XP 的兼容性

* update

* Delete unused code

* Disable automatic updates on Windows XP/Vista
2024-01-21 00:06:25 +08:00
Burning_TNTandGlavo 931808d808 Allow updates when self verification is disabled (#2639)
* Allow updates when self verification is disabled

* clean up IntegrityChecker

* Fix: UpdateHandler would transfer the current command properties to the new HMCL.

* Fix.

* Fix.

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-20 16:15:28 +08:00
Glavo b9a6401ef3 Move DATE_TIME_FORMATTER to SupportedLocale (#2662)
* Move dateTimeFormatter to SupportedLocale

* update
2024-01-20 14:55:28 +08:00
Glavo 59a9b3a926 清理 ManagedProcess::getPid() (#2660)
* Delete getPID0

* Delete blank line
2024-01-20 10:57:46 +08:00
Glavo 96270b1706 Bump dependencies (#2659)
* Bump dependencies

* Fix build
2024-01-19 23:23:44 +08:00
Glavo af784dd0d0 Fix HMCLauncher.sh (#2658) 2024-01-19 12:51:59 +08:00
Glavo 1a1f2133f7 支持 FreeBSD x86-64 (#2656)
close #2652
2024-01-18 20:13:47 +08:00
Burning_TNT 3f1be961a6 Set the network timeout to 8000ms. (#2650) 2024-01-17 16:25:28 +08:00
Burning_TNT 8f2afdfcff Support #2643 (#2647) 2024-01-15 15:52:09 +08:00
Burning_TNT 532ba6ec2f Fix #1549 (#2648)
* Fix #1549

* Revert changes on ModTranslations.

* Revert changes on StringUtils.
2024-01-15 15:51:22 +08:00
Kevin Z 8678c0dcf2 auth: Always check token.exp when launch game (#2642)
* auth: Always check token.exp when launch game

fix #2048

* Remove wrong notAfter check
2024-01-13 11:53:47 +08:00
Burning_TNT 2116cfd789 Render title in white at anytime (#2389) 2024-01-13 11:27:10 +08:00
zkitefly 45d9033252 Update link (#2633)
* updata link

* Update dynamic-remote-resources json

* Revert "Update dynamic-remote-resources json"

This reverts commit 3d92ff2f56.
2024-01-13 10:36:44 +08:00
Glavo 96df35f728 将 HMCLauncher 源码转换为 UTF-8 编码 (#2641) 2024-01-13 03:26:58 +08:00
zkitefly 16b0335fcf Fixed "isOfficial" judgment (#2636) 2024-01-10 17:50:59 +08:00
Loading InformationandYuhui Huang b69dadc80c Update I18N_ru.properties (#1950)
Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2024-01-09 15:54:43 +08:00
Burning_TNT 7619e48145 Support #2588 记忆 HMCL 窗口位置 (#2593)
* Build configuration DFU system.

* Remove redundant object allocation.

* Fix

* Code cleanup

* Support #2588

* Remove unnecessary GSON usage.

* Fix compiletime unchecked warning.
2024-01-09 15:51:26 +08:00
Burning_TNT fbc667798d Fix #2470 Enhance the behavior of authlib-injector.json (#2471)
* Fix #2470

* Revert "Fix #2470"

This reverts commit b0f391021c.

* Refactor Behavior

* Simpify AuthlibInjectorServers.

* Fix merge

* Fix checkstyle.

* Recover the validation of AuthlibInjectorServers.
2024-01-08 20:41:48 +08:00
Burning_TNT 4e22585faf Build configuration DFU system. (#2589)
* Build configuration DFU system.

* Remove redundant object allocation.

* Fix

* Code cleanup

* Remove unnecessary GSON usage.

* Fix compiletime unchecked warning.
2024-01-08 20:39:55 +08:00
Burning_TNTandGlavo 5d3660ffb8 Enable HMCL to export jstack dump file 让 HMCL 能够导出游戏运行栈文件 (#2582)
* Enable HMCL to create game thread dump while game is running

* Fix checkstyle

* Hide accessToken

* Code cleanup

* Code cleanup

* Enhance I18N and declare the charset (UTF-8) of output file

* Inline variables

* Update the modifier of org.jackhuang.hmcl.game.GameDumpCreator#writeDumpHeadTo from public to private

* Refactor

* Add license for GameDumpCreator, remove support for Java 8

* Remove unnecessary Arrays.copyOf

* Fix checkstyle

* Use system charset to read the inputstream from JVM

* opt GameDumpCreator

* retry on failed attach to vm

* update GameDumpCreator

* Opt GameDumpCreator

* Fix

* Include BCIG

* Use BCIG to get PID.

* Fix.

* Fix again.

* Code cleanup. Fix bugs.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-08 20:35:46 +08:00
Burning_TNTandBurning_TNT 4149876e04 Support Neoforge (#2548)
* Support #2376

* Add necessary @Nullable annotations

* Display different types of dependencies in different sections.

* Fix checkstyle

* Add I18N for different types of dependencies.

* Enhance UI

* Code cleanup

* Enhance UI

* Manually sort the result from curseforge when searching mods by name.

* Render the search results from remote mod repositories in several pages.

* Fix merge

* Fix

* Add a button which navigates to the modpack download page in the modpack installl page

* Fix I18N

* Render the mod loaders supported by the version in mod info page.

* Fix #2104

* Enhance TwoLineListItem

* Render the mod loader supported by this mod file on the ModListPage

* Fix chinese searching and curseforge searching

* Update I18N

* Fix

* Fix

* Select the specific game version when clicking the 'download' button on ModListPage

* Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json

* Enhance :HMCL:build.gradle.kts

* Revert parse_mcmod_data.py

* Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage

FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError.

FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden.

* Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage.

* Fix checkstyle

* Fix

* Fix

* Fix

* Add license for RemoteResourceManager

* Remove TODO

* Enhance Chinese searching

* Support to decode metadata for local quilt mod.

* Enhance ModManager

* Fix checkstyle

* Refactor

* Fix

* Fix

* Refactor DownloadPage

* Fix

* Revert "Refactor DownloadPage"

This reverts commit 953558da77.

* Refactor DownloadPage

* Refactor

* Fix

* Fix checkstyle

* Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class.

* Fix

* Fix

* Fix

* Enhance SimpleMultimap

* Revert TwoLineListItem

* Fix

* Code cleanup

* Code cleanup

* Fix

* Code cleanup

* Add license for IModMetadataReader

* Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage

* Fix #2498

* Update README_cn.md

* Opti ModMananger

* Log a warning message when 'hmcl.update_source.override' is used.

* Fix chinese searching

* Enhance chinese searching.

* Enhance memory usage

* Add basic support for NeoForge.

* Enable HMCl to show the version list of NeoForged.

* Fix checkstyle

* Enable HMCL to download and install NeoForged from BMCL. Fix some bugs.

* Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty.

* Fix

* Enable HMCL to install NeoForge from modpacks.

* Fix the dirty implememtation

* Fix UI

* Cache builtin images.

* Enhance FXUtils (Make tooltip installer faster).

* Fix

* Fix

* Fix #2560

* Fix typo

* Fix remote image cache.

* Fix javadoc

* Fix checkstyle

* Optimize FXUtils::shutdown

* Support NeoForged 1.20.2

* Fix merge

* I have no idea on why the sha1 was matched.

* Revert "Enhance FXUtils (Make tooltip installer faster)."

This reverts commit 0a49eb2c12.

* Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland.

* Modify dynamic remote resource urls.

* Optimize codes with StringUtils.DynamicCommonSubsequence.

* Prevent unofficial HMCL to access HMCL Resource Update URL.

* Zip the dynamic-remote-resources json by Gradle automatically.

* Remove unnecessary getters.

* Fix

* Fix LibraryAnalyzer: Cannot analyze the version number of NeoForge correctly.

* Support to choose the NeoForge icon if a game is installed with NeoForge.

* Fix checkstyle

* Fix: The version number on DownloadPage of NeoForge is incorrect, which has leading string 'neoforge-'.

* Fix.

* Rename references. Fix: Invalid spaces in VersionIconType.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
2024-01-08 20:35:03 +08:00
zkitefly e6e4e36537 在每个检测句后加上引导句 (#2324)
* 在每个检测句后加上引导句

* 更新 GameCrashWindow.java

* 小小调整

* 更新 I18N_zh_CN.properties

* 按要求修改

* Update GameCrashWindow.java
2024-01-07 22:44:28 +08:00
Burning_TNTandburningtnt 5b7149b967 Suppport feature #2554. Fix the width of task progess bar. (#2573)
* Suppport feature #2554. Fix the width of task progess bar.

* Simply codes.

* Fix checkstyle.

---------

Co-authored-by: burningtnt <pangyl08@163.com“>
2024-01-07 22:41:08 +08:00
Burning_TNT f558be7193 Fix (#2622) 2024-01-07 22:40:21 +08:00
Burning_TNT a9159e1a72 Fix: The return value of the dependent mods may be duplicated from CurseForge API. (#2630) 2024-01-07 22:40:05 +08:00
Burning_TNT b0e3ae7684 Remove useless class ClassicLibrary (#2621) 2024-01-02 17:03:55 +08:00
Burning_TNTandBurning_TNT 242df8a81a Enhance mod download (#2411)
* Support #2376

* Add necessary @Nullable annotations

* Display different types of dependencies in different sections.

* Fix checkstyle

* Add I18N for different types of dependencies.

* Enhance UI

* Code cleanup

* Enhance UI

* Manually sort the result from curseforge when searching mods by name.

* Render the search results from remote mod repositories in several pages.

* Fix merge

* Fix

* Add a button which navigates to the modpack download page in the modpack installl page

* Fix I18N

* Render the mod loaders supported by the version in mod info page.

* Fix #2104

* Enhance TwoLineListItem

* Render the mod loader supported by this mod file on the ModListPage

* Fix chinese searching and curseforge searching

* Update I18N

* Fix

* Fix

* Select the specific game version when clicking the 'download' button on ModListPage

* Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json

* Enhance :HMCL:build.gradle.kts

* Revert parse_mcmod_data.py

* Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage

FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError.

FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden.

* Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage.

* Fix checkstyle

* Fix

* Fix

* Fix

* Add license for RemoteResourceManager

* Remove TODO

* Enhance Chinese searching

* Support to decode metadata for local quilt mod.

* Enhance ModManager

* Fix checkstyle

* Refactor

* Fix

* Fix

* Refactor DownloadPage

* Fix

* Revert "Refactor DownloadPage"

This reverts commit 953558da77.

* Refactor DownloadPage

* Refactor

* Fix

* Fix checkstyle

* Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class.

* Fix

* Fix

* Fix

* Enhance SimpleMultimap

* Revert TwoLineListItem

* Fix

* Code cleanup

* Code cleanup

* Fix

* Code cleanup

* Add license for IModMetadataReader

* Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage

* Fix #2498

* Update README_cn.md

* Opti ModMananger

* Log a warning message when 'hmcl.update_source.override' is used.

* Fix chinese searching

* Enhance chinese searching.

* Enhance memory usage

* Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty.

* Cache builtin images.

* Enhance FXUtils (Make tooltip installer faster).

* Fix

* Fix

* Fix #2560

* Fix typo

* Fix remote image cache.

* Fix javadoc

* Fix checkstyle

* Optimize FXUtils::shutdown

* Fix merge

* I have no idea on why the sha1 was matched.

* Revert "Enhance FXUtils (Make tooltip installer faster)."

This reverts commit 0a49eb2c12.

* Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland.

* Modify dynamic remote resource urls.

* Optimize codes with StringUtils.DynamicCommonSubsequence.

* Prevent unofficial HMCL to access HMCL Resource Update URL.

* Zip the dynamic-remote-resources json by Gradle automatically.

* Remove unnecessary getters.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
2023-12-31 23:15:54 +08:00
Burning_TNT 333fd0ef80 Fix #2375 Fix network NPE (#2579)
* Fix #2375

* Fix

* Fix

* Simply codes.
2023-12-31 22:51:06 +08:00
5defff2bb0 Fix string tokenizer (#2538)
* Fix

* Parse quote and double quote at the same time. Add TokenizerTest.

* Simplify TokenizerTest

* Fix handling multiple space

* Fix handling empty part

* Supports escape sequences

* Remove an unnecessary lambda.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
Co-authored-by: Glavo <zjx001202@gmail.com>
2023-12-31 22:45:26 +08:00
zkitefly bdcbe6c948 修复 #2524 带来的一些小问题 (#2535)
* #2524 不小心带来的翻译错误

* `Tencent` -> `Tencent Cloud`
2023-12-31 22:37:28 +08:00
Burning_TNT cc192c840c [Enhance] Enhance issue templates 改进 Issue 模板 (#2381)
* Enhance issue templates

* Update feature.yml

* Update

* Update issue templates

* Add the missing chinese translation

* Fix typo
2023-12-31 21:57:53 +08:00
Burning_TNT e5ece58d17 Minimize OpenNBT. (#2601) 2023-12-31 21:55:24 +08:00
Burning_TNT 2391b26bd6 Fix #1750 (#2580) 2023-12-31 21:54:19 +08:00
zkitefly c7fad05bae Update exitCode (#2500) 2023-12-31 21:49:56 +08:00
Burning_TNT 1d9d31f906 Exclude META-INF/maven/** (#2613) 2023-12-27 00:03:03 +08:00
Glavo d584c101d4 Filter package-info.class (#2606) 2023-12-20 20:10:06 +08:00
Burning_TNT ab41feff42 Fix DefaultLauncher (#2584) 2023-12-15 20:38:23 +08:00
Burning_TNT cb7975515a Update Minecraft version filter to 1.20.4 when downloding mods. (#2595) 2023-12-14 18:58:55 +08:00
zkitefly 91f684ae2e 不默认设置 forceUnicodeFont (#2501) 2023-12-09 01:02:08 +08:00
Glavo 55c55e9bda Bump Gradle to 8.5 (#2586) 2023-12-01 04:04:41 +08:00
Burning_TNTandBurning_TNT 453ba6eb75 Fix build.gradle and README (#2565)
* Update build.gradle.kts

* Fix table format and typo in README(_cn).md

* Replace Github to GitHub

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
2023-11-12 20:30:36 +08:00
zkitefly 5aeaba4636 add PLATFORM_cn (#2536)
* add PLATFORM_cn

* Update PLATFORM_cn.md

* 润色

* Update PLATFORM_cn.md

* Update PLATFORM.md

* 格式化
2023-11-09 21:24:02 +08:00
Burning_TNT df9499b723 Catch Internal Error (#2520)
* Catch Internal Error

* Fix

* Fix I18N
2023-11-09 21:23:10 +08:00
Burning_TNT f84be1e109 Fix #2539 (#2541) 2023-11-09 21:20:39 +08:00
Burning_TNT 31872f4f77 Fix #2547 (#2549) 2023-11-09 21:19:26 +08:00
Burning_TNTandGlavo 54188bf8f2 Enhance booting (#2531)
* Enhance booting

* Delete @Booting

* Rollback OperatingSystem

* Delete Booting

* cleanup

* Update FractureiserDetector

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2023-09-30 14:27:56 +08:00
zkitefly 9fb2bc4c7e 将所有的阿里镜像源地址换成腾讯云镜像源 (#2524) 2023-09-29 19:45:45 +08:00
Glavo 5d8a56cf66 更新对 ARM 平台的支持 (#2522)
* 修复对 Windows Arm64 的支持

* 修复对 Linux Arm64 的支持
2023-09-22 16:48:24 +08:00
ZekerZhayard 833ac2e32e Fix missing null checking. (#2512) 2023-09-12 23:15:44 +08:00
Glavo 50173a3987 添加龙芯新世界 LWJGL2 支持 (#2506)
* 添加龙芯新世界 LWJGL2 支持

* Update Documents
2023-09-06 15:28:55 +08:00
Glavo 9d5202c19f 优化龙芯支持 (#2504)
* 添加龙芯平台提示

* 添加旧世界 JNA

* Update Launcher

* Update HMCLauncher.sh

* Update I18N
2023-09-05 14:47:46 +08:00
何杰豪 e1bbd8b326 System GLFW & System OpenAL are available on Windows too (#2496) 2023-08-29 12:36:55 +08:00
Glavo 06f81b9b09 启动时验证 JavaFX 完整性 (#2489)
* 启动时验证 JavaFX 完整性

* fix

* fix
2023-08-26 12:59:04 +08:00
Glavo 150baa0e30 重构 SVG (#2487)
* Refactoring SVG

* enum

* update IconedMenuItem

* fix checkstyle

* update Theme

* fix checkstyle

* update
2023-08-26 11:25:05 +08:00
Glavo 413d962db0 优化日志性能 (#2488)
* 优化日志性能

* update

* update

* update
2023-08-26 11:13:15 +08:00
Glavo 74118049ce 添加龙芯新世界支持 (#2480)
* 添加龙芯新世界支持

* Update message
2023-08-23 21:29:14 +08:00
zkitefly 05086fa76d 修复日志导出 (#2400)
* 优化日志导出

以防万一的操作,防止一些必要的 log 没被打包

* Update LogExporter.java

* 更新 LogExporter.java

* add

* Revert "add"

This reverts commit 287f9300d7.

* add

* 奇怪我什么时候改过这里了?
2023-08-19 08:00:42 +08:00
Glavo c1ffd26c80 Optimization for Hex (#2476)
* Optimization for Hex

* fix checkstyle
2023-08-19 07:36:52 +08:00
yangyangdaji a8b0f81b68 Update Hub Mirror Action to 1.3 (#2458)
Update `Hub Mirror Action` to 1.3

[Release note](https://github.com/Yikun/hub-mirror-action/releases/tag/v1.3)

> 升级PyYaml以修复PyYaml安装问题 https://github.com/Yikun/hub-mirror-action/pull/182
> 开启pull_request_target帮助开发者触发CI验证 https://github.com/Yikun/hub-mirror-action/pull/139
> 新增clone_style的说明文档 https://github.com/Yikun/hub-mirror-action/pull/146 感谢@AdoShan
> 新增与repo-list-generator共同使用的文档 https://github.com/Yikun/hub-mirror-action/pull/150 感谢@yi-Xu-0100

Fix sync to gitee action

`ModuleNotFoundError: No module named 'yaml'`

[show detail](https://github.com/huanghongxun/HMCL/actions/runs/5783970016/job/15673761600)

Signed-off-by: yangyangdaji <1504305527@qq.com>
2023-08-19 06:16:19 +08:00
Glavo d83b508ce4 Bump Gradle to 8.3 (#2475) 2023-08-19 06:13:39 +08:00
Burning_TNT fe622579cf Support #2416 Enable HMCL to analyze crash reasons from latest.log (#2433)
* Support analyze crash reasons from latest.log

* Fix
2023-08-11 15:14:20 +08:00
zkitefly edcdeaf018 修改语言文件 (#2412)
* 更新 I18N_zh_CN.properties

* 因为 MMC 不再支持 Curse 整合包,所以 MCBBS 整合包格式将无法被识别

* https://github.com/huanghongxun/HMCL/pull/1410

* 更改一些提示
2023-08-07 18:26:19 +08:00
zkitefly 9d4eb9662e Update CrashReportAnalyzer.java (#2439)
* Update CrashReportAnalyzer.java

Fix

* add

* add

https://mclo.gs/Fp8GLWo
2023-08-07 18:21:28 +08:00
Burning_TNT 42e88fe6f5 Fix #2444 (#2445) 2023-08-02 23:17:08 +08:00
Burning_TNT 61165dd71d [Fix 2378] Invalid Modrinth Mod ID 错误的 Modrinth Mod ID (#2385)
* Upgrade timeout to 30s

* Fix #2378

* Revert "Upgrade timeout to 30s"

This reverts commit 366a9dd34b.
2023-07-20 14:30:22 +08:00
zkitefly 83120afb63 add (#2401) 2023-07-18 20:58:38 +08:00
Burning_TNTandGlavo 4fca632efb Fix #2397 Fractureiser 检测没有判断是否为文件 (#2398)
* Fix #2397

* Update FractureiserDetector.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2023-07-16 03:07:33 +08:00
zkitefly 03b20a70bb Update Metadata.java (#2390) 2023-07-14 12:50:55 +08:00
Burning_TNT 20ee9c8ce6 [Fix ----] Game option UseNativeGLFW could work on Windows. Now, it is ignored on Windows. (#2386)
* Fix: Game option UseNativeGLFW could work on Windows. Now, it is ignored on Windows.

* Update translation value
2023-07-12 15:02:52 +08:00
zkitefly 9c0e1b1dc8 check-update.yml 支持 stable 发版 (#2332)
* Update check-update.yml

* test

* test

* test

* Update check-update.yml

* test

* Update check-update.yml

* test

* test

* test

* test

* test

* test

无语了(

* test

* test

* test

* fix

* OK!

* Update check-update.yml

* 按照要求修改

* 按照要求修改

* test

* finish
2023-07-10 20:39:17 +08:00
GlavoandYuhui Huang f29e350b1a #2353: 优化游戏进程被 SIGKILL 信号终止时的提示 (#2358)
Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2023-07-10 15:06:28 +08:00
Burning_TNT bbe0f5d750 Support selecting the best icon when a new version is installed. (#2370) 2023-07-10 15:03:04 +08:00
Burning_TNT 371295d9fe Fix #2372 (#2373) 2023-07-10 15:01:09 +08:00
Burning_TNT cc8a4ddf19 Fix #2371 (#2374) 2023-07-10 15:00:24 +08:00
zkitefly f3ff037f1c 添加日志分析规则 (#2361)
* add

* fix
2023-07-08 23:01:55 +08:00
Burning_TNT 3b36bbc88e Support viewing mods in ModListPage (#2366) 2023-07-08 23:01:27 +08:00
Burning_TNT 80b53119ed Support copying the UUID of the account (#2364) 2023-07-08 23:00:19 +08:00
zkitefly 4d88c2b1bb 在一些地方加些提示 (#2329)
* tips

* add

* 更新 DownloadListPage.java

* fix

* fix
2023-07-07 15:16:50 +08:00
Burning_TNT 29582f6c50 Warn if username is invalid when creating a offline account. 在离线账户创建时对不合法的账户名做出警告 (#2309)
* Fix #2258

* Fix checkstyle

* Merge official branch into current branch and fix checkstyle
2023-07-07 15:15:50 +08:00
zkitefly 623c54cf58 调整日志分析规则 (#2217)
* Update bug-report.yml

* 更新 CrashReportAnalyzer.java

* 添加 帮助 按钮

* 调整日志分析规则

* add

https://mclo.gs/EheKneq#L324

* add

https://mclo.gs/VyH2mV1#L135

* add

https://mclo.gs/rVgURRg

* add

https://mclo.gs/qCDf08i
https://mclo.gs/pyYhqhx

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModListPage.java

* add

* add and fix

* Edit MCEdit Link

* add

https://mclo.gs/Iq1Y5J0#L30
https://mclo.gs/D5MVHDa#L30
https://mclo.gs/nWMnElV#L34
https://mclo.gs/eoO8xjH#L14
https://mclo.gs/Jw1dRMZ#L27
https://mclo.gs/BVagUsw#L14
https://mclo.gs/NCfhHYC#L14
https://mclo.gs/PYRFCj3#L21
https://mclo.gs/4dCbKcJ#L19

* add

https://mclo.gs/h63mMy7#L47

* add

https://mclo.gs/iTTW7Ph#L16

* fix?

* fix?

* fix

* fix

无语,眼瞎(

* add

https://github.com/Hex-Dragon/PCL2/issues/1277

* fix

* fix

* 更新 I18N_zh_CN.properties

* add JADE_FOREST_OPTIFINE

* fix
2023-07-07 15:14:37 +08:00
zkitefly 6eee8b89e1 导出更多日志 (#2262)
* 更新 LogExporter.java

* add

* 更新 LogExporter.java

* 按照要求修改

~嘻嘻,这个是GPT生成的(~
2023-07-07 15:14:02 +08:00
Burning_TNT c74ef3eca8 Fix 2338 自定义命令问题 (#2339)
* Fix 2338

* Fix: Now HMCL could read Post exit command from config file.

* Invoke processListener.onExit before invoking exit command. Remove 'throw new RuntimeException'.
2023-07-07 15:13:12 +08:00
zkitefly 372b6376fc 在一些地方插入些log (#2325) 2023-07-07 15:10:29 +08:00
Burning_TNT 0c3bfc933c Support #1775 Enable HMCL to download resource packs from Modrinth. (#2355)
* Support downloading resource packs from Modrinth. Fix #1775

* Fix: Forget to register ResourcepackDownloadListPage to a button.
2023-07-07 15:08:59 +08:00
Burning_TNT 2414db1c61 Support #2010 一键导出待更新模组列表 (#2354)
* Fix #2010

* Improve behaviour

* Add License
2023-07-07 14:58:14 +08:00
Burning_TNT 008fe17035 Support2128 log window copy able (#2359)
* Support copy action in LogWindow

* Use a pseudo class state to manage render whether the line is selected
2023-07-07 14:57:16 +08:00
何杰豪andYuhui Huang 2be95fdb51 add logs folder open and change the shaderpacks icon (#2347)
* shaderpacks folder and resourcepacks icon

* i18n

---------

Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2023-07-06 23:46:35 +08:00
Yuhui Huang e5842d5a49 Revert "Support copy action in LogWindow (#2351)" (#2357)
This reverts commit 9a59ca95a1.
2023-07-06 23:34:10 +08:00
Burning_TNT 9a59ca95a1 Support copy action in LogWindow (#2351) 2023-07-06 23:31:55 +08:00
Burning_TNT 421f6f8b6d Fix #2303 (#2346) 2023-07-06 23:29:45 +08:00
Burning_TNT a5ee6b05e8 Fix #2305 修复游戏语言自动切换功能与不同版本的兼容性问题 (#2345)
* Fix #2305

* fix
2023-07-06 23:28:41 +08:00
zkitefly c9465d89f5 Fix #2335 (#2336)
* Fix #2335

https://github.com/zkitefly/PCL2/blob/log/Plain%20Craft%20Launcher%202/Modules/Minecraft/ModMinecraft.vb#L2043-L2059

* Update Constants.java
2023-07-06 23:23:10 +08:00
zkitefly 097a8eaf26 Fix #1939 (#2352) 2023-07-06 23:19:54 +08:00
何杰豪 648b671210 i18n (#2344)
* 改错

* 改错

* 改错
2023-07-06 23:12:39 +08:00
zkitefly 75aeed9747 add shaderpacks open (#2331)
* add shaderpacks

* fix

* fix
2023-07-03 00:02:25 +08:00
zkitefly c87f7f39c7 修复搜索栏处的版本顺序错误 (#2318)
我当时写反了(
2023-06-29 01:35:50 +08:00
Burning_TNT 32b10038d9 Fix checkstyle (#2315) 2023-06-26 21:58:27 +08:00
TomatoPuddin 204cbcbed0 添加 Curse RemoteMod.Version 对多个 ModLoaderType 的支持 (#2311) 2023-06-26 17:43:11 +08:00
TomatoPuddin da5a04b3ba 改进 ModItem 中渲染 ModLoaderType 的代码 (#2310)
* 移除 ModItem 中多余的设置 VersionType 图标的代码

* 添加 ModItem 中缺失的 Quilt 标签
2023-06-26 17:43:02 +08:00
zkitefly 126a6dbbed Update HMCLauncher (#2264)
* 更新 lang.h

* 更新 main.cpp

* 更新 HMCLauncher.sh
2023-06-24 09:40:13 +08:00
zkitefly ab43d4a6cf 将配置文件的 logLines 键默认值改为 1000 (#2261) 2023-06-23 19:49:36 +08:00
zkiteflyandYuhui Huang 5f6e11f709 添加 帮助 按钮 (#2214)
* 添加 帮助 按钮

* 调整

* Revert "调整"

This reverts commit 69c7552933.

* 更新 DecoratorSkin.java

* Update DecoratorSkin.java

* Update DecoratorSkin.java

* Revert "Update DecoratorSkin.java"

This reverts commit cdb6267e23.

* Revert "Merge branch 'helpButton' of https://github.com/zkitefly/HMCL into helpButton"

This reverts commit 03673540ee, reversing
changes made to 5342625678.

* add

* Revert "add"

This reverts commit 8381f5f57f.

* add

* Revert "add"

This reverts commit ee14d9ac82.

---------

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2023-06-23 19:45:12 +08:00
Burning_TNT cf61abcf03 Render supported mod loader in org.jackhuang.hmcl.ui.versions.DownloadPage.ModItem. Support #2284 (#2296) 2023-06-23 19:43:10 +08:00
zkitefly 9549819571 检测到多个原因时提示 (#2293) 2023-06-23 18:21:28 +08:00
Nathan Du 95b85c13eb Reorder priority of mod info elements in list entry (#2159)
Gives emphasis to the name and version as reported by the mod, instead
of emphasizing the filename.
2023-06-23 18:19:44 +08:00
Burning_TNT d46efe8410 Enable HMCL to update mod from Modrinth (#2236)
* Enable HMCL to update mod from Modrinth

* Fix UI

* Fix: HMCL won't select the latest version when there is newer version in several remote mod repository.
2023-06-23 18:15:16 +08:00
zkitefly 501ecac4d5 添加一些 tooltip (#2215)
* 添加几个 FXUtils.installFastTooltip

* add

* add

* add

* 更新 VersionPage.java

* add

* add
2023-06-23 18:12:59 +08:00
TomatoPuddin 0dce5bc9a6 添加分类modrinth.category.folia (#2300) 2023-06-23 18:11:17 +08:00
Burning_TNT 9092c8b7a6 Fix #2268 (#2295) 2023-06-23 18:10:48 +08:00
Burning_TNT a5858495ef Fix #2281 (#2294)
* Fix #2281

* Use switch instead of Enum.name().toLowerCase(Locale.ENGLISH)
2023-06-23 18:09:20 +08:00
Burning_TNT 88984378e8 Add support to ModLoaderType.QUILT (#2302) 2023-06-23 18:07:31 +08:00
huanghongxun 470c3d6c24 fix(HMCLCore): do not init JavaFX in JTest. 2023-06-16 08:48:59 +08:00
zkitefly 6133e7e730 Update PURCHASE_URL and MIGRATION_FAQ_URL (#2229) 2023-06-16 08:39:16 +08:00
zkitefly c5cbafea60 添加 remove_assets 按钮 (#2257) 2023-06-16 08:38:27 +08:00
zkitefly 167e25e859 添加更多日志分析规则 (#2192)
* 添加“Forge 安装不完整”的检查

* fix

* fix

* add modName

* 更新 CrashReportAnalyzer.java

* 更新 CrashReportAnalyzerTest.java
2023-06-16 08:36:03 +08:00
zkitefly f47bd8a407 更新 CrashReportAnalyzer.java (#2175) 2023-06-16 08:31:44 +08:00
zkitefly 464be411d1 Updata account.failed.server_disconnected (#2247) 2023-06-16 08:23:11 +08:00
zkitefly d3b6eb36d9 Update DefaultGameRepository.java (#2273) 2023-06-16 08:21:27 +08:00
Glavo 71458f5167 启动时检测 Fractureiser 病毒 (#2270) 2023-06-16 08:21:06 +08:00
zkitefly 597730389f Close #1367 (#2232) 2023-06-16 08:18:41 +08:00
zkitefly 56b227f467 添加新显示项目:物理内存大小 (#2279) 2023-06-16 08:17:30 +08:00
Glavo 213b03ea56 Print warning message when signature key is missing (#2283) 2023-06-16 08:13:05 +08:00
Glavo 959c4d0698 检测 macOS 上的 App Translocation (#2252)
* check app translocation on macOS

* update

* update
2023-06-16 08:12:41 +08:00
zkitefly 57b3bd9ff0 修复分类翻译键问题 (#2277)
* 修复分类翻译键问题

* 更新搜索版本号
2023-06-16 08:11:09 +08:00
zkitefly 68f4038879 更新 Issue 模板 (#2278)
* Create feature.yml

* Update config.yml

* Update config.yml

* Update feature.yml
2023-06-16 08:10:44 +08:00
Glavo 6010008409 使用 ObservableMap 记录已显示的提示 (#2271) 2023-06-13 10:21:03 +08:00
Haowei Wen 4b2e857f08 Fix chat is broken on Minecraft 1.20 2023-06-12 00:42:41 +08:00
Glavo 0280a34d00 fix #2267: filter duplicate jars in the class path (#2280) 2023-06-11 15:47:43 +08:00
Glavo 811707b611 代码清理以及微优化 (#2244) 2023-05-26 00:52:46 +08:00
Glavo 9152550bda Suppress compilation warnings (#2243)
* Suppress compilation warnings

* update
2023-05-24 22:05:30 +08:00
Glavo 6cb4d24600 Log XDG_SESSION_TYPE (#2242) 2023-05-24 21:51:22 +08:00
Glavo 49287453eb 优化 VersionNumber (#2240)
* 优化 VersionNumber

* Add more tests

* update

* update tests
2023-05-24 21:28:22 +08:00
Glavo e7c108d843 修复使用系统 GLFW 选项对 1.19+ 不生效的问题 (#2241)
* 修复使用系统 GLFW 选项对 1.19+ 不生效的问题

* Use UTF-8 in NativePatcher

* update
2023-05-24 21:15:09 +08:00
Glavo 6c9bf6cb09 修复翻译 (#2239) 2023-05-23 15:07:53 +08:00
Glavo efc27c8e21 添加 Linux RISC-V 64 平台支持 (#2238) 2023-05-23 05:17:26 +08:00
Glavo 7c5ccc906e 优化 SSL 异常报错信息 (#2234)
* 改进 SSL 异常报错

* update
2023-05-22 16:40:34 +08:00
zkitefly 69a6ff6053 Fix 2226 (#2227) 2023-05-20 02:18:25 +08:00
zkitefly 605d0c6a92 Fix #2204 (#2228)
https://github.com/huanghongxun/HMCL/issues/2204#issuecomment-1519823562
2023-05-20 02:17:18 +08:00
Glavo 9d54ddf456 修复无法使用 macOS aarch64 JRE 启动部分版本的问题 (#2222) 2023-05-10 20:08:53 +08:00
Glavo c6afd53e73 Bump dependencies (#2191) 2023-04-06 02:41:57 +08:00
Glavo fd9bb88033 添加隐藏提示按钮 (#2018)
* 添加隐藏提示按钮

* update

* update

* update

* update
2023-04-06 02:33:22 +08:00
Glavo 316607b333 添加对 Linux RISC-V 64 平台的基本支持 (#2187) 2023-04-01 06:29:51 +08:00
Glavo af36a0ac2a Bump Gradle to 8.0.1 (#2158) 2023-03-01 23:32:24 +08:00
Glavo 7356335106 添加环境变量设置 (#2157)
* Remove unused methods

* 添加环境变量设置

* update

* update
2023-02-26 23:46:00 +08:00
Glavo d8c96769ea Bump dependencies (#2149) 2023-02-24 04:31:45 +08:00
Glavo d71815aa3d 清理编译警告 (#2148) 2023-02-24 04:14:23 +08:00
Glavo 9a4286620f 简化 JFXHyperlink 用法 (#2147)
* 简化 JFXHyperlink 用法

* fix checkstyle
2023-02-24 03:49:13 +08:00
Glavo 0eac7b5d12 Use MESA_LOADER_DRIVER_OVERRIDE on Linux (#2146) 2023-02-24 01:56:01 +08:00
Glavo af63c3aa3a 添加渲染器设置 (#2145)
close #2143
2023-02-24 01:32:15 +08:00
Glavo 72a8199453 将版本高级设置拆分至单独页面中 (#2142)
* update

* update

* update

* update
2023-02-22 20:37:27 +08:00
Glavo 090b83bf9c fix #2129: Fix computePerfHeight of ComponentList (#2139)
* update

* update style

* update

* update

* update

* update
2023-02-19 19:22:01 +08:00
Glavo 6f70da51f9 Use computeIfAbsent in EventBus (#2138) 2023-02-19 13:25:20 +08:00
Glavo ed0a4c8cf4 fix #2131: Hide feedbackTextFlow from gameDirPane if possible (#2136)
* Add scroll bar to GameCrashWindow

* Hide feedbackTextFlow when it is not visible

* fix checkstyle

* update I18N

* update I18N
2023-02-19 12:36:35 +08:00
Glavo 5fa066de6b fix crash (#2124) 2023-02-15 23:27:59 +08:00
zkitefly 1454ea8358 添加提示 (#2108)
* 添加提示

* Update I18N_zh_CN.properties
2023-02-15 17:45:25 +08:00
Glavo ab264955f5 添加动画开关 (#2111) 2023-02-14 19:12:30 +08:00
Glavo 389d430dbd fix #2094 (#2096) 2023-02-14 19:08:26 +08:00
Glavo 8b98932ae8 fix crash (#2115) 2023-02-13 19:59:24 +08:00
Glavo 62f39a3280 更新 I18N (#2083)
* Remove multiplayer messages

* update

* update

* update

* update

* update

* update

* update

* update
2023-02-12 09:33:52 +08:00
Glavo b116186f42 fix export batch script (#2107) 2023-02-11 23:43:13 +08:00
Glavo 40eeee7137 fix #2105: catch NPE in Accounts (#2106) 2023-02-11 22:53:58 +08:00
zkitefly 54eab87356 初始化内容修改为“热度” (#2099)
* 将初始化内容设定为“名称”排序

* 更新 RemoteModRepository.java

* 初始化内容修改为“下载量”

搜索时排序依然是“名称”

* 还是默认“热度”吧

因为一般搜模组都是搜热度比较高的,搜出的结果就较为准确

* Update DownloadListPage.java
2023-02-11 13:51:32 +08:00
zkitefly d828f7bc7b 初始化排序修改为“下载量” (#2097)
* 将初始化内容设定为“名称”排序

* 更新 RemoteModRepository.java

* 初始化内容修改为“下载量”

搜索时排序依然是“名称”
2023-02-11 11:47:26 +08:00
Glavo ae3e135e06 FileSelector needs to update the value after selecting a file (#2095)
* FileSelector needs to update the value after selecting a file

* update
2023-02-11 00:14:51 +08:00
Glavo 5ba8bdad10 Catch i18n exception (#2093)
* Catch i18n exception

* update

* update
2023-02-10 11:01:34 +08:00
Glavo bffbfd7af9 Fix the problem of adding redundant G1 parameters (#2092) 2023-02-09 22:18:01 +08:00
Glavo a8c1e8d2cb Do not save the default number of log lines to the configuration (#2086) 2023-02-09 00:34:17 +08:00
Glavo 230d6b20ab 修复下载源选择 ComboBox 过长的问题 (#2085) 2023-02-09 00:33:47 +08:00
Glavo 109a14dd74 Remove unused files 2023-02-08 23:17:21 +08:00
Glavo 1ac1ff3c95 fix crash 2023-02-08 21:16:24 +08:00
Glavo 7e522ff136 弃用 commons-lang (#2061)
* Remove ArrayUtils

* Remove MutableObject

* VersionRange

* Replace Range with VersionRange

* fix checkstyle

* Update

* update

* update

* fix checkstyle

* fix checkstyle

* update
2023-02-08 19:57:52 +08:00
zkiteflyandYuhui Huang 90c261ddd1 Quilt 相关功能完善 (#2035)
* Quilt 相关功能完善

- Mcbbs 整合包适配 Quilt
- 自定义命令支持 Quilt 环境变量
- 顺便把语言文件内的机翻错误“\:”修复

* Fix

---------

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2023-02-08 19:54:54 +08:00
Sharll b9bdc29dc2 更改版权声明 (#2078)
* Update AboutPage.java

* Update build.gradle.kts
2023-02-08 19:51:47 +08:00
Glavo 13968fe3aa close #1917 2023-02-08 13:09:42 +08:00
Glavo 825df1981d Use FXUtils.bindString in FileSelector 2023-02-08 13:09:42 +08:00
Glavo dd56f4f8a6 fix #2002: Fix smooth scrolling for GTK3 2023-02-08 13:07:44 +08:00
Glavo b9ef10a73d update 2023-02-07 08:07:20 +08:00
Glavo 0f68a7c703 fix checkstyle 2023-02-07 08:07:20 +08:00
Glavo f5b24b62f1 update 2023-02-07 08:07:20 +08:00
Glavo f9eca35422 update 2023-02-07 08:07:20 +08:00
Glavo 394a74e566 search bar 2023-02-07 08:07:20 +08:00
Glavo 870b73c101 Revert "close #2043: Make ModListPage searchable (#2044)"
This reverts commit 9a3b8545ca.
2023-02-07 08:07:20 +08:00
zkitefly d89a32f620 https://hmcl.huangyuhui.net/eula to https://docs.hmcl.net/eula/hmcl.html (#1967) 2023-02-06 22:58:44 +08:00
zkitefly 29410b951d 优化日志分析器 (#2038)
* 更新 CrashReportAnalyzer.java

* Edit

* Fix

* add

* 删除 CrashReportAnalyzerTest.java

试试删掉的话能不能构建成功

* Fix

* Fix

* Revert "删除 CrashReportAnalyzerTest.java"

This reverts commit 4fe873cfcb.

* 换回 LibericaJDK

* Fix

尴尬(

* 更新 GameCrashWindow.java

修改大小以显示更多内容(防止左下角按钮被吞掉)

* 更新 I18N_zh_CN.properties

- 修改链接,可以跳转至特定地方
- 统一叫法 "全局(特定)游戏设置"
- 将升级显卡的相关教程文章更换

* Fix

* Update CrashReportAnalyzer.java

* Fix

- 删除重复检查项

* 添加日志检查

https://paste.ee/p/vv7DD

* fix

* add

怎么tm还来一个

* 更新 CrashReportAnalyzerTest.java

* 调整

* fix

* 更新 I18N_zh_CN.properties

* 更新 I18N_zh_CN.properties

* 添加重复安装 Optifine检查

https://github.com/huanghongxun/HMCL/discussions/1904#discussioncomment-4340734

* 添加重复安装 Forge 的检查

test

更新 CrashReportAnalyzer.java

fix

* 完善 OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE

完善 OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE

https://github.com/huanghongxun/HMCL/discussions/1904#discussioncomment-4332485
https://github.com/huanghongxun/HMCL/discussions/1904#discussioncomment-4332522

* fix

* 调整 FABRIC_WARNINGS

https://paste.ee/p/KOTyX
2023-02-06 22:56:58 +08:00
Glavo 461cde3282 改进 LogWindow (#1971)
* Add CircularArrayList

* fix test

* update

* update
2023-02-06 22:55:16 +08:00
Glavo 94b54f3dce Exclude JavaFX (#2073) 2023-02-06 22:54:41 +08:00
zkitefly 6e20ce4db3 插入更多”请求帮助”链接 (#2051)
* 更新 I18N_zh_CN.properties

* 更新

* 更新 I18N_zh_CN.properties
2023-02-06 22:50:09 +08:00
Glavo e76d520cac x86-64 平台上不自动选择 32 位 Java (#2042)
* close #2041

* update

* update
2023-02-06 22:49:37 +08:00
CyenochandCyenoch 9a3b8545ca close #2043: Make ModListPage searchable (#2044)
* close #2043: Make ModListPage searchable

* Optimize layout

---------

Co-authored-by: Cyenoch <cyenoch@qq.com>
2023-02-06 22:49:22 +08:00
Glavo bb66be7c3a 向非官方构建添加提示 (#2069)
* update

* Add GitHub SHA to manifest

* Add unofficial to build channel

* Optimize IntegrityChecker

* update hint

* Add hint
2023-02-06 22:47:23 +08:00
zkitefly 8514eb904d 将日志窗口的最大输出值调整至 10000 (#2065) 2023-02-06 22:45:30 +08:00
Glavo a6c8867ec4 Use Locale.forLanguageTag (#2064) 2023-02-06 22:45:03 +08:00
Glavo 5753c2993c Allow users to override system properties (#2062)
* Allow users to override system properties

* update
2023-02-06 22:44:48 +08:00
Glavo ec6637728a 代码清理 (#2072)
* Remove unused methods

* newRaisedButton

* update

* update

* update

* update

* update

* toLowerCase and toUpperCase

* update

* update
2023-02-06 22:44:19 +08:00
Glavo 3198daa60a fix #2070 2023-02-06 11:01:33 +08:00
Glavo 1a6dffab74 fix #2008: Replace JFXTreeTableView with TableView
close #2068
2023-02-05 17:12:23 +08:00
zkitefly d54d9ae5cb 修改 登录失败 弹窗 (#2057) 2023-02-02 21:36:08 +08:00
Glavo e543ee3f55 建议 Forge 用户使用官方推荐版本的 Java 启动游戏 (#2033)
* Share LibraryAnalyzer

* Add a new JavaVersionConstraint rule

* Update message
2023-02-02 21:35:22 +08:00
zkitefly f7526d4cdc 将初始化内容设定为“名称”排序 (#2054)
* 将初始化内容设定为“名称”排序

* 更新 RemoteModRepository.java
2023-02-02 21:33:43 +08:00
Glavo cf136fe7df 导出更多日志 (#1970)
* close #1883: export vm crash log

* fix #2047: export fml-client-latest.log
2023-02-02 21:30:15 +08:00
Glavo 5d9d9b9cc4 fix #2020: write stylesheet with default charset (#2024)
* fix #2020: write stylesheet with default charset

* Check JavaFX Version

* javadoc

* close #1051
close #1957
2023-02-02 21:29:24 +08:00
Glavo 6a05df2c35 Prompt when there is no mods to update (#2026) 2023-02-02 21:28:32 +08:00
Gamom 34cfdb8073 添加翻译键:modrinth.category.datapack (#2053)
* 添加翻译键:modrinth.category.datapack

* More translation
2023-02-02 21:28:15 +08:00
Glavo 259ed570ab Remove drawAvatarJ2D (#2027) 2023-02-02 21:26:58 +08:00
Glavo c86302df58 Update NormalizedSkin (#2036)
* Update NormalizedSkin

* Add tests

* update

* update
2023-02-02 21:26:27 +08:00
Glavo 3d9c9689e3 fix #2023: Set Tooltip text fill (#2028)
* fix #2023: Set tooltip text fill

* close #1077
2023-02-02 21:25:27 +08:00
Glavo d4398764a1 Update Texture (#2037)
* Update Texture

* fix checkstyle
2023-02-02 21:25:05 +08:00
zkitefly fb573d8205 Update bug-report.yml (#2050) 2023-02-02 21:19:30 +08:00
CyenochandCyenoch da5097ab78 Related to #1961: Add a Dialog to warn. (#2055)
Co-authored-by: Cyenoch <cyenoch@qq.com>
2023-02-02 21:19:07 +08:00
Glavo 59b5aabfe5 Bump OpenJFX from 17.0.2 to 19.0.2.1 (#2029)
fix #1896
fix #1923
2023-02-02 21:15:53 +08:00
Glavo 8355f02de0 fix memory leak (#2034) 2023-02-02 21:15:34 +08:00
zkitefly bd4b334185 清除所有 QQ频道 相关内容 (#2016)
* 删除日志分析器的QQ频道链接

* 清除所有QQ频道相关内容

应该都清理完了吧
2023-01-16 22:48:39 +08:00
Glavo bbe0a3f567 #1998: Log prism pipeline name (#2007)
* #1998: Log prism pipeline name

* update

* update
2023-01-14 22:36:27 +08:00
Glavo 41620dcd70 fix #2006 (#2009) 2023-01-14 22:35:24 +08:00
Glavo e865b31b62 fix I18N_es (#2011) 2023-01-14 22:35:04 +08:00
Glavo 92ce7c83ca 清理代码 (#1990)
* cleanup

* Cleanup StringUtils

* Cleanup CommandBuilder

* Cleanup CommandBuilder

* Use 'String::replace(char, char)'
2023-01-14 02:37:55 +08:00
Glavo 753ba956fe fix #1705: Do not re-enable mod after update (#2001)
* fix #1705: Do not re-enable mod after update

* update

* Do not select disabled mods by default
2023-01-14 02:37:11 +08:00
Glavo 0130676b42 fix #1999: Add overrideJavaArguments to LaunchOptions (#2000)
* fix #1999: Add overrideJavaArguments to LaunchOptions

* fix checkstyle
2023-01-14 02:16:20 +08:00
Glavo 0ec00ede70 close #1980: Make LittleSkin optional (#1995)
* close #1980: Make LittleSkin optional

* update

* update

* update
2023-01-14 02:15:07 +08:00
Glavo 42dd877b97 Bump junit from 4 to 5 (#1997) 2023-01-14 01:48:38 +08:00
Glavo 00e8ae691c fix CI 2023-01-12 15:17:37 +08:00
Glavo 1cc429c7ca 优化 mod 列表加载速度 (#1985)
* Share ZipFileSystem

* fix memory leak
2023-01-08 18:15:55 +08:00
Glavo bf822d7cf7 Update all icons (#1988) 2023-01-08 18:15:01 +08:00
Glavo 5dc1d14f73 fix #1562: Do not save stage size when window is minimized (#1979) 2023-01-07 21:38:36 +08:00
Glavo 15a93a9dc0 Deprecated Pack200 (#1977) 2023-01-07 21:38:06 +08:00
Glavo 0c0cd97a6e Check whether HMCL is in temporary path when starting (#1960)
* Check whether HMCL is in temporary path when starting

* Update the path of the trash folder on Linux
2023-01-07 21:36:10 +08:00
Glavo b132aba1e3 fix #1927: limit title label width (#1982) 2023-01-07 21:34:01 +08:00
zkitefly 7d97e5d104 改进 Github Issue 模板 (#1976)
* Update bug-report.yml

* Update bug-report.yml

* Update config.yml

* Update bug-report.yml

* Update bug-report.yml

* Update bug-report.yml

* Update bug-report.yml
2023-01-07 21:33:35 +08:00
Glavo 7c8d75f5a4 fix memory leak (#1986) 2023-01-07 21:33:16 +08:00
Glavo 1b5522958a 更新 FeedbackPage (#1913)
* Update FeedbackPage

* Update logo

* close #1259
2023-01-07 21:32:52 +08:00
Glavo 593e70fa43 优化对 Java 兼容性问题的检测 (#1973)
* Optimization Tips

* cleanup code
2023-01-07 21:29:18 +08:00
Glavo 6475e03c01 Improve quilt support (#1974) 2023-01-07 21:27:25 +08:00
Glavo 2d4ca3100e fix NPE (#1983) 2023-01-07 21:26:01 +08:00
Glavo 3f0e8138b1 fix Modpack::acceptFile (#1972)
* fix Modpack::acceptFile

* fix separator
2023-01-07 21:25:36 +08:00
Glavo 9e6d6ff95a Delete javaw.exe extension filter (#1969) 2023-01-07 21:24:22 +08:00
Chen Youyou 6ae6020b73 语言文件中错别字改正 (#1981)
* 把“及表示登录成功”的“及”改为“即”

又写错别字了。。

* 把“及表示登入成功”的“及”改为“即”

又写错别字。。
2023-01-07 21:24:00 +08:00
Glavo 4b14673aaf Update JavaFXPatcher (#1955) 2023-01-01 14:35:55 +08:00
Glavo bd7365e944 Add icon for qq channel (#1956) 2023-01-01 14:34:58 +08:00
Glavo 8c38e4c628 fix tags color (#1959) 2023-01-01 14:34:36 +08:00
Glavo 2b4e476153 fix crash on Java 8 (#1954) 2022-12-30 17:21:57 +08:00
Glavo 0f0f5cbc28 fix #1905: fix path splicing (#1951) 2022-12-30 17:01:49 +08:00
Glavo bed74dc66c fix find HMCL directory (#1953) 2022-12-30 17:00:10 +08:00
Glavo ba196dcdf4 Do not put LittleSkin in authlibInjectorServers (#1952) 2022-12-30 16:59:29 +08:00
zkiteflyandYuhui Huang 9f8ca0255b 修改提示 (#1919)
* 修改提示

* fix

* Update I18N_zh_CN.properties

Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2022-12-30 02:07:09 +08:00
zkitefly b6a156b0d6 修复 (#1921) 2022-12-30 02:04:15 +08:00
Glavo d032b3c677 close #1376: 在用户文件夹中存储账户信息 (#1941)
* Save accounts in hmcl dir

* close #1377: Add conversion button

* fix YggdrasilAccount::getIdentifier()

* Check whether the account exists before moving

* Add server url to selected account

* Add global prefix
2022-12-30 02:03:19 +08:00
Glavo 4c2d1063ec fix #1922: Add Xms only when the min memory is less than the max memory (#1949) 2022-12-30 01:59:02 +08:00
Yingchi Long 2e7e2d261c feat: use XDG_DATA_HOME as for Linux working directories (#1948) 2022-12-30 01:58:15 +08:00
zkitefly 9750c69742 fix #1926 (#1940) 2022-12-28 23:22:46 +08:00
Glavo d67dd700e9 修复主题相关问题 (#1915)
* Update blue.css

* fix

* fix

* Cleanup Code

* Use blue.bss

* fix read theme
2022-12-17 18:34:10 +08:00
zkitefly 7f599638d7 完善 QQ 频道 的入口 (#1910)
* 完善 QQ 频道 的入口

* 日志分析窗口结果中添加相关入口

* 完善

* 取消使用qq图标
2022-12-11 00:43:26 +08:00
Glavo bf04854e68 弃用 FXML (#1911)
* Remove message-dialog.fxml

* Remove authlib-injector-server-add.fxml

* Remove profile.fxml

* Remove task-dialog.fxml

* Flatten AddAuthlibInjectorServerPane

* Flatten MessageDialogPane

* Remove unused fxml files

* Remove modpack.fxml

* Remove modpack-source.fxml

* Remove selection.fxml

* Remove type.fxml

* Remove versions.fxml

* Remove input-dialog.fxml

* Remove fxml

* Cleanup code

* Cleanup code

* Cleanup Code

* Cleanup Code
2022-12-10 22:36:14 +08:00
Glavo 2bcf9ee39e Optimize automatic memory allocation (#1914) 2022-12-10 22:32:09 +08:00
huanghongxun 7a90035278 feat(about): add link to qq channel. 2022-12-07 15:52:55 +08:00
Glavo a7108f318a Adjust JVM arguments priority (#1898)
* Adjust JVM arguments priority

* #1889: Use the default JVM arguments consistent with the official launcher

* Follow -Xmx settings in modpack info

* Do not force generation of -Xmx
2022-12-06 13:34:32 +08:00
zkitefly a3eaccb45b exe and sh download java link update (#1903)
* Update main.cpp

* exe and sh download java link update

* fix
2022-12-06 13:33:00 +08:00
zkitefly e0c97da9fd Update main.cpp (#1900) 2022-12-03 23:22:15 +08:00
zkitefly 3843002113 (c)->© (#1899)
* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N_es.properties

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N.properties

* `(c)`->`©`

* fix

* Update AboutPage.java

* Update AboutPage.java
2022-12-03 15:05:42 +08:00
Glavo 1ec9e41459 fix #1885: filter letter color code (#1890) 2022-12-02 17:28:25 +08:00
Glavo a6231ac593 修复内存泄漏 (#1888)
* Fix memory leaks

* Cache icon in cell

* Lazy acquisition of availableCharsets
2022-12-02 17:27:52 +08:00
Glavo ea91ee91ec close #1199: Reimplement Zipper (#1891) 2022-12-02 17:26:00 +08:00
Glavo d4c0c30283 修复在精简 JRE 上崩溃的问题 (#1893)
* Do not load javafx.web on unsupported platforms

* No longer requires jdk.zipfs to exist
2022-12-02 17:23:08 +08:00
Glavo 9e2c46a6f8 Gradle 7.6 (#1895) 2022-12-02 17:22:09 +08:00
Glavo ce31bcc6e0 Catch the exception that the OperatingSystemMXBean does not exist 2022-12-01 03:51:21 +08:00
Glavo d11e22026c 修复遗留问题 (#1875)
* Check the sha1 file of java in the minecraft runtime dir

* Fix download Java on macOS

* misc

* Verify Java at startup

* Find the Java installed by official launcher on osx-arm64

* Extract the downloaded file to a temporary file

* Rollback JavaRepository.getSystemJavaPlatform
2022-11-26 21:43:35 +08:00
Glavo a96fe298b8 fix #1108 (#1882)
close #1791
close #1868
2022-11-26 21:43:03 +08:00
Glavo 8e28c2487f Load gif background (#1877) 2022-11-26 20:33:46 +08:00
Glavo fda6f75333 fix #1553: Hide popup before rolling back mod (#1881) 2022-11-26 20:33:02 +08:00
zkitefly e414450f3c 优化日志分析器 (#1813)
* 优化日志分析器

* 修复翻译错误

* 修复翻译错误

* 修复翻译错误

* Update CrashReportAnalyzer.java

* Update I18N_zh_CN.properties

* 逐步修复问题

* 逐步修复问题

* 尝试修复

* 修复错误

* 修复错误

* 尝试

* 尝试

* 修复

* 按照要求修改

* 依据 Check Style 修复问题

* 按提示修改

* 修复错误

* Update CrashReportAnalyzer.java

* 删除

* Update I18N_zh_CN.properties

* Update CrashReportAnalyzer.java
2022-11-25 22:45:00 +08:00
Glavo 95e15d7df4 Replace ObservableMap with MapProperty (#1870) 2022-11-25 22:40:16 +08:00
Glavo 7e826a5952 close #1803: Add new rule to CrashReportAnalyzer (#1872) 2022-11-25 22:37:41 +08:00
Glavo 43e2644f0e fix NPE (#1871) 2022-11-25 22:37:05 +08:00
Glavo a0ef16bef4 close #1781: Add suggestions for naming (#1873) 2022-11-25 22:35:01 +08:00
Glavo f5d83095cc 修复 Java 下载相关问题 (#1874)
* fix #1561: Replace existing files when downloading Java
close #1756

* close #1755: Download Java using mirror

* Skip existing files
2022-11-25 22:34:26 +08:00
Glavo a94bb407e5 Submit the modification of JFXTextField after editing (#1869) 2022-11-25 17:20:16 +08:00
Glavo 6611774aa9 Fix the problem of garbled logs on Java 18+ (#1866) 2022-11-25 17:17:31 +08:00
Glavo e1eb40c129 Optimization HMCLProcessListener::onLog (#1867) 2022-11-25 17:16:23 +08:00
Glavo 2e532be34c 将 Quilt 图标添加至 VersionIconDialog (#1864)
* Safely deserialize VersionSetting

* Add quilt to VersionIconDialog
2022-11-24 12:54:56 +08:00
Glavo b9d0cfc12a Update GitHub Action (#1863) 2022-11-24 12:54:48 +08:00
Glavo 3d88bc35bb fix memory leak (#1865) 2022-11-24 12:53:16 +08:00
Glavo 8c9c8fd28f 修复 macOS Dock 中的图标 (#1862)
* High resolution logo

* Load original icon on Java 8
2022-11-24 12:52:54 +08:00
Glavo 5e685b7f26 Modify the version number for unofficial builds (#1861) 2022-11-23 22:54:28 +08:00
Glavo c76b562705 Remove log4j-patch (#1860)
* Remove log4j-patch

* Update I18N
2022-11-23 17:20:15 +08:00
Glavo 4c9f8d3dca Complete the missing translation (#1859) 2022-11-23 17:19:45 +08:00
Glavo 0f26ae5cfc 代码清理与修复 UI 卡顿资源占用过高的问题 (#1849)
* Lazy initialization of Swing

* Load ISRG Root X1 certificate only on Java 8

* Replace JOptionPane with JavaFX Alert

* Avoid using java.awt.Desktop

* Rewrite TexturesLoader

* Optimization SelfDependencyPatcher

* fix typo

* close #968: Use computeIfAbsent to ensure thread safety

* Optimization GameVersion::minecraftVersion

* code cleanup

* Set the initial capacity of readFullyWithoutClosing

* code cleanup

* Mark inner classes as static if possible

* Cache version icon

* Code cleanup

* Fix ListView scrolling performance issues

* DatapackListPage::items

* Replace OutputStream with FileChannel::write
2022-11-23 16:33:14 +08:00
Glavo 035469240d close #1855: Add 1.19 to DEFAULT_GAME_VERSIONS (#1857) 2022-11-23 16:32:20 +08:00
Glavo 7ee81f7425 Deny access to CurseForge without api key (#1858) 2022-11-23 16:32:07 +08:00
zkitefly 024c90d688 在HMCL显示changelog时自动定位该更新内容 (#1850)
* 在HMCL显示changelog时自动定位该更新内容

* 尝试修复错误

* updata

* fix
2022-11-23 14:07:13 +08:00
Glavo 1253272626 fix #1852: Lookup manifest in this jar (#1853) 2022-11-23 14:06:35 +08:00
zkitefly 73b938cfd9 修复微软账户登录弹窗不显示“忘记密码”的问题 (#1829) 2022-11-13 23:44:01 +08:00
Glavo ee843543bf 性能与内存优化 (#1848)
* optimization

* Make ManagedProcess::getLines thread safe
2022-11-13 23:43:45 +08:00
skbeh fa8a479aac Bump Gradle Shadow to 7.1.2 (#1831)
This allows compilation with `sourceCompatibility = 17`.
2022-11-13 23:40:10 +08:00
Glavo bac14db432 修复无法正确处理带有 Quilt 的 MultiMC 整合包的问题 (#1841)
* fix #1760: typo

* export quilt info in mmc modpack
2022-11-13 23:39:56 +08:00
zkitefly f6242ea868 添加提示 (#1844) 2022-11-13 23:39:40 +08:00
zkitefly 16db35655a 修改 2022-11-11 21:50:10 +08:00
Glavo 53336aedc9 Remove HiPer Support 2022-11-11 18:30:27 +08:00
zkitefly eaddb67932 Update HelpPage.java (#1827) 2022-11-05 23:21:47 +08:00
Glavo c7b7dd9500 fix crash (#1826) 2022-11-05 22:51:10 +08:00
Glavo 9901bf738d fix crash (#1825) 2022-11-05 22:12:44 +08:00
huanghongxun a3d8c6b3b3 feat(skin): introduce new official skins. 2022-11-05 20:22:02 +08:00
huanghongxun 948928f740 feat: update HMCLauncher.exe 2022-11-05 20:11:22 +08:00
Rei Hatada 71e98c035e change (#1824) 2022-11-05 19:51:39 +08:00
Glavo af107b15c0 完善 HiPer 支持 (#1821)
* Display hiper token expiration time

* Automatically download missing files when starting HiPer

* Disable start HiPer when token is empty
2022-11-05 19:36:17 +08:00
Glavo 8687bb9487 修复 MessageDialogPane 总是折叠内容的问题 (#1815)
* fix #1751: Use ScrollPane in message dialog only when needed
close #1777

* Parse segment only if it contains xml tag
2022-11-05 19:28:24 +08:00
huanghongxun a70e53364d fix(multiplayer): force using authlib-injector in multiplayer mode. 2022-10-30 13:45:34 +08:00
zkitefly 11a1c37765 将日志更换至docs.hmcl.net (#1812)
* 将日志更换至docs.hmcl.net,以便更新日志

* 修改链接

* 更换目录
2022-10-30 01:29:51 +08:00
Glavo 68a8944810 改进 HiPer 支持 (#1763)
* Disable start hiper button when token is empty

* Use gsudo start hiper

* Automatically request administrator privileges on Windows and Linux

* Automatically start hiper with sudo on mac

* Update I18N

* Prompt user to chown unwritable files

* Update HiPer file verification failed prompt

* Log other hiper exceptions

* temporarily disable gsudo

* Detect admin rights with 'net session'

* Hide the hint pane when running with administrator privileges

* Prompt user when starting HMCL with sudo

* Fix ClassNotFoundException when no JavaFX

* Update the prompt when the permission cannot be obtained

* Add support for saving multiple HiPer configurations

* update link
close Glavo/HMCL#3

* Detect RISC-V 32

* Save original hiper authorization information

* Add support for importing and exporting license files

* Set initial license file name

* Complement the missing message

* Handling gsudo failure to obtain permission
2022-10-25 21:55:12 +08:00
zkitefly b280b238df 尝试完成 #1780 (#1789) 2022-10-25 21:52:23 +08:00
huanghongxun 0f39aad429 fix(auth): legacy user type for OfflineAccount. 2022-10-25 21:48:55 +08:00
Glavo ff78297ee5 fix IntegrityCheck::performCheck (#1792) 2022-10-23 23:28:22 +08:00
Glavo f75bbdcecc Log HMCL jar path (#1794) 2022-10-23 23:27:42 +08:00
Glavo 694e31a7fe Cache manifest in JarUtils (#1797) 2022-10-23 23:27:21 +08:00
Glavo c113670cda Migrate HMCL_DIRECTORY to the user directory (#1785) 2022-10-09 13:21:02 +08:00
zkitefly 1b3c399335 添加“测试能否联机”按钮 (#1757)
* add

* 修改图标(其实我不知道该弄啥图标好……)

* fix
2022-10-09 11:18:00 +08:00
huanghongxun 45cbe20449 fix: crash when there is an incorrect authlib-injector server url. Closes #1774. 2022-10-08 17:39:04 +08:00
zkiteflyandYuhui Huang 4d606510f6 修改 登录失败 弹窗 (#1782)
* 修改 登录失败 弹窗

* fix

* Update I18N.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Edit

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-10-08 17:33:07 +08:00
huanghongxun a6639552cf fix: lblCreationWarning not wrapping text. Closes #1783. 2022-10-08 17:31:28 +08:00
zkitefly 2a088ec555 更新多人联机页面 (#1758)
* 更新 multiplayer.error.file_not_found 翻译

* 修改翻译

* 移除标题

* add

* try fix

* try fix

* try fix

* try fix

* try fix

* try

* try

* try

* fix

* fix

* add

* fix

* fix

* add

* fix

* edit
2022-10-08 17:13:55 +08:00
zkitefly 1c46d63367 try (#1768) 2022-10-08 17:12:13 +08:00
zkitefly f2b7fae694 添加“忘记密码”按钮 (#1765) 2022-10-08 17:11:56 +08:00
Glavo 43986676d8 修复中文路径下启动 <= 1.12.2 的版本没有声音的问题 (#1754)
* Use native encoding by default

* Fix the problem that LWJGL fails to load OpenAL under Java 18
2022-10-08 17:10:27 +08:00
Glavo 2649e79d47 fix build fails with Java 19 (#1784) 2022-10-08 17:09:12 +08:00
zkitefly f83d3cd769 Fix https://github.com/huanghongxun/HMCL/issues/1703 (#1753) 2022-09-28 19:14:15 +08:00
zkitefly f124910f94 修改多人联机帮助与提示 (#1733)
* 修改多人联机提示

* text

* 撤回修改

* 1

* 修改帮助

* fix

* try

* Delete try

* Update I18N.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* 修改提示框样式

* 修改提示框样式

* fix

* 修改样式

* 修改样式

* 修改提示框样式

* 修改翻译

* 修改翻译

* add

* return

* add

* add

* 去掉图标

* 试图让左边图标为空

* Update MultiplayerPageSkin.java

* ruturn

* add

* add

* Update MultiplayerPageSkin.java

* Update I18N_zh.properties

* Update I18N_zh_CN.properties
2022-09-25 14:25:05 +08:00
zkitefly c1712c3e1e 修改翻译 (#1742) 2022-09-25 14:23:39 +08:00
huanghongxun d817c80629 fix: checkstyle. 2022-09-25 00:31:51 +08:00
huanghongxun 8d830dbf96 fix(microsoft): ok cannot be clicked. Closes #1729. 2022-09-24 23:54:36 +08:00
huanghongxun 0396e96334 feat(multiplayer): update agreement. Closes #1724. 2022-09-24 23:50:48 +08:00
huanghongxun 7b23c7bc79 feat(multiplayer): link to video tutorial. 2022-09-24 23:36:48 +08:00
huanghongxun d3ca50259a fix(modrinth): quilt is already supported. Closes #1746. 2022-09-24 23:16:11 +08:00
huanghongxun 499a65d5fb fix(multiplayer): connection. 2022-09-24 23:03:07 +08:00
huanghongxun 0b61f9856c fix(multiplayer): ui incorrect 2022-09-24 21:55:53 +08:00
huanghongxun c8409dd258 build: update gradle. 2022-09-24 18:38:04 +08:00
huanghongxun e5ea4cd79f feat(multiplayer): broadcast address to local network. 2022-09-24 18:31:22 +08:00
huanghongxun 4d54c7f6ba feat(multiplayer): show token expire time. 2022-09-24 18:31:22 +08:00
Cato : Love the world and myselfandzkitefly 0f4a5c26df 远端配置无法读取,使用本地缓存 (#1740)
* 远端配置无法读取,使用本地缓存

* 远程配置&本地配置确认

* fix err

* fix-err

* fix-err

* fix-err

* fix-err

* restore

* code clean

* fix-err

* add

* fix

* 远端读取异常日志

Co-authored-by: zkitefly <64117916+zkitefly@users.noreply.github.com>
2022-09-24 18:17:34 +08:00
Akira Yamazaki a8b09e2195 修复执行 PreLaunchCommand 时会被阻塞的问题 (#1737) 2022-09-24 16:57:19 +08:00
Cato : Love the world and myself 3ed6e38ad3 新增有效期getValidAt #1727 (#1732)
* 界面标题描述纠正

* 新增有效期getValidAt #1727

* fix err

* fix-err-again

* fix-err

* fix

* fix-4

* rename

* fix

* fix

* code clean
2022-09-24 16:56:19 +08:00
huanghongxun 2028b0ef9c fix(multiplayer): restore LocalServerBroadcaster and impl port forwarding. 2022-09-17 18:36:12 +08:00
zkitefly aa7f9b91c7 修改微软账户重新登录页面提示 (#1710)
* 修改微软账户重新登录页面提示

* 添加忘记添加的

* 修改翻译

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties
2022-09-17 12:15:56 +08:00
Cato : Love the world and myself d6598310b2 界面标题描述纠正 (#1728) 2022-09-17 12:11:00 +08:00
zkitefly ce6c06987c 修改多人联机提示 (#1725)
* 修改多人联机提示

* 修改

* fix
2022-09-17 12:10:46 +08:00
huanghongxun 90955d7916 fix(ui): ComponentList prefheight wrong. 2022-09-16 02:34:23 +08:00
huanghongxun b3bbd21f1f chore(multiplayer): remove JSTUN. 2022-09-16 01:41:23 +08:00
Cato : Love the world and myselfandYuhui Huang 735a407f0e 缺少字符矫正 / NAT 检测移除 (#1718)
* 缺少字符矫正 / NAT 检测移除

* 错误修复

* 修正nat移除不彻底导致的编译错误

* 授权协议发布

* code-clean

* fix-err

* fix-err-2

* update-in18

* url-fix

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-09-16 01:18:26 +08:00
Glavo 8461b0418c close #1534: open 'java.com' when need to download Java 8 (#1716) 2022-09-15 21:28:02 +08:00
Glavo e46ab8bf9e fix the build errors and checkstyle (#1717) 2022-09-15 21:27:43 +08:00
zkitefly 330548b24e 添加游戏档案创建链接 (#1696)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* add makegameidsettings

* fix

* fix

* fix

* Update CreateAccountPane.java

* Update CreateAccountPane.java

* 听取意见,修改变量名为createProfileLink
2022-09-14 21:46:38 +08:00
Cato : Love the world and myselfandYuhui Huang cf5ccca42d tap/tun二选一,目前优先tun先看效果。 (#1713)
* tap/tun二选一,目前优先tun先看效果。

* 描述修正

* 修正故障描述

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-09-14 21:45:55 +08:00
Cato : Love the world and myself a18bd0729b 配置文件读取失败及证书失效判断 (#1715)
* 配置文件读取失败及证书失效判断

* 描述修正
2022-09-14 21:43:44 +08:00
huanghongxun 662b42c40f fix(multiplayer): get ip from hiper log. 2022-09-12 23:05:13 +08:00
huanghongxun b69c400a08 fix(multiplayer): get ip from hiper log. 2022-09-12 22:45:38 +08:00
huanghongxun f1eb2da57c feat(multiplayer): hiper. 2022-09-12 22:38:36 +08:00
huanghongxun 99be810b5e feat(multiplayer): wip: hiper 2022-09-12 22:38:36 +08:00
zkitefly 03541a6f85 修改 response.errorDescription.contains() 参数 (#1708)
* 添加两个 response.errorDescription.contains()

* 再添加一个qwq

* 删除重复项

* 恢复删除的项

* 修改

* 修改
2022-09-10 21:50:08 +08:00
Chikage0o0 5fe1ef899d fix modrinth modpack install (#1702)
* fix modrinth modpack install

* fix NullPointerException
2022-09-10 21:49:31 +08:00
CNCloud_Casey 6a81f4d240 更新整合包安装的文本介绍 (#1695)
* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_ru.properties

* Update I18N_ja.properties

* Update I18N_es.properties

* Update I18N.properties
2022-09-05 20:11:10 +08:00
zkitefly 3c6aa489e8 添加sh支持 (#1697) 2022-09-05 20:10:49 +08:00
zkitefly ba7043fa92 修改一些提示 (#1699) 2022-09-05 20:10:37 +08:00
huanghongxun bfb6c99484 fix: checkstyle. 2022-09-04 00:26:14 +08:00
huanghongxun a6b26a0cd9 fix(microsoft): show relogin dialog when grant expired. Closes #1691. 2022-09-03 23:35:09 +08:00
huanghongxun 0c7b0f285e fix(microsoft): retry 5 times when socket timeout. Closes #1691. 2022-09-03 23:33:11 +08:00
huanghongxun 5e1ae56740 fix(ui): error dialog overflow. Closes #1677. 2022-09-03 22:44:38 +08:00
huanghongxun b17bac050b fix(ui): not parsing xml. Closes #1678. 2022-09-03 22:28:58 +08:00
huanghongxun b1952a7b37 fix(ui): not wrapping text. Closes #1673. 2022-09-03 22:26:05 +08:00
huanghongxun cc1fc1e4be fix(ui): some text overflow. Closes #1694. 2022-09-03 22:21:38 +08:00
Glavo 6c07924d9b 完善 osx-arm64 原生支持 (#1689)
* Add 1.6~1.18.2 support for osx-arm64

* fix crash on 1.12

* Update I18N
2022-09-03 16:55:27 +08:00
Glavo 835668d781 Update purchase link (#1692) 2022-09-03 16:53:55 +08:00
Glavo 8f7c09d2cc 添加 Linux MIPS64el 平台支持 (#1686)
* Add support for linux-mips64el

* Update PLATFORM.md
2022-09-02 11:37:09 +08:00
Chikage0o0 837939e27c Fix #1684 (#1685)
* 修复生成option.txt与YOSBR模组的冲突

* only search 1 layer
2022-09-02 11:36:58 +08:00
Glavo 058a8e2335 Update JavaVersionConstraint (#1683) 2022-09-01 22:20:16 +08:00
CNCloud_Casey a5af5265be Update README (#1682) 2022-08-31 15:25:29 +08:00
Gamom 8a724587ca 更新错误的翻译键 world.info.player.game_type.adventure (#1674)
* 更新错误的翻译键 world.info.player.game_type.adventure

我不理解 adventure 冒险 为什么会 翻译成极限 ...

* Update I18N_zh.properties
2022-08-31 14:20:56 +08:00
zkitefly bef9c0ad88 Fix #1675 (#1676) 2022-08-31 14:20:39 +08:00
Glavo 8d29775410 添加 Windows on Arm 平台的支持 (#1679)
* Add Minecraft 1.6~1.18.2 support for windows-arm64

* Prompt at startup on non-x86 platforms

* Automatically download JavaFX on windows-arm64

* Update PLATFORM.md
2022-08-31 14:20:27 +08:00
Glavo 620c0e4c73 Automatically download JavaFX on linux-loongarch64_ow (#1670) 2022-08-29 19:28:20 +08:00
Glavo b5c92c1564 Fix stuck on startup when no suitable Java is found (#1671) 2022-08-29 19:27:51 +08:00
huanghongxun 3c3c28aaa5 fix(launch): not escape spaces in exported shell script. Closes #1541. 2022-08-29 00:19:32 +08:00
huanghongxun a80fc34ef1 fix(launch): should not create file when throwing CommandTooLongException. Closes #1606. 2022-08-29 00:03:35 +08:00
huanghongxun 08d7ff138b fix(login): prompt to retry logging in. Closes #1658 #1591 #1544 1608. 2022-08-28 23:57:47 +08:00
huanghongxun 0c172e5d08 fix(crash): incorrect layout for feedbackLabel. Closes #1667. 2022-08-28 21:45:05 +08:00
Glavo bce2d1ea5c Revert ExitWaiter (#1668) 2022-08-28 21:05:43 +08:00
huanghongxun cbc1e26b6f feat(log): hyperlink for log analyzer feedback. Closes #1652. 2022-08-28 17:12:03 +08:00
Yuhui Huang 3b5ddcafa6 fix(quilt): Quilt Hash not enabled. Closes #1657. 2022-08-28 17:03:54 +08:00
Glavo c6a5624708 引导用户下载 Java (#1659)
* Direct users to download Java

* Read JVM options from environment variables
2022-08-28 17:00:15 +08:00
Glavo 23cc568bac Support Microsoft login in dev version (#1651) 2022-08-28 16:59:03 +08:00
Glavo 7ff679c3c8 Revert AdvancedListBox (#1647)
fix #1258, fix #1397, fix #1559, fix #1589, fix #1646
2022-08-28 16:56:52 +08:00
Glavo c873352ee9 更多平台的原生适配 (#1654)
* Add Minecraft 1.13+ support for linux-loongarch64_ow

* Add Minecraft 1.6~1.12.2 support for linux-arm32

* Update documents
2022-08-28 16:56:33 +08:00
Glavo a4164a86af Support path containing unicode characters on Mac OS (#1661) 2022-08-28 16:54:22 +08:00
Glavo c21671e86b 修复通过自动安装更新游戏时的崩溃 (#1662)
* fix game upgrade

* fix #1459: Invalidate the version number cache before saving the version
close #1566
2022-08-28 16:52:26 +08:00
Glavo d9bdc2b1f7 fix #1579: Hide tokens in the exported Minecraft logs (#1665) 2022-08-28 16:51:49 +08:00
huanghongxun 19b6633f4a fix: checkstyle. 2022-08-21 21:29:09 +08:00
huanghongxun 2ad3e7a291 fix(account): Authlib-Injector server may be unknown when in CreateAcountPane tab mode. 2022-08-21 21:26:20 +08:00
huanghongxun 1fa63a528d fix(download): installer items overflow. Closes #1645. 2022-08-21 21:20:10 +08:00
Glavo b80c787b60 为非 x86 平台提供开箱即用的启动支持 (#1610)
* Replace native libraries on non-x86 platforms

* Add support for Linux ARM

* Relax the constraint 'JavaVersionConstraint.VANILLA_X86'

* Fix several hints

* Add OpenGL software renderer option

* Add support for OpenGL software renderer on windows

* Add __GLX_VENDOR_LIBRARY_NAME on Linux

* Catch GLFW error 65543

* Allow users to disable auto completion of native libraries

* fix NPE when violated mandatory constraint

* Add support for Minecraft 1.13

* Add support for Minecraft 1.6~1.12

* Add base support for linux-loongarch64

* Improve warnings on Loongson platform

* Add linux launcher script

* fix typo

* Add support for loongarch64 (old world)
2022-08-21 19:00:05 +08:00
Lonelyxiya c8652012c5 I18N_zh.properties (#1611)
修訂了一些台灣詞彙。來自https://discord.com/channels/815819580840607807/815860830058840064/1003644502478704671
RPMTW的翻譯討論頻道
2022-08-21 18:27:27 +08:00
Glavo e70b43be26 Temporarily rollback HMCLauncher.exe (#1625)
close #1583, close #1577
2022-08-21 18:26:54 +08:00
K1t3AB 2d182807b2 feat(README_cn.md): Added Chinese translation for README.md (#1637)
* feat(README_cn.md): Added Chinese translation for README.md

* edit(README.md): Improved formats for README.md

* fix(README): Roll back to Commit ID to pin lines

* edit(README): Let languages switchers "more like buttons"

* fix(README): Fix errors caused by misunderstanding

* edit(README): Added 8 spaces
2022-08-21 18:26:42 +08:00
huanghongxun 675af3a5aa feat(download): quilt installation. 2022-08-21 18:20:11 +08:00
huanghongxun 4ba4c3e749 feat(quilt): WIP: partially implements quilt installation. 2022-08-20 15:52:22 +08:00
huanghongxun 11e2277c9e feat(account): show little-skin login method by default. Closes #1622. 2022-08-20 14:58:38 +08:00
Yuhui Huang 66642fe621 fix(i18n): line ending of Spanish lang file. 2022-08-08 17:21:19 +08:00
Nekrodamus 3189a22f26 Update I18N_es.properties (#1614)
* Update I18N_es.properties

Updated the Spanish language file to the latest Beta version, using as template the I18N.properties file.

[Completed]

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

More fixes.

* Update I18N_es.properties

Corrected the label for modpacks "Investigaciones" to "Quests".

* Update I18N_es.properties

More fixes

* Update I18N_es.properties

Final fixes

* Update I18N_es.properties

Fixed bug that caused a string not to be translated.

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

Little fixes

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

Improved the context of some sentences

* Update I18N_es.properties

Reverted fix

* Update I18N_es.properties

Changes in capitalization

* Update I18N_es.properties

Some fixes...

* Update I18N_es.properties

More corrections...
2022-08-08 17:19:59 +08:00
Tungstend 366e359d83 修复litemod信息获取失败 (#1545)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全

* Modrinth源获取类别列表,搜索时增加类别选项

* Update ModrinthRemoteModRepository.java

* Update ModrinthRemoteModRepository.java

* 修复cf源获取前置模组失败

* Revert "Update ModrinthRemoteModRepository.java"

This reverts commit 40b7c1d59d.

* Revert "Revert "Update ModrinthRemoteModRepository.java""

This reverts commit 2027c9ef4c.

* Revert "修复cf源获取前置模组失败"

This reverts commit 4404daf0ea.

* fix conflict

* 修复cf源无法获取前置模组

* fix check style

* Update LiteModMetadata.java
2022-08-08 16:58:55 +08:00
ZekerZhayard f7d8a473aa Fix NoSuchMethodError when compiling with Java 9+ (#1585) 2022-08-08 16:58:14 +08:00
zkitefly 8276bf5d8c 在 README 中添加 Fabric (#1587)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update README.md
2022-08-08 16:57:47 +08:00
zkitefly 371150dfa8 添加提示 (#1597)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* text

* text

* now

* now

* 优化下简体,繁体,和英文

* 再优化了下

* fix

* fix

* text

* text

* now
2022-08-08 16:57:31 +08:00
Glavo 38d71fd6bb Add world info page (#1620) 2022-08-08 16:56:03 +08:00
king tsaltedfishandYuhui Huang c627ca96c9 Rewrite badges (#1612)
Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-08-08 16:52:58 +08:00
Rovniced 09167bb302 修复curseforge文件名存在中文下载失败 (#1578)
* Update RemoteMod.java

* Update RemoteMod.java

* Update RemoteMod.java
2022-08-08 16:52:02 +08:00
zkitefly a3c24b1b02 因 开黑啦 改名成 KOOK,所以改名 (#1600)
* 修改加入链接的前缀“kaihei.co”->“kook.top”

* 因开黑啦改名成KOOK,所以改名。邀请链接也做相应调整。

* Update bug-report.yml

* Update bug-report.yml

顺便无伤大雅的修改了下这里

* Update config.yml

修改了下这里的英文空格隔离

* Add files via upload

上传新logo

* Delete kaiheila.jpeg

删除旧logo

* Update I18N.properties

修改回原来样式

* Update I18N_zh.properties

修改開黑啦为KOOK
2022-08-08 16:51:38 +08:00
Glavo 9f4a3cf8a8 Disable Microsoft login when auth id is empty 2022-06-19 18:58:38 +08:00
xmdhs 702d27393e replace piston-data.mojang.com 2022-06-17 11:16:33 +08:00
xmdhs 698d3a311d replace piston-meta.mojang.com 2022-06-17 11:16:33 +08:00
Glavo c92f8e3b51 Complete optifine that is not installed automatically 2022-06-16 20:35:14 +08:00
Glavo 7c502ecd01 fix #1311: fix auto complete OptiFine 2022-06-16 20:35:14 +08:00
Glavo a237a31d23 Output the URL in the log when the file is not found 2022-06-16 20:35:14 +08:00
Glavo 5f4fc77258 Use language-independent links 2022-06-15 11:42:16 +08:00
ZekerZhayard 8bd4df68f5 Fix #1543 2022-06-14 12:49:56 +08:00
Glavo 29cff02f3c Update JavaVersionConstraint 2022-06-13 23:30:41 +08:00
Glavo d1bc41e1f1 Detect loongarch 2022-06-13 23:30:41 +08:00
huanghongxun 2fd1bf4c29 fix(i18n): incorrect translation change. 2022-06-05 14:11:04 +08:00
huanghongxun edd89fb040 fix: test. 2022-06-03 23:59:35 +08:00
ByacryaandYuhui Huang 08d6b58c8c feat(i18n): greatly improve en_US translation (#1522)
It seems that the current translation was largely done by Google Translation, so I did some work to improve it.

Some translations can definitely be improved, but I don't have much time at the moment so I will leave it for now.

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-06-03 23:27:27 +08:00
huanghongxun e3fc9f5116 feat: auto find java by default. Closes #1511. 2022-06-03 23:19:47 +08:00
huanghongxun 6dcd808cc8 fix(crash): suggest jdk corruption. Closes #1509. 2022-06-03 23:17:00 +08:00
huanghongxun df6230fc01 fix(install): date deserialization. Closes #1512. 2022-06-03 23:13:40 +08:00
huanghongxun 9f502ae6ae fix(modpack): missing .mrpack extension. Closes #1531. 2022-06-03 22:53:23 +08:00
huanghongxun 2a979d2b15 fix(download): incorrect mcmod url for modpacks. Closes #1525. 2022-06-03 22:47:03 +08:00
huanghongxun ae484e3b67 fix(download): inappropriate translation for "Vanilla+" 2022-06-03 22:47:02 +08:00
Byacrya 7206832edb feat(i18n): correct zh_CN translation
Vanilla+ should be translated to '原版增强' instead of '香草+'.
2022-06-03 21:08:45 +08:00
zkitefly 288b32ed79 I18N.properties (#1526)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_ru.properties

* Update I18N_ja.properties

* Update I18N_es.properties

* Update I18N.properties
2022-06-03 17:01:37 +08:00
zkitefly 53bc3b354f 修改微软账户上传皮肤链接 (#1521)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update AccountListItem.java
2022-06-02 20:53:39 +08:00
Tungstend 30565c2b50 修复cf源无法获取前置模组 (#1520)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全

* Modrinth源获取类别列表,搜索时增加类别选项

* Update ModrinthRemoteModRepository.java

* Update ModrinthRemoteModRepository.java

* 修复cf源获取前置模组失败

* Revert "Update ModrinthRemoteModRepository.java"

This reverts commit 40b7c1d59d.

* Revert "Revert "Update ModrinthRemoteModRepository.java""

This reverts commit 2027c9ef4c.

* Revert "修复cf源获取前置模组失败"

This reverts commit 4404daf0ea.

* fix conflict

* 修复cf源无法获取前置模组

* fix check style
2022-06-02 14:14:51 +08:00
zkitefly c400c7be9d Account migration link update (#1513)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update YggdrasilService.java
2022-06-02 13:04:12 +08:00
zkitefly 7acd9d2491 Update AboutPage.java (#1516)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update AboutPage.java
2022-06-02 13:01:30 +08:00
huanghongxun dc26dba1d0 fix(download): category may be null. 2022-05-24 01:17:29 +08:00
huanghongxun 66cbb23a38 fix(java): broken macos java location. 2022-05-24 01:09:10 +08:00
huanghongxun 663fc176d8 feat(java): find java in minecraft runtime directory with any component name. As a new component name "java-runtime-gamma" was added. 2022-05-24 01:02:16 +08:00
Mingye Wang b9ec22677e chore(javaversion): refactor searchPotentialJavaExecutables (#1485)
* chore(javaversion): refactor searchPotentialJavaExecutables

This is a very draft quality PR. I don't even have JDK installed to build it, that's how draft it is.

1. Refactor function to share common steps of the code: system locations, Minecraft launcher runtimes, PATH/HMCL_JRES
2. Modify Minecraft launcher runtimes to include java-runtime-beta

* Update HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java

* Make optional happy

* correct stream-of paren; add macOS bundle exploration

* who needs exploreBundle?

* fix a stupid typo; add debugging tools. It works!
2022-05-23 23:17:32 +08:00
Tungstend 7d55c7f37f Modrinth获取类别列表,搜索时增加类别筛选 (#1501)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全

* Modrinth源获取类别列表,搜索时增加类别选项

* Update ModrinthRemoteModRepository.java

* Update ModrinthRemoteModRepository.java
2022-05-23 23:03:33 +08:00
huanghongxun f12882a389 fix(download): should update category list after download soruce changed. 2022-05-23 22:27:30 +08:00
huanghongxun 8cfde9d5c6 feat(download): modrinth moddpack category translations. 2022-05-23 21:57:08 +08:00
huanghongxun 10180a74b6 fix: style. 2022-05-22 15:43:08 +08:00
huanghongxun abd30c397d fix(ui): support reveal file in finder, or just open parent directory in Linux. Closes #1458. 2022-05-22 15:40:47 +08:00
huanghongxun a230027974 fix(download): not showing download mod progress. Closes #1456. 2022-05-22 15:31:57 +08:00
Tungstend 59f6837914 修复cf源获取模组版本列表不全 (#1499)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全
2022-05-22 14:34:54 +08:00
huanghongxun 7546af9067 feat(HMCLauncher): update HMCLauncher.exe. Closes #1498. 2022-05-22 14:30:56 +08:00
huanghongxun 1aa3860706 fix(auto-installing): catch logs of forge-installer. Closes #1420. 2022-05-22 14:24:25 +08:00
huanghongxun fd098a65c5 fix(download): not translated some modrinth categories. Closes #1493. 2022-05-22 13:22:40 +08:00
huanghongxun 77f7a56df9 fix(modpack): should accept dragging .mrpack file into rootpage. Closes #1497. 2022-05-22 13:15:20 +08:00
huanghongxun 4fe879085f chore: update gitignore. 2022-05-22 01:45:09 +08:00
huanghongxun 96914ec4f6 feat(launch): open microsoft openjdk download link for Chinese users. 2022-05-22 01:38:30 +08:00
Mingye Wang a1c6b6439c Update HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java 2022-05-22 01:36:20 +08:00
Mingye Wang b91dba3141 fixup! use suggested OS detection 2022-05-22 01:36:20 +08:00
Mingye Wang 718c0a668a fixup! Uh, I think *that's* how you override it. I don't know. Squash this please I don't want to install git qwq 2022-05-22 01:36:20 +08:00
Mingye Wang c62963f455 fix(config): Use Consolas by default on Windows only
Currently non-Windows systems get a proportional font for logging. Bad.

Fix #139.
2022-05-22 01:36:20 +08:00
huanghongxun cc39a37beb feat(download): show modrinth dependencies. 2022-05-22 01:33:27 +08:00
huanghongxun 1bc8450110 fix(skin): littleskin domain changed. Closes #1484. 2022-05-22 01:10:13 +08:00
huanghongxun bec0ca6dea feat: add .editorconfig. Closes #1425. 2022-05-22 00:54:57 +08:00
huanghongxun 7c7296a57d fix(download): show Chinese name of mods in English locale. Closes #1461. 2022-05-22 00:49:26 +08:00
huanghongxun 9684ce4a1b feat(modpack): support installing Modrinth modpack. 2022-05-22 00:39:33 +08:00
huanghongxun c0df55ee62 fix(download): modrinth category forge. 2022-05-21 21:54:41 +08:00
huanghongxun b598e2613e feat(crash): emphasise words that requires user not takking screenshot of game crash window. 2022-05-21 19:02:18 +08:00
huanghongxun 99e578b1bc fix(download): switch to api.curseforge.com. 2022-05-21 19:00:51 +08:00
huanghongxun 3766491c28 feat(download): modpack search chinese words. 2022-05-21 18:58:41 +08:00
huanghongxun 71f13b24cb chore: update mod_data.txt 2022-05-21 18:58:41 +08:00
Mingye Wang 4a3d20576e fix(i18n): mod "nickname" -> filename 2022-05-10 18:04:27 +08:00
zkitefly ecbc243c6b Uptada I18N (#1442)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* updata curse.category.+~

* Update I18N_zh.properties

* updata curse.category.+~

* Update I18N_zh_CN.properties

* 删除。

* 0

* 0

* Update I18N_zh_CN.properties

* 按照 @huanghongxun 的要求“不要去掉句号”

* Update I18N.properties

* Update launch.advice.vanilla_linux_java_8

* 修改错误

* Update I18N_zh_CN.properties
2022-05-08 18:58:46 +08:00
zkitefly c012ca1183 Update MultiplayerPageSkin.java (#1477)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java
2022-05-08 00:06:44 +08:00
AustinC2 820acd90e7 Update README (#1466)
* Update README

* Update README

* Update README

* Update README

* Update README

* Delete ⛏_Hello_Minecraft!_Launcher_💎.png
2022-05-08 00:06:21 +08:00
zkitefly e904a5b42d 修改链接 s.mcer.cn 至 mcer.cn (#1452)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update AboutPage.java

* Update I18N.properties

* Update I18N.properties

* Update I18N_ja.properties

* Update I18N_ja.properties

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java
2022-05-07 18:52:17 +08:00
Haowei Wen cb051c033f Add openjfx to testImplementation dependencies 2022-05-07 18:51:52 +08:00
zkiteflyandYuhui Huang 565fc1f7b6 添加 Glavo 鸣谢 (#1450)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Add files via upload

* Update AboutPage.java

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N.properties

* Update AboutPage.java

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update AboutPage.java

* Update I18N.properties

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-04-09 16:39:24 +08:00
ShellWen | 颉文 f6b8fae903 Update I18N_zh_CN.properties
修改翻译
2022-04-09 16:36:43 +08:00
ShellWen | 颉文 5819af5e7c Update I18N_zh_CN.properties
修复语法问题
2022-04-09 16:36:43 +08:00
vanja-san f4057fb8f8 Update I18N_ru.properties 2022-04-09 16:36:12 +08:00
vanja-san 783a36be84 Update I18N_ru.properties 2022-04-09 16:36:12 +08:00
vanja-san 091d6e00b1 Update I18N_ru.properties 2022-04-09 16:36:12 +08:00
zkitefly d721d00139 Update Metadata.java 2022-04-09 16:34:29 +08:00
zkitefly f7797d4b6f Delete 获得加入Cato社区网络身份凭证的方法.txt 2022-04-09 16:34:29 +08:00
zkitefly f810373227 Create 获得加入Cato社区网络身份凭证的方法.txt 2022-04-09 16:34:29 +08:00
ZekerZhayard c808225f78 Fix #1405 2022-03-20 21:02:19 +08:00
zkitefly 0b06bf82d8 Update FeedbackPage.java 2022-03-20 16:39:47 +08:00
zkitefly 6969107c68 Update I18N_zh_CN.properties 2022-03-20 16:39:47 +08:00
zkitefly 6347de62f0 Update I18N_zh.properties 2022-03-20 16:39:47 +08:00
zkitefly 1f09c122de Update I18N_ru.properties 2022-03-20 16:39:47 +08:00
zkitefly f3de209f99 Update I18N_ja.properties 2022-03-20 16:39:47 +08:00
zkitefly d29c833866 Update I18N.properties 2022-03-20 16:39:47 +08:00
Glavo edac522d06 fix #1411: Set file executable permission after download Java 2022-03-20 16:36:24 +08:00
Glavo 3b1a890423 Filter exported startup parameters 2022-03-20 16:34:42 +08:00
Glavo 4d4864819a Register forbiddenTokens in MicrosoftSession and YggdrasilSession 2022-03-20 16:34:42 +08:00
Glavo 151e7e04eb Add a filter policy to logs 2022-03-20 16:34:42 +08:00
Glavo 03c634658b fix I18N_zh_CN.properties 2022-03-15 20:49:18 +08:00
Glavo 10c54509aa Prohibit login with Mojang account 2022-03-15 20:49:18 +08:00
zkitefly 8c74a559c3 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly d3ae8045c0 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly b9753beeac Update I18N.properties 2022-03-15 20:48:47 +08:00
zkitefly 6a602adc4d Update CreateAccountPane.java 2022-03-15 20:48:47 +08:00
zkitefly c6835ac4f9 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly 7af4332d04 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly af5b5e0f67 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly e6368f5355 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly 1fe9826871 Update CreateAccountPane.java 2022-03-15 20:48:47 +08:00
zkitefly 01ea047521 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly f24a73412a Update CreateAccountPane.java 2022-03-15 20:48:47 +08:00
zkitefly 4def209c62 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly a4a7a1e507 Add files via upload 2022-03-15 20:48:47 +08:00
zkitefly 560725431d Add files via upload 2022-03-15 20:48:47 +08:00
Glavo ffde4987e9 delete css on exit 2022-03-02 16:14:17 +08:00
Glavo 3334371ae3 Show upload skin button for microsoft account
close #1328
2022-03-02 16:13:57 +08:00
Glavo 8b1808fa2e fix #1385: crash when show DatapackListPage
close #1373
2022-03-02 16:13:28 +08:00
CNCloud_Casey 7d86d31844 多人联机界面Cato版本标识优化 (#1353)
* 对Cato版本号问题做出了修改

* test to come back

* 更改了一个因Cato现在实时更新而不合时间的问题

* 学着编译了一下

在gradlew.bat末尾加了一个pause以检查编译结果

* 把pause改掉了
2022-03-01 23:32:13 +08:00
zkiteflyandYuhui Huang 0cbfa62731 Update language (#1388)
* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

修改微软登录的提示
修改noin网站的前缀为https://s.mcer.cn,内容不变,因为网站地址已变更

* Update I18N_zh_CN.properties

添加微软账户添加提示
修改noin.cn为s.mcer.cn,因为链接已变更,但内容不变

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

将https://s.mcer.cn/agreement改成https://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement,可以在链接修改时上游同步即可

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N_ru.properties

* Update I18N_ja.properties

* Update I18N_zh_CN.properties

* Update AboutPage.java

* Accidentally edit license header.

Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2022-03-01 23:31:38 +08:00
Glavo db735b58ec No longer download from tuna mirror 2022-03-01 23:27:51 +08:00
Glavo 17fe11e8f5 Download Zulu by default 2022-03-01 23:27:51 +08:00
Glavo fc65d8a535 Fix the problem that SSL connection cannot be established on Windows 7 2022-03-01 23:27:51 +08:00
Glavo 6ade056bf8 Enable Visual Styles 2022-03-01 23:27:51 +08:00
Glavo a5ebf08a8c Reduce refresh UI controls 2022-03-01 23:27:51 +08:00
Glavo ca2eccc762 Fix compatibility issues with Windows XP 2022-03-01 23:27:51 +08:00
Glavo efd76a1407 Download JRE instead of JDK; Add support for changing download sources 2022-03-01 23:27:51 +08:00
Glavo 48785e93f0 Import error messages 2022-03-01 23:27:51 +08:00
Glavo 20ddf60c48 Make HMCLLauncher find Java in more places 2022-03-01 23:27:51 +08:00
Glavo 1b9b0226f2 Automatically download Java when Java is not found 2022-03-01 23:27:51 +08:00
Glavo a1f8ff7463 Extract and execute the script 'java-download.ps1' 2022-03-01 23:27:51 +08:00
Glavo aafe7f44e7 Replace 'PathAppend' with 'MyPathAppend' 2022-03-01 23:27:51 +08:00
Glavo 8909224a13 Add 'java-download.ps' as win32 resource 2022-03-01 23:27:51 +08:00
Glavo f5d01bef93 git: No longer track file 'HMCL.APS' 2022-03-01 23:27:51 +08:00
Glavo f7de97c844 Fix download dialog title 2022-03-01 23:27:51 +08:00
Glavo 2136fddec5 Update java download window style 2022-03-01 23:27:51 +08:00
Glavo 312152de78 Download and install Java using scripts 2022-03-01 23:27:51 +08:00
Glavo 4fe7d10872 Find Java in HMCL directory 2022-03-01 23:27:51 +08:00
Glavo 9ec10dc5dc Extract 'FindJavaInDirAndLaunchJVM' 2022-03-01 23:27:51 +08:00
Glavo 2672459287 Check Windows version in HMCLLauncher 2022-03-01 23:27:51 +08:00
Glavo 8a8918c049 Try starting with JRE in 'jre-x86' on x64 2022-03-01 23:27:51 +08:00
Glavo 98e0a92745 Get system arch using 'GetNativeSystemInfo' 2022-03-01 23:27:51 +08:00
Glavo 2b21f5f48f Replace 'java.com' with 'www.microsoft.com/openjdk' 2022-03-01 23:27:51 +08:00
ZekerZhayard 69ddc939b3 Fix #645, #1381 2022-03-01 23:16:34 +08:00
Glavo 17ad6362c3 Use Commons Compress to readModpackManifest 2022-03-01 22:48:11 +08:00
Glavo 0f22c9e31a fix #1368: Guess Zip file encoding using Commons Compress 2022-03-01 22:48:11 +08:00
Lonelyxiya 07f9b30ed5 修订繁体中文里面的用语 整合包=模組包
修订繁体中文用语
2022-03-01 22:37:25 +08:00
Glavo 7756becb35 Add an eye-catching version isolation prompt 2022-02-09 15:08:54 +08:00
Gamom cf2ac010bb Update bug-report.yml
修复About过长(???)
2022-02-03 22:15:18 +08:00
zkitefly 76c8e98c02 Update I18N_zh.properties 2022-02-03 22:14:36 +08:00
zkitefly fe66b44b10 Update I18N_zh.properties 2022-02-03 22:14:36 +08:00
zkitefly 97695e4b5a Update I18N_zh_CN.properties 2022-02-03 22:14:36 +08:00
Gamom 59653f4030 Update bug-report.yml
添加英文
方便部分玩家反馈。
2022-02-03 18:18:32 +08:00
Glavo 530b632c46 Fix the problem that the suggestion does not prevent starting the game 2022-02-03 18:17:27 +08:00
Glavo c8f1a10b8b Analyze the crash caused by incompatibility between modlauncher and Java 2022-02-03 18:17:27 +08:00
Glavo 84570c5024 fix import curse forge modpack without modlist.html 2022-02-03 18:16:48 +08:00
Glavo 624d21c4dc Show launcher version on game crash window 2022-02-03 18:16:08 +08:00
Glavo 8db54d015a When creating symbolic links, delete the existing ones first
fix #1342
2022-02-03 18:15:41 +08:00
kitefly 9204c720a4 Update I18N_zh.properties 2022-02-03 18:15:19 +08:00
kitefly daf81b5d78 Update MultiplayerManager.java
更新一下版本号
2022-01-25 22:01:49 +08:00
Glavo c2c3f55925 Replace 'Launcher.getCurrentJarFiles()' with 'JarUtils.thisJar()' 2022-01-25 22:01:39 +08:00
kitefly ba520f1d90 Update I18N_zh_CN.properties 2022-01-25 22:00:58 +08:00
Glavo 81d1dbdcf0 fix the 'run' task of Gradle 2022-01-25 22:00:05 +08:00
kitefly 23cb2655d1 Update I18N_zh_CN.properties 2022-01-24 16:47:06 +08:00
huanghongxun 9c4f94238d feat: support hyperlinks in message dialog. 2022-01-24 16:29:33 +08:00
huanghongxun d28723916d fix(multiplayer): incorrect cato arguments. 2022-01-24 01:45:23 +08:00
Glavo 65d4c9c8ee Fix build on JDK without JavaFX 2022-01-23 19:47:26 +08:00
Glavo 3a1f7a13a1 Add a new GameJavaVersion 'java-runtime-beta' 2022-01-23 19:47:07 +08:00
Glavo 69d97a508c Add JavaVersionConstraint rules 2022-01-23 19:47:07 +08:00
Glavo e7dc23ae26 fix: Cato exit code is not displayed normally 2022-01-23 19:36:31 +08:00
huanghongxun 4cffe30910 fix: test build 2022-01-23 15:06:22 +08:00
NekoCaffeine 6f0dfba4aa fix updateSkin 2022-01-23 10:01:54 +08:00
NekoCaffeine 6d617216c7 fix #1130 2022-01-23 10:01:54 +08:00
kitefly 13d5e5e6af Update MultiplayerManager.java
更新cato版本;将String[] commands = new String[]{exe.toString(), "-auth.token", StringUtils.isBlank(token) ? "new" : token};改成String[] commands = new String[]{exe.toString(), "-auth.token", token};原因是新版cato已经取消这个now传值了
2022-01-23 09:59:19 +08:00
kitefly c697413554 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 994ae3e2f5 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 69ab292394 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 1985d31a07 Update I18N_zh_CN.properties
根据@huanghongxun 的说法修改
2022-01-23 09:55:42 +08:00
kitefly b813185dc3 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 6daf04c9fc Update I18N_zh_CN.properties
决定删除:<b>登录后请勿将同HMCL目录下的hmcl.json文件开放给他人,否则可能存在文件里的token(不是明文密码)会泄露账号(仅限微软账号)</b>
并添加:若发现账号头像是默认头像、进入游戏看到是默认皮肤且无法进入开启正版认证的服务器,则可能需要重新登录微软账号
2022-01-23 09:55:42 +08:00
kitefly fd49ec4f97 Update I18N_zh_CN.properties
添加
<b>你需要在新打开的网页窗口中按照提示完成登录</b> 设备代码为:<b>%1$s</b>,已拷贝至剪切板中,若无法粘贴,可点击此蓝方框再次拷贝。若网页未能打开,请手动在网页浏览器中打开:%2$s <b>登录后请勿将同HMCL目录下的hmcl.json文件开放给他人,否则可能存在文件里的token(不是明文密码)会泄露账号(仅限微软账号)、

讨论:是否添加?:登录后请勿将同HMCL目录下的hmcl.json文件开放给他人,否则可能存在文件里的token(不是明文密码)会泄露账号(仅限微软账号)
2022-01-23 09:55:42 +08:00
kitefly 84a4921629 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
Glavo b4aeaff783 fix preTouchOpenJFXDependencies 2022-01-23 09:55:03 +08:00
Glavo caf73b89ea Fix #1196: Use OpenJFX 17.0.2 2022-01-23 09:55:03 +08:00
Glavo 1c406557a4 Allow different platforms to use different versions of OpenJFX 2022-01-23 09:55:03 +08:00
Glavo d39eac9512 Replace static fields with instance fields 2022-01-23 09:55:03 +08:00
Glavo 2530d31d58 Clean up build scripts 2022-01-23 09:55:03 +08:00
Glavo c6894a1e95 Modify the path where openjfx is placed 2022-01-23 09:55:03 +08:00
Glavo 05fdea3267 Improve the format of openjfx-dependencies.json 2022-01-23 09:55:03 +08:00
Glavo e0f7d9cd32 Generate 'openjfx-dependencies.json' at build time 2022-01-23 09:55:03 +08:00
asdqp233 91b9fe23cc Update Japanese translation
Update and fix Japanese translation
2022-01-23 09:47:35 +08:00
kitefly 4e4e2907d9 Update I18N_zh_CN.properties
添加一些小提示
multiplayer.exit.timeout.static_token=无法连接多人联机服务,你的凭证可能无法正常工作,你可以使用空凭证再试。\n你可以联系在线管理员询问鉴权服务器是否正常工作,或者耐心等待十分钟。
multiplayer.exit.timeout.dynamic_token=无法连接多人联机服务,你可以在多人联机页面的反馈中反馈问题。\n可能原因:鉴权服务器被人占满,请等待每个半点,鉴权服务器会自动刷新一次。\n没有耐心且有闲钱的可以考虑购买联机凭证,不受鉴权服务器影响。
2022-01-23 09:44:44 +08:00
Glavo b94f54f56d Delete unused submodule 'JSTUN' 2022-01-15 01:48:30 +08:00
Glavo 12932a1d33 Adjust the policy of automatic memory allocation 2022-01-15 01:44:41 +08:00
Glavo 7beef30d20 Set an upper limit of 8G for the suggested memory 2022-01-15 01:44:41 +08:00
Glavo f1e4e3c753 Prevent HMCL from crashing when exiting 2022-01-15 01:43:28 +08:00
Glavo d1ba758c6f fix #1297: export logs without 'Schedulers.io()' 2022-01-15 01:43:11 +08:00
Glavo c0472056ac fix #1287, fix #1284: sort type is null 2022-01-10 19:08:55 +08:00
kitefly 1780c479d5 Update I18N_zh_CN.properties 2022-01-07 21:38:24 +08:00
kitefly 59f8465ece 修复多人联机界面过大的问题
通过修改I18N_zh_CN.properties中的
2022-01-03 14:24:53 +08:00
Glavo 256f72dd08 Fix 'DefaultGameRepository::getVersionJar' 2022-01-02 14:28:32 +08:00
huanghongxun 836c7769f6 fix(skin): when texture file is lost, continue launching game instead of failing. 2022-01-02 01:40:37 +08:00
huanghongxun ebe880bd39 fix(multiplayer): ConcurrentModificationException. Closes #1278. 2022-01-02 01:19:14 +08:00
huanghongxun 10fea79a35 fix: cannot update Forge for Minecraft 1.18 when "patches" lost in game version json. Closes #1262. 2022-01-02 01:08:22 +08:00
huanghongxun 451600448e fix: do not show install failure if it is cancelled by user. 2022-01-02 01:08:22 +08:00
huanghongxun 7fccd6261e fix: not on FX thread. 2022-01-02 01:08:21 +08:00
huanghongxun 2436dab797 fix: load fabric api version list by incorrect game version. Closes #1273. 2022-01-02 01:08:21 +08:00
huanghongxun 56538623a5 fix: sort not work in modrinth. Closes #1274. 2022-01-02 01:08:20 +08:00
YuanYuanOwO 42a667c0ef 修正句子 2022-01-02 00:10:58 +08:00
Glavo 5df95ad0fa Use Gradle Kotlin DSL 2022-01-01 20:59:36 +08:00
Glavo 345598016a Use more hash algorithms to generate checksum files 2021-12-30 23:58:57 +08:00
kitefly 6da38e537b 修改错字
将"室"改成"是"
2021-12-25 14:16:21 +08:00
Glavo e65f664821 Check for illegal characters of the HMCL directory path 2021-12-24 23:02:53 +08:00
Glavo cfa1eedae7 Extract the log4j patch to the HMCL directory 2021-12-24 23:02:53 +08:00
Glavo 130c8d0e89 Add 'javaAgents' to LaunchOptions 2021-12-24 23:02:53 +08:00
Hcat c4a2695b69 Update MultiplayerPage.java
Replace the stun server to solve the problem of slow and inaccurate detection
2021-12-24 17:34:31 +08:00
Glavo 246069cc48 Use ArrayList instead of LinkedList 2021-12-21 22:36:42 +08:00
Glavo 36a7d4a510 Stop syncing to gitee on fork 2021-12-21 22:31:21 +08:00
Glavo 1b4a53f572 Update version string to 3.5 2021-12-21 22:26:02 +08:00
huanghongxun 61a9d80e2c fix: checkstyle. 2021-12-20 01:46:25 +08:00
huanghongxun 31568a5806 fix(download): we can check hash of fabric-api jar. 2021-12-20 01:43:41 +08:00
huanghongxun 9aa73f5a5c fix(download): download fabric-api from curseforge. 2021-12-20 01:22:54 +08:00
Glavo 35943feb08 Use GitHub action sync to gitee 2021-12-20 01:21:00 +08:00
huanghongxun 73de21f936 feat(game): select last renamed game version. Closes #1185. 2021-12-19 20:35:15 +08:00
huanghongxun 4f1af87ba0 fix(download): 1.18 released. 2021-12-19 20:31:05 +08:00
huanghongxun d129bd3e6b fix(multiplayer): allow updating cato at server. 2021-12-19 20:30:03 +08:00
Glavo c2c52e8747 fix: Bootstrap launcher version does not need to be displayed on GameCrashWindow 2021-12-19 19:55:00 +08:00
huanghongxun 7e2ab49f89 fix(about): missing JSTUN. Mentioned in #1203. 2021-12-19 19:50:02 +08:00
huanghongxun 3192d41fe3 fix(download): cannot refresh mod download list. Mentioned in #1203. 2021-12-19 19:47:55 +08:00
huanghongxun ad5b260460 fix(download): cannot refresh game list. Mentioned in #1203. 2021-12-19 19:45:01 +08:00
huanghongxun e57cb0ce51 fix(launch): should not patch offline account skin when generating launch script or stopping the launcher immediately after game launched. 2021-12-19 18:45:26 +08:00
Glavo 9514357391 Temporarily disabled adjust process priority on Windows 2021-12-19 17:59:13 +08:00
Glavo 6fd0a40cc6 Delete redundant exception catches 2021-12-19 17:17:40 +08:00
Glavo 350d63231f Improved the logging message of log4j-patch 2021-12-19 17:17:40 +08:00
Glavo cc28f1d5b5 Fix #1241: Use javaagent patch log4j 2021-12-19 17:17:40 +08:00
Glavo 4b73af9250 update check-update.yml 2021-12-19 01:52:48 +08:00
zhixuan 6e0efe521d Change the wrong translation for ja 2021-12-19 01:18:24 +08:00
huanghongxun 041ab30778 fix(multiplayer): incorrect cato option name. 2021-12-18 19:54:51 +08:00
huanghongxun 678be4b92f feat(multiplayer): upgrade cato to 1.2.0 2021-12-18 17:15:18 +08:00
Glavo 74d759a45b Update I18N text 2021-12-18 17:08:16 +08:00
huanghongxun 295499c17b fix: missing translations. 2021-12-18 16:55:33 +08:00
Glavo 043d1c88c8 Fix #1096: Feedback item cannot be clicked 2021-12-18 16:49:42 +08:00
bling-yshs 55e9b93b1c fix curse category 2021-12-18 16:49:23 +08:00
Glavo aa5e3e9697 Fix #1148: Specify console encoding explicitly 2021-12-18 15:59:24 +08:00
Glavo fbf7ff6ebd Lifting GB2312 to GB18030 2021-12-18 15:59:24 +08:00
Glavo 3a5e8a827c update README 2021-12-18 15:59:24 +08:00
Glavo 6176dfaaf7 Lifting encoding to be compatible with more Unicode characters 2021-12-18 15:59:24 +08:00
Glavo 76c361e9bc Fix #1201: Download version jar if the jar is empty 2021-12-18 15:58:33 +08:00
Glavo ccb8981634 Remove invalid Appender 2021-12-18 15:47:41 +08:00
Glavo 99b55d03c1 Allows user to enable message lookup 2021-12-18 15:47:41 +08:00
Glavo 651fa6e2d1 fix #663: Incorrect judgment of WebView loading status 2021-12-18 15:46:24 +08:00
Glavo 4b258da0a0 Fix code format 2021-12-18 15:45:35 +08:00
Glavo 9a66605616 Disable JndiLookup using log4j-patch 2021-12-18 15:45:35 +08:00
Glavo da13895b72 fix: InputStream not closed 2021-12-18 15:45:35 +08:00
Glavo c657a1d2cc Introducing log4j-patch 2021-12-18 15:45:35 +08:00
Glavo eac8896f3e fix NPE in MultiplayerManager 2021-12-18 15:39:57 +08:00
Gamom 113f4f948b Update I18N_zh_CN.properties
翻译问题
解决 https://github.com/huanghongxun/HMCL/issues/1203#issuecomment-997155654
2021-12-18 15:39:26 +08:00
huanghongxun 1b8dc1f75e fix(mcbbs): NPE when fileApi is not empty. Closes #1197. 2021-12-18 01:40:25 +08:00
huanghongxun 001236e8e6 feat(download): refresh button 2021-12-18 01:40:25 +08:00
Glavo 8025b8e24b Support offline installation of OptiFine 1.17.1 H1 and later 2021-12-18 01:26:03 +08:00
Glavo e48bf012a6 Safely save config 2021-12-18 01:21:45 +08:00
Glavo 94e32694b2 fix: Game should use global settings by default 2021-12-12 16:51:01 +08:00
Glavo cbcf033b15 Delete unnecessary properties 2021-12-11 23:52:03 +08:00
Glavo 7da2671ec5 Use custom log4j2.xml 2021-12-11 23:52:03 +08:00
Glavo b6ea4c7003 Fix the problem that you cannot continue after canceling the prompt 2021-12-11 23:52:03 +08:00
Glavo 49378061bf Always issue security warning 2021-12-11 23:52:03 +08:00
Glavo 38013a9298 Issue security warning for CVE-2021-44228 2021-12-11 23:52:03 +08:00
Glavo 5ffbd16800 Prohibit JNDI remote invoke 2021-12-11 23:52:03 +08:00
Glavo bcb29ef792 Update GitHub workflow configuration 2021-12-11 23:50:37 +08:00
Glavo d20adb20d3 fix: Logs should be encoded in UTF-8 2021-12-11 23:49:29 +08:00
Kylepoops 4f28cdd58f Fix CVE-2021-44228 again
https://github.com/apache/logging-log4j2/pull/614
2021-12-11 23:49:09 +08:00
huanghongxun 4b6b5c2f0e fix(game): fix remote code execution from log4j. 2021-12-10 14:22:16 +08:00
Glavo c31269a358 Disable message pattern lookup by default 2021-12-10 14:19:32 +08:00
Glavo 55092326a4 Use StringWriter instead of ByteArrayOutputStream 2021-12-10 14:18:08 +08:00
Glavo 10dbcf63b7 Adjust the default capability to reduce memory allocation 2021-12-10 14:18:08 +08:00
Glavo 32fad77104 Optimize logging performance 2021-12-10 14:18:08 +08:00
SettingDust 05d2fbbbb9 fix(core): zipper lose the file attributes 2021-12-10 14:17:03 +08:00
ttsdzb 916733a1e8 根据最近的变化对README下载部分稍作修改 2021-12-10 14:16:50 +08:00
huanghongxun 83b4b58cb2 feat(crash): 1.18 requires Java 17. Closes #1186. 2021-11-22 01:54:05 +08:00
huanghongxun 11f53c0e03 feat(crash): show link for jdk downloading. Closes #1186. 2021-11-22 01:15:15 +08:00
huanghongxun 2fa8aee055 fix(multiplayer): incorrect cato checksum. 2021-11-20 18:42:25 +08:00
huanghongxun d2848e12f3 fix: not reloading VersionPage when renaming the game version. Closes #1176. 2021-11-20 18:24:00 +08:00
huanghongxun 36ba840fe8 fix: should close version settings page when deleting related version. Closes #1175. 2021-11-20 18:19:39 +08:00
asdqp233 819d41a75c Update I18N.properties 2021-11-20 01:57:39 +08:00
huanghongxun 3a6c3dc51d fix(multiplayer): update cato. 2021-11-11 22:00:46 +08:00
huanghongxun db721107ae fix(microsoft): remove redundant login steps. 2021-11-09 00:35:52 +08:00
huanghongxun 5b9be471e3 fix(microsoft): should not pass client secret when refreshing token. Closes #1164. 2021-11-06 22:32:08 +08:00
huanghongxun e8f8617412 fix: crash when all profiles removed. Close #1165. 2021-11-06 22:32:07 +08:00
Glavo eb472037db fix: checkstyle 2021-11-03 17:30:27 +08:00
Glavo 81b7afd08d Update issue template 2021-11-02 22:46:41 +08:00
Glavo 7a71ecf177 Fix #1153: On Windows, the 'classpath' environment variable in the context overrides the 'CLASSPATH' 2021-11-02 22:46:13 +08:00
huanghongxun a12e485a27 chore(multiplayer): adjust some translations. 2021-10-31 18:42:27 +08:00
huanghongxun af521b2893 chore: localize SSLHandshakeException. 2021-10-31 18:42:27 +08:00
Glavo aab6f68492 Fix #1147: shadowJar minimize exclude JFoenix 2021-10-31 17:04:11 +08:00
huanghongxun f67c3d5284 fix(launch): cannot launch game in path that contains CJK characters. Closes #1141. 2021-10-30 23:03:49 +08:00
Glavo 46385a8445 Do not delete soft links when exiting HMCL 2021-10-30 23:01:27 +08:00
Glavo 9f617c33f5 Support path containing unicode characters on Linux 2021-10-30 23:01:27 +08:00
Glavo 6aef034880 Avoid generating command lines repeatedly 2021-10-30 23:01:27 +08:00
huanghongxun c4941a01d6 fix(multiplayer): show "failed to test" instead of "testing". 2021-10-30 22:40:44 +08:00
huanghongxun 7380423eee fix(multiplayer): does not show correct kick message. 2021-10-30 22:13:32 +08:00
huanghongxun f35073ffb5 fix(ui): alignment. 2021-10-30 21:05:40 +08:00
huanghongxun f89ea56d86 fix(multiplayer): session name null. 2021-10-30 21:04:53 +08:00
huanghongxun a240b8ca9f fix(skin): Do not load authlib-injector when skin manually set to steve/alex. 2021-10-29 22:52:16 +08:00
huanghongxun 230ec2bc8e feat(update): show update dialog when found update. 2021-10-29 02:51:08 +08:00
huanghongxun a3c2642b93 feat(download): customize name of the mod to be downloaded. 2021-10-29 01:21:48 +08:00
huanghongxun de755d5495 feat(game): more version icon choices. 2021-10-29 01:21:48 +08:00
huanghongxun 08054f3026 feat(countly): initial 2021-10-27 00:21:42 +08:00
Glavo 026239903d Fix IllegalAccessError: tried to access protected method 2021-10-25 17:23:07 +08:00
Glavo a320fe4cc4 Use shadowJar minimize 2021-10-24 17:26:26 +08:00
vanja-san b122d4c5e4 Update I18N_ru.properties
Full text translation
2021-10-24 17:25:40 +08:00
743 changed files with 63986 additions and 22998 deletions

No files matched your search

+1117
View File
File diff suppressed because it is too large. Load diff
-29
View File
@@ -1,29 +0,0 @@
# 公告
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [Discord频道](https://discord.gg/jVvC7HfM6U) [开黑啦频道](https://kaihei.co/Kx7n3t) 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://afdian.net/@huanghongxun) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
# 问题提交
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
* 启动器版本:
* 操作系统:
* Java 版本:
* 错误截图(最好请将整个电脑屏幕的截图发上来):
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
* 问题描述(如何触发问题):
+53
View File
@@ -0,0 +1,53 @@
name: Bug 反馈 | Bug Feedback
description:
反馈一个 HMCL 错误。| Bug Feedback.
title: "[Bug] "
labels: bug
body:
- type: markdown
attributes:
value: |
提交前请确认:
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新快照版本。
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
Before submitting, please confirm:
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher version is **the latest snapshot version**. You can click [here](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) to download the latest snapshot version.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
- type: input
id: platform
attributes:
label: 平台 | Platform
description: 请输入您遇到 BUG 的平台。 Please enter the platform on which you encountered the bug.
placeholder: e.g. Windows 11
validations:
required: true
- type: textarea
id: bug-report
attributes:
label: 问题描述 | Problem Description
description: 请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。 Please describe the problem you met in as much detail as possible. Besides, please describe how to trigger this problem again.
placeholder: |
1. 点击 HMCL 上的某个按钮 | Click a button named ...
2. 向下翻页 | Scroll down
3. ...
validations:
required: true
- type: textarea
id: hmcl-crash-report-or-logs
attributes:
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
description: |
如果您的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
如果您的启动器没有崩溃,请在遇到问题后不要退出启动器,在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
If your launcher crashes, please fill it in (or drag the file in) below.
If your launcher doesn't crash, please DO NOT EXIT your launcher, click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
**Attention: The crash report or the log file is the key to solve the problem. Please update it!**
validations:
required: true
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群 | QQ Groups
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
- name: Discord 频道 | Discord Channel
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
- name: 其他反馈 | Others
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
+22
View File
@@ -0,0 +1,22 @@
name: 新功能 | Feature Request
description: 为 HMCL 提出新功能 | A new feature for HMCL
title: "[Feature] "
labels: enhancement
body:
- type: markdown
attributes:
value: 请确认 Issues 列表无重复的项目。| Please make sure that no duplicated issues has already been delivered.
- type: textarea
id: description
attributes:
label: 描述 | Description
description: 请详细描述你想加入的新功能。| Please describe the new feature detaily.
validations:
required: true
- type: textarea
id: reason
attributes:
label: 原因 | Reason
description: 请描述该功能带来的好处及原因。| Please describe why you want to add the feature into HMCL.
validations:
required: true
+46
View File
@@ -0,0 +1,46 @@
name: Build HMCLauncher
on:
push:
paths:
- 'HMCLauncher/**'
- '.github/workflows/build-launcher.yml'
pull_request:
paths:
- 'HMCLauncher/**'
- '.github/workflows/build-launcher.yml'
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
with:
msbuild-architecture: x86
- name: Build HMCLauncher
run: msbuild /p:Configuration=Release /t:Rebuild /verbosity:detailed .\HMCLauncher\
- name: Copy HMCLauncher to assets
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\src\main\resources\assets\HMCLauncher.exe
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Build with Gradle
run: .\gradlew makeExecutables --no-daemon
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=$("${{ github.sha }}".SubString(0, 7))" >> $env:GITHUB_ENV
- name: Copy HMCLauncher to libs
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\build\libs\HMCLauncher.exe
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: HMCLauncher-${{ env.SHORT_SHA }}
path: HMCL/build/libs/*.exe
+12 -10
View File
@@ -1,23 +1,25 @@
name: Check Style
on: [push, pull_request]
on:
push:
paths:
- '**.java'
pull_request:
paths:
- '**.java'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain
run: ./gradlew checkstyleMain --no-daemon
- name: Check style test
run: ./gradlew checkstyleTest
run: ./gradlew checkstyleTest --no-daemon
+11 -9
View File
@@ -1,21 +1,23 @@
name: Check Translations
on: [push, pull_request]
on:
push:
paths:
- '**.properties'
pull_request:
paths:
- '**.properties'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
java-package: 'jdk+fx'
- name: Check style test
run: ./gradlew checkTranslations
run: ./gradlew checkTranslations --no-daemon
+95 -22
View File
@@ -1,18 +1,19 @@
name: Check Update
on:
push:
branches:
- javafx
workflow_dispatch:
schedule:
- cron: '30 * * * *'
permissions:
contents: write
jobs:
check-update:
if: ${{ github.repository_owner == 'huanghongxun' }}
dev-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
env:
HMCL_BASE_VERSION: '3.4'
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
@@ -20,15 +21,15 @@ jobs:
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_BUILD_NUMBER}" != 3 ]; then exit 1; fi
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
@@ -38,30 +39,102 @@ jobs:
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
prerelease: ture
stable-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
needs: dev-check-update
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
- name: Check for existing tags
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "**This version is a stable version.**" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
+21
View File
@@ -0,0 +1,21 @@
name: Sync to Gitee
on:
push
jobs:
run:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to Gitee
uses: Yikun/hub-mirror-action@v1.4
with:
src: github/HMCL-dev
dst: gitee/huanghongxun
static_list: 'HMCL'
force_update: true
debug: true
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
cache_path: /github/workspace/hub-mirror-cache
+17 -10
View File
@@ -1,6 +1,11 @@
name: Java CI
on: [push, pull_request]
on:
push:
pull_request:
paths-ignore:
- '**.md'
- 'HMCLauncher/**'
jobs:
build:
@@ -8,21 +13,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Set up JDK 11
uses: actions/setup-java@v2
- uses: actions/checkout@v4
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
java-version: 8
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build
run: ./gradlew build --no-daemon
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload Artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
-44
View File
@@ -1,44 +0,0 @@
name: Create Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check and set HMCL version
run: |
export HMCL_VERSION=`echo ${{ github.ref }} | awk '{print substr($0,12)}'`
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
if [ -z `echo $HMCL_VERSION | grep "^[0-9]\.[0-9]\.[0-9]\{3\}\$"` ]; then
exit 1
fi
echo "HMCL_BUILD_NUMBER=`echo $HMCL_VERSION | awk '{print substr($0,5)}'`" >> $GITHUB_ENV
- name: Download artifacts
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
- name: Generate release note
run: |
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
- name: Create release
uses: softprops/action-gh-release@v1
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
+7 -2
View File
@@ -12,25 +12,27 @@ hs_err_pid*
.mine*
/externalgames
NVIDIA
minecraft-exported-crash-info*
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
# idea
.idea
/out/
/HMCL/out/
/HMCLCore/out/
/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
.classpath
.project
.settings
@@ -44,6 +46,9 @@ NVIDIA
# macos
.DS_Store
# vscode
.vscode/
# test
/hmcl.json
/.hmcl.json
-3
View File
@@ -1,3 +0,0 @@
[submodule "JSTUN"]
path = JSTUN
url = https://github.com/huanghongxun/JSTUN
-14
View File
@@ -1,14 +0,0 @@
language: java
jdk:
- oraclejdk8
branches:
only:
- master
before_install:
- chmod +x gradlew
script: "bash ./gradlew clean build --stacktrace"
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- "$HOME/.gradle/caches/"
-313
View File
@@ -1,313 +0,0 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
classpath 'com.guardsquare:proguard-gradle:7.1.0' // The ProGuard Gradle plugin.
}
}
plugins {
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
import java.nio.file.FileSystems
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
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 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.4"
def microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
def versionType = System.getenv("VERSION_TYPE") ?: "nightly"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation rootProject.files("lib/JFoenix.jar")
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
}
def digest(String algorithm, byte[] bytes) {
return MessageDigest.getInstance(algorithm).digest(bytes)
}
def createChecksum(File file) {
def algorithm = "SHA-1"
def suffix = "sha1"
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
}
def attachSignature(File jar) {
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
if (keyLocation == null)
return
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
def signer = Signature.getInstance("SHA512withRSA")
signer.initSign(privatekey)
new ZipFile(jar).withCloseable { zip ->
zip.stream()
.sorted(Comparator.comparing { it.name })
.filter { it.name != "META-INF/hmcl_signature" }
.forEach {
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
}
}
def signature = signer.sign()
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs ->
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature }
}
}
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) }
}
sourceSets {
java11 {
java {
srcDirs = ['src/main/java11']
}
}
}
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
}
shadowJar {
classifier = "shadow"
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': project.version,
'Microsoft-Auth-Id': microsoftAuthId,
'Microsoft-Auth-Secret': microsoftAuthSecret,
'Build-Channel': versionType,
'Class-Path': 'pack200.jar',
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
'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(" ")
}
}
task proguard(type: proguard.gradle.ProGuardTask) {
dependsOn shadowJar
injars shadowJar
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
dontobfuscate
dontoptimize
keep 'class org.jackhuang.** { *; }'
keep 'class com.jfoenix.** { *; }'
keep 'class com.google.gson.** { *; }'
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
keep 'class * extends com.google.gson.TypeAdapter'
keep 'class * implements com.google.gson.TypeAdapterFactory'
keep 'class * implements com.google.gson.JsonSerializer'
keep 'class * implements com.google.gson.JsonDeserializer'
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
System.out.println(new File(".").absolutePath)
dontwarn 'com.nqzero.**'
dontwarn 'org.slf4j.**'
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
dontwarn 'com.sun.javafx.**'
dontwarn 'com.jfoenix.**'
dontwarn 'org.apache.**'
adaptclassstrings
// next block taken verbatim from Proguard's documentation examples:
libraryjars files(configurations.compileClasspath.collect())
keepattributes 'SourceFile,LineNumberTable'
keepattributes '*Annotation*'
keepattributes 'Signature'
var javaHome = System.getProperty('java.home')
// Automatically handle the Java version of this build.
if (System.getProperty('java.version').startsWith('1.')) {
// Before Java 9, the runtime classes were packaged in a single jar file.
libraryjars "${javaHome}/lib/rt.jar"
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
} else {
// As of Java 9, the runtime classes are packaged in modular jmod files.
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
}
}
}
task finalJar(type: Jar) {
dependsOn proguard
manifest = shadowJar.manifest
from { proguard.outJarFiles.collect { zipTree(it)} }
into('/') {
from { shadowJar.outputs.files.collect { zipTree(it) } }
include("META-INF/versions/**")
}
doLast {
repack(jar.archivePath) // see repack()
attachSignature(jar.archivePath)
createChecksum(jar.archivePath)
}
}
jar {
enabled = false
dependsOn finalJar
}
def createExecutable(String suffix, String header) {
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
output.bytes = new File(project.projectDir, header).bytes
output << jar.archivePath.bytes
createChecksum(output)
}
processResources {
ext.convertToBSS = { String resource ->
// exclude resource
doFirst {
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()
javaexec {
classpath = sourceSets.main.compileClasspath
mainClass = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
from "build/compiled-resources"
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) {
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
doLast {
outputPath.newOutputStream().withCloseable { out ->
new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) }
}
createChecksum(outputPath)
}
}
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)
}
task makePackGz(dependsOn: makePack) doLast {
def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz")
new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packGz)
}
task makeExecutables(dependsOn: jar) doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
}
build.dependsOn makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
+252
View File
@@ -0,0 +1,252 @@
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.zip.ZipFile
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
version = "$versionRoot.$buildNumber"
dependencies {
implementation(project(":HMCLCore"))
implementation("libs:JFoenix")
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
fun createChecksum(file: File) {
val algorithms = linkedMapOf(
"MD5" to "md5",
"SHA-1" to "sha1",
"SHA-256" to "sha256",
"SHA-512" to "sha512"
)
algorithms.forEach { (algorithm, ext) ->
File(file.parentFile, "${file.name}.$ext").writeText(
digest(algorithm, file.readBytes()).joinToString(separator = "", postfix = "\n") { "%02x".format(it) }
)
}
}
fun attachSignature(jar: File) {
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY")
if (keyLocation == null) {
logger.warn("Missing signature key")
return
}
val privatekey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(File(keyLocation).readBytes()))
val signer = Signature.getInstance("SHA512withRSA")
signer.initSign(privatekey)
ZipFile(jar).use { zip ->
zip.stream()
.sorted(Comparator.comparing { it.name })
.filter { it.name != "META-INF/hmcl_signature" }
.forEach {
signer.update(digest("SHA-512", it.name.toByteArray()))
signer.update(digest("SHA-512", zip.getInputStream(it).readBytes()))
}
}
val signature = signer.sign()
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), emptyMap<String, Any>()).use { zipfs ->
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).use { it.write(signature) }
}
}
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
}
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
if (JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler.set(javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(11))
})
}
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
}
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
exclude("META-INF/maven/**")
minimize {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("libs:JFoenix:.*"))
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2024 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach"
).joinToString(" ")
)
System.getenv("GITHUB_SHA")?.also {
attributes("GitHub-SHA" to it)
}
}
doLast {
attachSignature(jarPath)
createChecksum(jarPath)
}
}
fun createExecutable(suffix: String, header: String) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + suffix)
output.outputStream().use {
it.write(File(project.projectDir, header).readBytes())
it.write(jarPath.readBytes())
}
createChecksum(output)
}
tasks.processResources {
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
}
val makeExecutables = tasks.create("makeExecutables") {
dependsOn(tasks.jar)
doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
}
}
tasks.build {
dependsOn(makeExecutables)
}
fun parseToolOptions(options: String?): List<String> {
if (options == null)
return listOf()
val builder = StringBuilder()
val result = mutableListOf<String>()
var offset = 0
loop@ while (offset < options.length) {
val ch = options[offset]
if (Character.isWhitespace(ch)) {
if (builder.isNotEmpty()) {
result += builder.toString()
builder.clear()
}
while (offset < options.length && Character.isWhitespace(options[offset])) {
offset++
}
continue@loop
}
if (ch == '\'' || ch == '"') {
offset++
while (offset < options.length) {
val ch2 = options[offset++]
if (ch2 != ch) {
builder.append(ch2)
} else {
continue@loop
}
}
throw GradleException("Unmatched quote in $options")
}
builder.append(ch)
offset++
}
if (builder.isNotEmpty()) {
result += builder.toString()
}
return result
}
tasks.create<JavaExec>("run") {
dependsOn(tasks.jar)
group = "application"
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
jvmArgs(vmOptions)
doFirst {
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero">
␍</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 -0.5 52 56" shape-rendering="crispEdges" xmlns="http://www.w3.org/2000/svg"><path stroke="#38342a" d="M28 0h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m4 0h4M24 5h4m4 0h4M24 6h4m4 0h4M24 7h4m4 0h4M24 8h4m8 0h4M24 9h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-20 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-28 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-36 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8M8 24h4m28 0h4m4 0h4M8 25h4m28 0h4m4 0h4M8 26h4m28 0h4m4 0h4M8 27h4m28 0h4m4 0h4M4 28h4m32 0h8M4 29h4m32 0h8M4 30h4m32 0h8M4 31h4m32 0h8M0 32h4m32 0h4M0 33h4m32 0h4M0 34h4m32 0h4M0 35h4m32 0h4M0 36h4m28 0h4M0 37h4m28 0h4M0 38h4m28 0h4M0 39h4m28 0h4M4 40h4m20 0h4M4 41h4m20 0h4M4 42h4m20 0h4M4 43h4m20 0h4M8 44h4m12 0h4M8 45h4m12 0h4M8 46h4m12 0h4M8 47h4m12 0h4m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-12 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#dbd0b4" d="M28 4h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-28 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-24 1h16m-16 1h16m-16 1h16m-16 1h16M8 28h8m4 0h12M8 29h8m4 0h12M8 30h8m4 0h12M8 31h8m4 0h12M8 32h12m8 0h4M8 33h12m8 0h4M8 34h12m8 0h4M8 35h12m8 0h4m-20 1h12m-12 1h12m-12 1h12m-12 1h12m-8 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#c6bca5" d="M28 8h4m-4 1h4m-4 1h4m-4 1h4m-12 5h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8m-20 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#bcb29c" d="M32 12h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#807a6d" d="M44 24h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#aea694" d="M4 32h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#9a927e" d="M4 36h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

+20 -35
View File
@@ -19,6 +19,7 @@
import json
import codecs
import re
import sys
from urllib.parse import urlparse, parse_qs
S = ';'
@@ -26,18 +27,21 @@ S = ';'
MOD_SEPARATOR = ','
CURSEFORGE_PATTERN1 = re.compile(
r'^/minecraft/(mc-mods|customization|mc-addons|customization/configuration)/(?P<modid>[\w-]+)(/(.*?))?$')
r'^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons)/+(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN2 = re.compile(
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN3 = re.compile(
r'^/mc-mods/minecraft/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN4 = re.compile(
r'^/legacy/mc-mods/minecraft/(\d+)-(?P<modid>[\w-]+)'
)
def parseCurseforge(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3]:
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3, CURSEFORGE_PATTERN4]:
match = pattern.match(res.path)
if match:
return match.group('modid')
@@ -45,7 +49,7 @@ def parseCurseforge(url):
MCMOD_PATTERN = re.compile(
r'^http(s)?://www\.mcmod\.cn/class/(?P<modid>\d+)\.html$')
r'^http(s)?://www\.mcmod\.cn/(class|modpack)/(?P<modid>\d+)\.html$')
def parseMcmod(url):
@@ -54,32 +58,16 @@ def parseMcmod(url):
return match.group('modid')
return ''
MCBBS_HTML_PATTERN = re.compile(r'/thread-(?P<modid>\d+)-(\d+)-(\d+).html')
MCBBS_PHP_PATTERN = re.compile(r'')
def parseMcbbs(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
if res.netloc != 'www.mcbbs.net':
return ''
match = MCBBS_HTML_PATTERN.match(res.path)
if match:
return match.group('modid')
query = parse_qs(res.query)
if res.path == '/forum.php':
if 'mod' in query and 'tid' in query and query['mod'] == ['viewthread']:
return query['tid']
return ''
skip = [
'Minecraft',
'The Building Game'
]
if __name__ == '__main__':
with codecs.open('data.json', mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
json_name = sys.argv[1] or 'data.json'
with codecs.open(json_name, mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
data = json.load(jsonfile)
for mod in data:
@@ -87,6 +75,9 @@ if __name__ == '__main__':
sub_name = mod['name']['sub']
abbr = mod['name']['abbr']
if sub_name in skip:
continue
if S in chinese_name:
print('Error! ' + chinese_name)
exit(1)
@@ -96,12 +87,11 @@ if __name__ == '__main__':
exit(1)
black_lists = [
'Master Chef'
'Wood Converter'
]
curseforge_id = ''
mcmod_id = ''
mcbbs_id = ''
links = mod['links']['list']
if 'curseforge' in links and links['curseforge'] and sub_name not in black_lists and chinese_name not in black_lists:
for link in links['curseforge']:
@@ -116,14 +106,9 @@ if __name__ == '__main__':
if mcmod_id == '':
print('Error mcmod ' + chinese_name)
exit(1)
if 'mcbbs' in links and links['mcbbs']:
mcbbs_id = parseMcbbs(links['mcbbs'][0]['url'])
if mcbbs_id == '':
print('Error mcbbs ' + chinese_name)
exit(1)
mod_id = []
if 'list' in mod['modid']:
if 'modid' in mod and 'list' in mod['modid']:
for id in mod['modid']['list']:
if MOD_SEPARATOR in id:
print('Error mod id!' + id)
@@ -133,6 +118,6 @@ if __name__ == '__main__':
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
outfile.write(
f'{curseforge_id}{S}{mcmod_id}{S}{mcbbs_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
f'{curseforge_id}{S}{mcmod_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
print('Success!')
@@ -19,36 +19,41 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.task.Schedulers;
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.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.*;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
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.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
@@ -60,14 +65,43 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
try {
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
} catch (Throwable e) {
LOG.warning("Failed to get prism pipeline", e);
}
try {
try {
ConfigHolder.init();
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", e);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
}
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
} else {
checkConfigInTempDir();
}
if (ConfigHolder.isOwnerChanged()) {
if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
}
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -77,8 +111,6 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
initIcon();
UpdateChecker.init();
primaryStage.show();
@@ -88,20 +120,100 @@ public final class Launcher extends Application {
}
}
@Override
public void stop() throws Exception {
super.stop();
Controllers.onApplicationStop();
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
private void initIcon() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
AwtUtils.setAppleIcon(image);
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
String tmpdir = System.getProperty("java.io.tmpdir");
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
return true;
String[] tempFolderNames = {"Temp", "Cache", "Caches"};
for (String name : tempFolderNames) {
if (configPath.contains(File.separator + name + File.separator))
return true;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
return configPath.contains("\\Temporary Internet Files\\")
|| configPath.contains("\\INetCache\\")
|| configPath.contains("\\$Recycle.Bin\\")
|| configPath.contains("\\recycler\\");
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
return configPath.startsWith("/tmp/")
|| configPath.startsWith("/var/tmp/")
|| configPath.startsWith("/var/cache/")
|| configPath.startsWith("/dev/shm/")
|| configPath.contains("/Trash/");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
return configPath.startsWith("/var/folders/")
|| configPath.startsWith("/private/var/folders/")
|| configPath.startsWith("/tmp/")
|| configPath.startsWith("/private/tmp/")
|| configPath.startsWith("/var/tmp/")
|| configPath.startsWith("/private/var/tmp/")
|| configPath.contains("/.Trash/");
} else {
return false;
}
}
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
Main.exit(0);
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
String owner;
try {
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
} catch (IOException ioe) {
LOG.warning("Failed to get file owner", ioe);
return;
}
if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_DIRECTORY))
files.add(Metadata.HMCL_DIRECTORY.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
if (showAlert(AlertType.ERROR,
i18n("fatal.config_loading_failure.unix", owner, command),
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
Main.exit(1);
}
@Override
public void stop() throws Exception {
Controllers.onApplicationStop();
LOG.shutdown();
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
LOG.shutdown();
return;
}
@@ -110,17 +222,29 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("Current Directory: " + System.getProperty("user.dir"));
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
.filter(bean -> bean.getName().equals("Metaspace"))
.findAny()
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
.orElse("Unknown"));
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
}
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -154,34 +278,5 @@ public final class Launcher extends Application {
});
}
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
for (URL u : urls)
try {
File f = new File(u.toURI());
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
result.add(f);
} 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;
else
return result;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
+67 -32
View File
@@ -17,13 +17,17 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.Logging;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -37,10 +41,9 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
@@ -49,35 +52,35 @@ public final class Main {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
// Use System look and feel
initLookAndFeel();
LOG.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
checkDirectoryPath();
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
initIcon();
checkJavaFX();
verifyJavaFX();
Launcher.main(args);
}
private static void initLookAndFeel() {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
public static void exit(int exitCode) {
LOG.shutdown();
System.exit(exitCode);
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
@@ -93,14 +96,27 @@ public final class Main {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
System.exit(0);
LOG.error("User cancels downloading JavaFX", e);
exit(0);
}
}
/**
* Check if JavaFX exists but is incomplete
*/
private static void verifyJavaFX() {
try {
Class.forName("javafx.beans.binding.Binding"); // javafx.base
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -110,8 +126,17 @@ public final class Main {
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
exit(1);
}
/**
@@ -120,10 +145,19 @@ public final class Main {
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
SwingUtils.showWarningDialog(message);
}
static void fixLetsEncrypt() {
private static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
@@ -150,8 +184,9 @@ public final class Main {
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -17,8 +17,8 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Path;
@@ -32,34 +32,40 @@ public final class Metadata {
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
public static final String HELP_URL = "https://docs.hmcl.net";
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.html";
public static final String BUILD_CHANNEL = JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Build-Channel")).orElse("nightly");
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
public static final Path HMCL_DIRECTORY;
private static Path getHMCLDirectory() {
String home = System.getProperty("user.home", ".");
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// to fulfill XDG standard.
String xdgCache = System.getenv("XDG_CACHE_HOME");
if (StringUtils.isNotBlank(xdgCache)) {
return Paths.get(xdgCache, "hmcl");
static {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
} else {
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
}
} else {
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
return Paths.get(home, ".cache", "hmcl");
} else {
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
return OperatingSystem.getWorkingDirectory("hmcl");
}
public static boolean isStable() {
@@ -0,0 +1,54 @@
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CrashReport {
private final Thread thread;
private final Throwable throwable;
private final String stackTrace;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
}
public Throwable getThrowable() {
return this.throwable;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
if (throwable instanceof VirtualMachineError)
return false;
return true;
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + "\n" +
" Thread: " + thread + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
}
@@ -21,16 +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 org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
@@ -59,15 +59,60 @@ public final class HMCLGameLauncher extends DefaultLauncher {
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
if (optionsFile.exists())
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
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);
}
if (configFolder.isDirectory()) {
if (findFiles(configFolder, "options.txt")) {
return;
}
}
if (!I18n.isUseChinese()) {
return;
}
String lang;
/*
1.0- :没有语言选项,遇到这些版本时不设置
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
*/
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
if (gameVersion.compareTo("1.1") < 0) {
lang = null;
} else if (gameVersion.compareTo("1.11") < 0) {
lang = "zh_CN";
} else {
lang = "zh_cn";
}
if (lang != null) {
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
}
}
private boolean findFiles(File folder, String fileName) {
File[] fs = folder.listFiles();
if (fs != null) {
for (File f : fs) {
if (f.isDirectory())
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
return true;
if (f.getName().equals(fileName))
return true;
}
}
return false;
}
@Override
@@ -20,36 +20,42 @@ 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.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.mod.ModAdviser;
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.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -58,6 +64,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
@@ -95,7 +103,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
public Stream<Version> getDisplayVersions() {
return getVersions().stream()
.filter(v -> !v.isHidden())
.sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime())
.sorted(Comparator.comparing((Version v) -> Lang.requireNonNullElse(v.getReleaseTime(), Instant.EPOCH))
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
}
@@ -115,10 +123,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
} catch (IOException ex) {
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
// https://github.com/huanghongxun/HMCL/issues/938
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
}
@@ -157,7 +165,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
Files.move(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
@@ -168,17 +176,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // HMCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
@@ -231,6 +229,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
@@ -240,6 +239,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
@Nullable
public VersionSetting getLocalVersionSettingOrCreate(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) {
vs = createLocalVersionSetting(id);
}
return vs;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
@@ -250,22 +258,92 @@ public class HMCLGameRepository extends DefaultGameRepository {
return vs;
}
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
public Optional<File> getVersionIconFile(String id) {
File root = getVersionRoot(id);
File iconFile = new File(root, "icon.png");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
iconFile = new File(root, "icon.jpg");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
iconFile = new File(root, "icon.bmp");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
iconFile = new File(root, "icon.gif");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
return Optional.empty();
}
public void setVersionIconFile(String id, File iconFile) throws IOException {
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
if (!ext.equals("png") && !ext.equals("jpg") && !ext.equals("bmp") && !ext.equals("gif")) {
throw new IllegalArgumentException("Unsupported icon file: " + ext);
}
deleteIconFile(id);
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
}
public void deleteIconFile(String id) {
File root = getVersionRoot(id);
new File(root, "icon.png").delete();
new File(root, "icon.jpg").delete();
new File(root, "icon.bmp").delete();
new File(root, "icon.gif").delete();
}
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return newImage("/assets/img/grass.png");
return VersionIconType.DEFAULT.getIcon();
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
VersionSetting vs = getLocalVersionSettingOrCreate(id);
VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT;
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
Optional<File> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try (InputStream inputStream = new FileInputStream(iconFile.get())) {
return new Image(inputStream);
} catch (IOException e) {
LOG.warning("Failed to load version icon of " + id, e);
}
}
if (LibraryAnalyzer.isModded(this, version)) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
return VersionIconType.FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
return VersionIconType.FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
return VersionIconType.QUILT.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
return VersionIconType.OPTIFINE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
return VersionIconType.CHICKEN.getIcon();
else
return VersionIconType.FURNACE.getIcon();
}
return VersionIconType.DEFAULT.getIcon();
} else {
return iconType.getIcon();
}
}
public boolean saveVersionSetting(String id) {
@@ -279,7 +357,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
LOG.error("Unable to save version setting of " + id, e);
return false;
}
}
@@ -305,7 +383,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -315,14 +393,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory((int)(getAllocatedMemory(
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setEnvironmentVariables(
Lang.mapOf(StringUtils.tokenize(vs.getEnvironmentVariables())
.stream()
.map(it -> {
int idx = it.indexOf('=');
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
})
.collect(Collectors.toList())
)
)
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
@@ -334,8 +422,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setRenderer(vs.getRenderer())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL());
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
.setJavaAgents(javaAgents);
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
@@ -349,16 +440,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
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);
}
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
} catch (IOException | JsonParseException e) {
e.printStackTrace();
}
}
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
builder.setMaxMemory(null);
return builder.create();
}
@@ -405,7 +496,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
return false;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase()))
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return OperatingSystem.isNameValid(id);
@@ -429,6 +520,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
return auto ? Math.max(minimum, (long) (available * 0.8)) : minimum;
if (auto) {
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024;
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16384L * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
}
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
@@ -33,7 +32,8 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class HMCLModpackInstallTask extends Task<Void> {
@@ -42,8 +42,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final Modpack modpack;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
@@ -66,16 +66,15 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
if (!MODPACK_TYPE.equals(config.getType()))
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
}
} 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"));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
@@ -92,7 +91,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null);
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
// reinstall libraries
// libraries of Forge and OptiFine should be obtained by installation.
@@ -104,6 +103,4 @@ public final class HMCLModpackInstallTask extends Task<Void> {
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
public static final String MODPACK_TYPE = "HMCL";
}
@@ -17,8 +17,16 @@
*/
package org.jackhuang.hmcl.game;
public final class HMCLModpackManifest {
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
public final class HMCLModpackManifest implements ModpackManifest {
public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
private HMCLModpackManifest() {}
@Override
public ModpackProvider getProvider() {
return HMCLModpackProvider.INSTANCE;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2022 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,8 +18,13 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -30,26 +35,39 @@ import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
/**
* @author huangyuhui
*/
public final class HMCLModpackManager {
private HMCLModpackManager() {
public final class HMCLModpackProvider implements ModpackProvider {
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
@Override
public String getName() {
return "HMCL";
}
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return null;
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
}
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
Profile profile = repository.getProfile();
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
}
@Override
public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
@@ -67,4 +85,5 @@ public final class HMCLModpackManager {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,38 +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 static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public enum LoadingState {
DEPENDENCIES("launch.state.dependencies"),
MODS("launch.state.modpack"),
LOGGING_IN("launch.state.logging_in"),
LAUNCHING("launch.state.waiting_launching"),
DONE("launch.state.done");
private final String key;
LoadingState(String key) {
this.key = key;
}
public String getLocalizedMessage() {
return i18n(key);
}
}
@@ -0,0 +1,130 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.ui.versions.ModTranslations;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
private static final int CONTAIN_CHINESE_WEIGHT = 10;
private static final int INITIAL_CAPACITY = 16;
protected abstract RemoteModRepository getBackedRemoteModRepository();
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
int count = 0;
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) {
if (englishSearchFiltersSet.contains(englishWord)) {
continue;
}
englishSearchFiltersSet.add(englishWord);
}
count++;
if (count >= 3) break;
}
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
{
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
if (chineseIndex > englishIndex) {
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
continue;
}
RemoteMod remoteMod = iterator.next();
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
searchResultArray[chineseIndex++] = remoteMod;
} else {
searchResultArray[englishIndex--] = remoteMod;
}
}
totalPages = searchResult.getTotalPages();
}
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
return Pair.pair(remoteMod, Integer.MAX_VALUE);
}
String chineseRemoteModName = chineseRemoteMod.getName();
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
}
int l = levCalculator.calc(searchFilter, chineseRemoteModName);
for (int i = 0; i < searchFilter.length(); i++) {
if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) {
l -= CONTAIN_CHINESE_WEIGHT;
}
}
return Pair.pair(remoteMod, l);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
}
@Override
public Stream<Category> getCategories() throws IOException {
return getBackedRemoteModRepository().getCategories();
}
@Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
}
@Override
public RemoteMod getModById(String id) throws IOException {
return getBackedRemoteModRepository().getModById(id);
}
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
return getBackedRemoteModRepository().getModFile(modId, fileId);
}
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
}
@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.util.Log4jLevel;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Log {
public static final int DEFAULT_LOG_LINES = 2000;
public static int getLogLines() {
Integer lines = config().getLogLines();
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
}
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
}
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
public String getLog() {
return log;
}
public Log4jLevel getLevel() {
Log4jLevel level = this.level;
if (level == null) {
level = Log4jLevel.guessLevel(log);
if (level == null)
level = Log4jLevel.INFO;
this.level = level;
}
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
}
}
@@ -17,21 +17,27 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class LogExporter {
private LogExporter() {
}
@@ -58,15 +64,14 @@ public final class LogExporter {
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
}
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
}
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
processLogs(runDirectory, "*.log", "runDirectory", zipper);
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
zipper.putTextFile(launchScript, OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
for (String id : versions) {
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
@@ -77,6 +82,28 @@ public final class LogExporter {
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, Schedulers.io());
});
}
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
for (Path file : stream) {
if (Files.isRegularFile(file)) {
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
if (time.toMillis() >= processStartTime) {
try {
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
zipper.putTextFile(crashLog, file.getFileName().toString());
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
}
}
}
}
} catch (Throwable e) {
LOG.warning("Failed to find any log on " + logDirectory, e);
}
}
}
@@ -18,17 +18,16 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipFile;
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.curse.CurseModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.mod.server.ServerModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
@@ -41,6 +40,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
@@ -51,44 +51,54 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.toIterable;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModpackHelper {
private ModpackHelper() {}
private static final Map<String, ModpackProvider> providers = mapOf(
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
pair(McbbsModpackProvider.INSTANCE.getName(), McbbsModpackProvider.INSTANCE),
pair(ModrinthModpackProvider.INSTANCE.getName(), ModrinthModpackProvider.INSTANCE),
pair(MultiMCModpackProvider.INSTANCE.getName(), MultiMCModpackProvider.INSTANCE),
pair(ServerModpackProvider.INSTANCE.getName(), ServerModpackProvider.INSTANCE),
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
);
@Nullable
public static ModpackProvider getProviderByType(String type) {
return providers.get(type);
}
public static boolean isFileModpackByExtension(File file) {
String ext = FileUtils.getExtension(file);
return "zip".equals(ext) || "mrpack".equals(ext);
}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
// ignore it, not a valid MCBBS modpack.
}
try {
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
// Order for trying detecting manifest is necessary here.
// Do not change to iterating providers.
for (ModpackProvider provider : new ModpackProvider[]{
McbbsModpackProvider.INSTANCE,
CurseModpackProvider.INSTANCE,
ModrinthModpackProvider.INSTANCE,
HMCLModpackProvider.INSTANCE,
MultiMCModpackProvider.INSTANCE,
ServerModpackProvider.INSTANCE}) {
try {
return provider.readManifest(zipFile, file, charset);
} catch (Exception ignored) {
}
}
} catch (IOException ignored) {
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
@@ -130,24 +140,12 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
}
private static String getManifestType(Object manifest) throws UnsupportedModpackException {
if (manifest instanceof HMCLModpackManifest)
return HMCLModpackInstallTask.MODPACK_TYPE;
else if (manifest instanceof MultiMCInstanceConfiguration)
return MultiMCModpackInstallTask.MODPACK_TYPE;
else if (manifest instanceof CurseManifest)
return CurseInstallTask.MODPACK_TYPE;
else
throw new UnsupportedModpackException();
}
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
@@ -161,7 +159,7 @@ public final class ModpackHelper {
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
@@ -203,7 +201,7 @@ public final class ModpackHelper {
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
@@ -232,38 +230,13 @@ public final class ModpackHelper {
}
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
switch (configuration.getType()) {
case CurseInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof CurseManifest))
throw new MismatchedModpackTypeException(CurseInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new CurseInstallTask(profile.getDependency(), zipFile, modpack, (CurseManifest) modpack.getManifest(), name));
case MultiMCModpackInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration))
throw new MismatchedModpackTypeException(MultiMCModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
case HMCLModpackInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
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();
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
@@ -23,22 +23,20 @@ import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@@ -81,7 +79,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
try {
session.parseBody(files);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read post data", e);
LOG.warning("Failed to read post data", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
@@ -98,16 +96,16 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
Logging.LOG.warning("Error: " + parameters);
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
}
thread(() -> {
@@ -115,7 +113,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
Thread.sleep(1000);
stop();
} catch (InterruptedException e) {
Logging.LOG.log(Level.SEVERE, "Failed to sleep for 1 second");
LOG.error("Failed to sleep for 1 second");
}
});
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
@@ -160,15 +158,19 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse(""));
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
@@ -17,42 +17,47 @@
*/
package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
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.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author yushijinhun
@@ -64,15 +69,15 @@ public final class TexturesLoader {
// ==== Texture Loading ====
public static class LoadedTexture {
private final BufferedImage image;
private final Image image;
private final Map<String, String> metadata;
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
public LoadedTexture(Image image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public BufferedImage getImage() {
public Image getImage() {
return image;
}
@@ -96,7 +101,7 @@ public final class TexturesLoader {
return TEXTURES_DIR.resolve(prefix).resolve(hash);
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
@@ -110,19 +115,20 @@ public final class TexturesLoader {
} catch (Exception e) {
if (Files.isRegularFile(file)) {
// concurrency conflict?
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
LOG.warning("Failed to download texture " + texture.getUrl() + ", but the file is available", e);
} else {
throw new IOException("Failed to download texture " + texture.getUrl());
}
}
}
BufferedImage img;
Image img;
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
img = new Image(in);
}
if (img == null)
throw new IOException("Texture is malformed");
if (img.isError())
throw img.getException();
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
@@ -133,34 +139,42 @@ public final class TexturesLoader {
// ====
// ==== Skins ====
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
private static final String[] DEFAULT_SKINS = {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"};
static {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
public static Image getDefaultSkinImage() {
return FXUtils.newBuiltinImage("/assets/img/skin/wide/steve.png");
}
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 (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
public static LoadedTexture getDefaultSkin(UUID uuid) {
int idx = Math.floorMod(uuid.hashCode(), DEFAULT_SKINS.length * 2);
TextureModel model;
Image skin;
if (idx < DEFAULT_SKINS.length) {
model = TextureModel.SLIM;
skin = FXUtils.newBuiltinImage("/assets/img/skin/slim/" + DEFAULT_SKINS[idx] + ".png");
} else {
model = TextureModel.WIDE;
skin = FXUtils.newBuiltinImage("/assets/img/skin/wide/" + DEFAULT_SKINS[idx - DEFAULT_SKINS.length] + ".png");
}
return new LoadedTexture(skin, singletonMap("model", model.modelName));
}
public static LoadedTexture getDefaultSkin(TextureModel model) {
return DEFAULT_SKINS.get(model);
public static TextureModel getDefaultModel(UUID uuid) {
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
? TextureModel.WIDE
: TextureModel.SLIM;
}
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
LoadedTexture uuidFallback = getDefaultSkin(uuid);
return BindingMapping.of(service.getProfileRepository().binding(uuid))
.map(profile -> profile
.flatMap(it -> {
try {
return YggdrasilService.getTextures(it);
} catch (ServerResponseMalformedException e) {
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
LOG.warning("Failed to parse texture payload", e);
return Optional.empty();
}
})
@@ -172,8 +186,8 @@ public final class TexturesLoader {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
} catch (Throwable e) {
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
@@ -183,64 +197,177 @@ 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;
public static ObservableValue<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(account.getUUID());
if (account instanceof OfflineAccount) {
OfflineAccount offlineAccount = (OfflineAccount) account;
SimpleObjectProperty<LoadedTexture> binding = new SimpleObjectProperty<>();
InvalidationListener listener = o -> {
Skin skin = offlineAccount.getSkin();
String username = offlineAccount.getUsername();
binding.set(uuidFallback);
if (skin != null) {
skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.warning("Failed to load texture", exception);
} else if (result != null && result.getSkin() != null && result.getSkin().getImage() != null) {
Map<String, String> metadata;
if (result.getModel() != null) {
metadata = singletonMap("model", result.getModel().modelName);
} else {
metadata = emptyMap();
}
}, POOL);
} else {
binding.set(new LoadedTexture(result.getSkin().getImage(), metadata));
}
}).start();
}
};
listener.invalidated(offlineAccount);
binding.addListener(new Holder<>(listener));
offlineAccount.addListener(new WeakInvalidationListener(listener));
return binding;
} else {
return BindingMapping.of(account.getTextures())
.asyncMap(textures -> {
if (textures.isPresent()) {
Texture texture = textures.get().get(TextureType.SKIN);
if (texture != null && StringUtils.isNotBlank(texture.getUrl())) {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (Throwable e) {
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
}
}
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}, uuidFallback);
}
}
// ====
// ==== Avatar ====
public static BufferedImage toAvatar(BufferedImage skin, int size) {
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = avatar.createGraphics();
public static void drawAvatar(Canvas canvas, Image skin) {
GraphicsContext g = canvas.getGraphicsContext2D();
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
int scale = skin.getWidth() / 64;
int size = (int) canvas.getWidth();
int scale = (int) skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
g.drawImage(skin,
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
g.drawImage(skin,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
g.dispose();
return avatar;
try {
g.setImageSmoothing(false);
drawAvatar(g, skin, size, scale, faceOffset);
} catch (NoSuchMethodError ignored) {
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
// In order to prevent the blurring caused by bilinear interpolation,
// we use the self-implemented nearest neighbor interpolation to scale
drawAvatarSlow(g, skin, size, scale, faceOffset);
}
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(FXUtils::toFXImage);
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
g.drawImage(skin,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
g.drawImage(skin,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(FXUtils::toFXImage);
} else {
return Bindings.createObjectBinding(
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
PixelReader reader = skin.getPixelReader();
PixelWriter writer = g.getPixelWriter();
drawImage(writer, reader,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
drawImage(writer, reader,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
/*
* Scale and draw image using nearest-neighbor interpolation
*/
private static void drawImage(PixelWriter writer, PixelReader reader,
int sx, int sy, int sw, int sh,
int dx, int dy, int dw, int dh) {
double xScale = ((double) sw) / dw;
double yScale = ((double) sh) / dh;
for (int xOffset = 0; xOffset < dw; xOffset++) {
for (int yOffset = 0; yOffset < dh; yOffset++) {
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
// Draw only non-transparent pixels
if ((color >>> 24) != 0)
writer.setArgb(dx + xOffset, dy + yOffset, color);
}
}
}
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
final WeakReference<Canvas> canvasRef;
final ObservableValue<LoadedTexture> binding;
SkinBindingChangeListener(Canvas canvas, ObservableValue<LoadedTexture> binding) {
this.canvasRef = new WeakReference<>(canvas);
this.binding = binding;
}
@Override
public void changed(ObservableValue<? extends LoadedTexture> observable,
LoadedTexture oldValue, LoadedTexture loadedTexture) {
Canvas canvas = canvasRef.get();
if (canvas != null)
drawAvatar(canvas, loadedTexture.image);
}
}
public static void fxAvatarBinding(Canvas canvas, ObservableValue<LoadedTexture> skinBinding) {
synchronized (SkinBindingChangeListener.hole) {
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
if (oldListener != null)
oldListener.binding.removeListener(oldListener);
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
listener.changed(skinBinding, null, skinBinding.getValue());
skinBinding.addListener(listener);
SkinBindingChangeListener.hole.put(canvas, listener);
}
}
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
fxAvatarBinding(canvas, skinBinding(service, uuid));
}
public static void bindAvatar(Canvas canvas, Account account) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof OfflineAccount)
fxAvatarBinding(canvas, skinBinding(account));
else {
unbindAvatar(canvas);
drawAvatar(canvas, getDefaultSkin(account.getUUID()).image);
}
}
public static void unbindAvatar(Canvas canvas) {
synchronized (SkinBindingChangeListener.hole) {
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
if (oldListener != null)
oldListener.binding.removeListener(oldListener);
}
}
// ====
@@ -0,0 +1,221 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaRemoteFiles;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HMCLJavaRepository implements JavaRepository {
public static final String MOJANG_JAVA_PREFIX = "mojang-";
private final Path root;
public HMCLJavaRepository(Path root) {
this.root = root;
}
public Path getPlatformRoot(Platform platform) {
return root.resolve(platform.toString());
}
@Override
public Path getJavaDir(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name);
}
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Path getManifestFile(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name + ".json");
}
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public boolean isInstalled(Platform platform, String name) {
return Files.exists(getManifestFile(platform, name));
}
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public @Nullable Path getJavaExecutable(Platform platform, String name) {
Path javaDir = getJavaDir(platform, name);
try {
return JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException ignored) {
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
try {
return JavaManager.getMacExecutable(javaDir).toRealPath();
} catch (IOException ignored1) {
}
}
}
return null;
}
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path root = getPlatformRoot(platform);
if (!Files.isDirectory(root))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
for (Path file : stream) {
try {
String name = file.getFileName().toString();
if (name.endsWith(".json") && Files.isRegularFile(file)) {
Path javaDir = file.resolveSibling(name.substring(0, name.length() - ".json".length()));
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), true));
}
}
} catch (Throwable e) {
LOG.warning("Failed to parse " + file, e);
}
}
} catch (IOException ignored) {
}
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfo.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.getComponent());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
files.put(path, new JavaLocalFiles.LocalFile(downloadInfo.getSha1(), downloadInfo.getSize()));
}
} else if (file instanceof MojangJavaRemoteFiles.RemoteDirectory) {
files.put(path, new JavaLocalFiles.LocalDirectory());
} else if (file instanceof MojangJavaRemoteFiles.RemoteLink) {
files.put(path, new JavaLocalFiles.LocalLink(((MojangJavaRemoteFiles.RemoteLink) file).getTarget()));
}
});
JavaManifest manifest = new JavaManifest(info, update, files);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
Path javaDir = getJavaDir(platform, name);
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
if (!result.getInfo().getPlatform().equals(platform))
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@Override
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
return Task.runAsync(() -> {
Files.deleteIfExists(getManifestFile(platform, name));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
});
}
@Override
public Task<Void> getUninstallJavaTask(JavaRuntime java) {
return Task.runAsync(() -> {
Path root = getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
String name = relativized.getName(0).toString();
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
}
});
}
}
@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Glavo
*/
public final class JavaInstallTask extends Task<JavaManifest> {
private final Path targetDir;
private final Map<String, Object> update;
private final Path archiveFile;
private final Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
private final ArrayList<String> nameStack = new ArrayList<>();
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
private final MessageDigest messageDigest = DigestUtils.getDigest("SHA-1");
public JavaInstallTask(Path targetDir, Map<String, Object> update, Path archiveFile) {
this.targetDir = targetDir;
this.update = update;
this.archiveFile = archiveFile;
}
@Override
public void execute() throws Exception {
JavaInfo info;
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(archiveFile)) {
info = JavaInfo.fromArchive(tree);
copyDirContent(tree, targetDir);
}
setResult(new JavaManifest(info, update, files));
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, Path targetDir) throws IOException {
copyDirContent(tree, tree.getRoot().getSubDirs().values().iterator().next(), targetDir);
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, ArchiveFileTree.Dir<E> dir, Path targetDir) throws IOException {
Files.createDirectories(targetDir);
for (Map.Entry<String, E> pair : dir.getFiles().entrySet()) {
Path path = targetDir.resolve(pair.getKey());
E entry = pair.getValue();
nameStack.add(pair.getKey());
if (tree.isLink(entry)) {
String linkTarget = tree.getLink(entry);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalLink(linkTarget));
Files.createSymbolicLink(path, Paths.get(linkTarget));
} else {
long size = 0L;
try (InputStream input = tree.getInputStream(entry);
OutputStream output = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
messageDigest.reset();
int c;
while ((c = input.read(buffer)) > 0) {
size += c;
output.write(buffer, 0, c);
messageDigest.update(buffer, 0, c);
}
}
if (tree.isExecutable(entry))
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
}
nameStack.remove(nameStack.size() - 1);
}
for (Map.Entry<String, ArchiveFileTree.Dir<E>> pair : dir.getSubDirs().entrySet()) {
nameStack.add(pair.getKey());
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalDirectory());
copyDirContent(tree, pair.getValue(), targetDir.resolve(pair.getKey()));
nameStack.remove(nameStack.size() - 1);
}
}
}
@@ -0,0 +1,128 @@
/*
* 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.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import java.lang.reflect.Type;
/**
* @author Glavo
*/
public final class JavaLocalFiles {
@JsonAdapter(Serializer.class)
public abstract static class Local {
private final String type;
Local(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public static final class LocalFile extends Local {
private final String sha1;
private final long size;
public LocalFile(String sha1, long size) {
super("file");
this.sha1 = sha1;
this.size = size;
}
public String getSha1() {
return sha1;
}
public long getSize() {
return size;
}
}
public static final class LocalDirectory extends Local {
public LocalDirectory() {
super("directory");
}
}
public static final class LocalLink extends Local {
private final String target;
public LocalLink(String target) {
super("link");
this.target = target;
}
public String getTarget() {
return target;
}
}
public static class Serializer implements JsonSerializer<Local>, JsonDeserializer<Local> {
@Override
public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("type", src.getType());
if (src instanceof LocalFile) {
obj.addProperty("sha1", ((LocalFile) src).getSha1());
obj.addProperty("size", ((LocalFile) src).getSize());
} else if (src instanceof LocalLink) {
obj.addProperty("target", ((LocalLink) src).getTarget());
}
return obj;
}
@Override
public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
JsonObject obj = json.getAsJsonObject();
if (!obj.has("type"))
throw new JsonParseException(json.toString());
String type = obj.getAsJsonPrimitive("type").getAsString();
try {
switch (type) {
case "file": {
String sha1 = obj.getAsJsonPrimitive("sha1").getAsString();
long size = obj.getAsJsonPrimitive("size").getAsLong();
return new LocalFile(sha1, size);
}
case "directory": {
return new LocalDirectory();
}
case "link": {
String target = obj.getAsJsonPrimitive("target").getAsString();
return new LocalLink(target);
}
default:
throw new AssertionError("unknown type: " + type);
}
} catch (Throwable e) {
throw new JsonParseException(json.toString());
}
}
}
}
@@ -0,0 +1,698 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.JavaVersionConstraint;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManager {
private JavaManager() {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "windows-x86";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "windows-x64";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "windows-arm64";
}
} else if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "linux-i386";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "linux";
}
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "mac-os";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "mac-os-arm64";
}
}
return null;
}
public static Path getExecutable(Path javaHome) {
return javaHome.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable());
}
public static Path getMacExecutable(Path javaHome) {
return javaHome.resolve("jre.bundle/Contents/Home/bin/java");
}
public static boolean isCompatible(Platform platform) {
if (platform.getOperatingSystem() != OperatingSystem.CURRENT_OS)
return false;
Architecture architecture = platform.getArchitecture();
if (architecture == Architecture.SYSTEM_ARCH || architecture == Architecture.CURRENT_ARCH)
return true;
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
}
return false;
}
private static volatile Map<Path, JavaRuntime> allJava;
private static final CountDownLatch LATCH = new CountDownLatch(1);
private static final ObjectProperty<Collection<JavaRuntime>> allJavaProperty = new SimpleObjectProperty<>();
private static Map<Path, JavaRuntime> getAllJavaMap() throws InterruptedException {
Map<Path, JavaRuntime> map = allJava;
if (map == null) {
LATCH.await();
map = allJava;
}
return map;
}
private static void updateAllJavaProperty(Map<Path, JavaRuntime> javaRuntimes) {
JavaRuntime[] array = javaRuntimes.values().toArray(new JavaRuntime[0]);
Arrays.sort(array);
allJavaProperty.set(Arrays.asList(array));
}
public static boolean isInitialized() {
return allJava != null;
}
public static Collection<JavaRuntime> getAllJava() throws InterruptedException {
return getAllJavaMap().values();
}
public static ObjectProperty<Collection<JavaRuntime>> getAllJavaProperty() {
return allJavaProperty;
}
public static JavaRuntime getJava(Path executable) throws IOException, InterruptedException {
executable = executable.toRealPath();
JavaRuntime javaRuntime = getAllJavaMap().get(executable);
if (javaRuntime != null) {
return javaRuntime;
}
JavaInfo info = JavaInfo.fromExecutable(executable);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
updateAllJavaProperty(result);
}
}).start();
}
public static Task<JavaRuntime> getAddJavaTask(Path binary) {
return Task.supplyAsync("Get Java", () -> JavaManager.getJava(binary))
.thenApplyAsync(Schedulers.javafx(), javaRuntime -> {
if (!JavaManager.isCompatible(javaRuntime.getPlatform())) {
throw new UnsupportedPlatformException("Incompatible platform: " + javaRuntime.getPlatform());
}
String pathString = javaRuntime.getBinary().toString();
ConfigHolder.globalConfig().getDisabledJava().remove(pathString);
if (ConfigHolder.globalConfig().getUserJava().add(pathString)) {
addJava(javaRuntime);
}
return javaRuntime;
});
}
public static Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
assert java.isManaged();
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
FXUtils.runInFX(() -> {
try {
removeJava(java);
} catch (InterruptedException e) {
throw new AssertionError("Unreachable code", e);
}
});
String name = relativized.getName(0).toString();
return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name);
} else {
return Task.completed(null);
}
}
// FXThread
public static void addJava(JavaRuntime java) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (!oldMap.containsKey(java.getBinary())) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.put(java.getBinary(), java);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
// FXThread
public static void removeJava(JavaRuntime java) throws InterruptedException {
removeJava(java.getBinary());
}
// FXThread
public static void removeJava(Path realPath) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (oldMap.containsKey(realPath)) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.remove(realPath);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
}
@Nullable
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
return findSuitableJava(getAllJava(), gameVersion, version);
}
@Nullable
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, Version version) {
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
if (forceX86) {
if (!java.getArchitecture().isX86())
continue;
} else {
if (java.getArchitecture() != Architecture.SYSTEM_ARCH)
continue;
}
boolean violationMandatory = false;
boolean violationSuggested = false;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violationMandatory = true;
} else {
violationSuggested = true;
}
}
}
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
}
}
return suggested != null ? suggested : mandatory;
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
}
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
}
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
}
break;
case LINUX:
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
break;
case OSX:
tryAddJavaHome(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines/Contents/Home"));
tryAddJavaHome(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines/Contents/Home"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// Homebrew
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
for (Path dir : dirs) {
searchAllJavaInDirectory(javaRuntimes, dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
}
break;
default:
break;
}
// Search Minecraft bundled runtimes
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
}
if (System.getenv("HMCL_JRES") != null) {
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
} catch (InvalidPathException ignored) {
}
}
}
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
}
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
javaRuntimes.put(currentJava.getBinary(), currentJava);
}
LOG.trace(javaRuntimes.values().stream().sorted()
.map(it -> String.format(" - %s %s (%s, %s): %s",
it.isJDK() ? "JDK" : "JRE",
it.getVersion(),
it.getPlatform().getArchitecture().getDisplayName(),
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
it.getBinary()))
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
return javaRuntimes;
}
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
}
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
try {
info = JavaInfo.fromExecutable(executable, false);
} catch (IOException e2) {
e2.addSuppressed(e);
LOG.warning("Failed to lookup Java executable at " + executable, e2);
}
}
}
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfo.fromExecutable(executable);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
if (info != null && isCompatible(info.getPlatform())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
}
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
tryAddJavaHome(javaRuntimes, dir);
}
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_X86_64), verify);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Paths.get(programFiles);
} catch (InvalidPathException ignored) {
return;
}
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
private static List<String> querySubFolders(String location) {
List<String> res = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith(location) && !line.equals(location)) {
res.add(line);
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query sub folders of " + location, e);
}
return res;
}
private static String queryRegisterValue(String location, String name) {
boolean last = false;
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (StringUtils.isNotBlank(line)) {
if (last && line.trim().startsWith(name)) {
int begins = line.indexOf(name);
if (begins > 0) {
String s2 = line.substring(begins + name.length());
begins = s2.indexOf("REG_SZ");
if (begins > 0) {
return s2.substring(begins + "REG_SZ".length()).trim();
}
}
}
if (location.equals(line.trim())) {
last = true;
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query register value of " + location, e);
}
return null;
}
}
@@ -0,0 +1,112 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 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.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
/**
* @author Glavo
*/
@JsonAdapter(JavaManifest.Serializer.class)
public final class JavaManifest {
private final JavaInfo info;
@Nullable
private final Map<String, Object> update;
@Nullable
private final Map<String, JavaLocalFiles.Local> files;
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
this.info = info;
this.update = update;
this.files = files;
}
public JavaInfo getInfo() {
return info;
}
public Map<String, Object> getUpdate() {
return update;
}
public Map<String, JavaLocalFiles.Local> getFiles() {
return files;
}
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
private static final Type LOCAL_FILES_TYPE = mapTypeOf(String.class, JavaLocalFiles.Local.class).getType();
@Override
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject res = new JsonObject();
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.getInfo().getVersion());
res.addProperty("java.vendor", src.getInfo().getVendor());
if (src.getUpdate() != null)
res.add("update", context.serialize(src.getUpdate()));
if (src.getFiles() != null)
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
return res;
}
@Override
public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
try {
JsonObject jsonObject = json.getAsJsonObject();
OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString();
String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null);
Map<String, Object> update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null;
Map<String, JavaLocalFiles.Local> files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null;
if (osName == null || osArch == null || javaVersion == null)
throw new JsonParseException(json.toString());
return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files);
} catch (JsonParseException e) {
throw e;
} catch (Throwable e) {
throw new JsonParseException(e);
}
}
}
}
@@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.*;
@@ -32,19 +32,20 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
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;
import java.util.*;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
@@ -52,24 +53,28 @@ 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;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
*/
public final class Accounts {
private 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);
LOG.warning("Failed to check update for authlib-injector", e);
}
});
}
@@ -78,17 +83,16 @@ public final class Accounts {
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
static {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.put("microsoft", FACTORY_MICROSOFT);
@@ -96,14 +100,24 @@ public final class Accounts {
}
public static String getLoginType(AccountFactory<?> factory) {
return Optional.ofNullable(factory2type.get(factory))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
String type = factory2type.get(factory);
if (type != null) return type;
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
return factory2type.get(FACTORY_AUTHLIB_INJECTOR);
}
throw new IllegalArgumentException("Unrecognized account factory");
}
public static AccountFactory<?> getAccountFactory(String loginType) {
return Optional.ofNullable(type2factory.get(loginType))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
}
public static BoundAuthlibInjectorAccountFactory getAccountFactoryByAuthlibInjectorServer(AuthlibInjectorServer server) {
return new BoundAuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, server);
}
// ====
public static AccountFactory<?> getAccountFactory(Account account) {
@@ -111,67 +125,26 @@ public final class Accounts {
return FACTORY_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
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);
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static final String GLOBAL_PREFIX = "$GLOBAL:";
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
updateAccountStorages();
}
};
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[]{account});
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
@@ -181,7 +154,64 @@ public final class Accounts {
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
ArrayList<Map<Object, Object>> global = new ArrayList<>();
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
for (Account account : accounts) {
Map<Object, Object> storage = getAccountStorage(account);
if (account.isPortable())
portable.add(storage);
else
global.add(storage);
}
if (!global.equals(globalAccountStorages))
globalAccountStorages.setAll(global);
if (!portable.equals(config().getAccountStorages()))
config().getAccountStorages().setAll(portable);
}
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
} catch (Throwable e) {
LOG.warning("Failed to load global accounts", e);
}
}
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
LOG.info("Saving global accounts");
synchronized (globalAccountsFile) {
try {
synchronized (globalAccountsFile) {
FileUtils.saveSafely(globalAccountsFile, json);
}
} catch (IOException e) {
LOG.error("Failed to save global accounts", e);
}
}
});
globalAccountStorages.addListener(onInvalidating(() ->
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
}
private static Account parseAccount(Map<Object, Object> storage) {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return null;
}
try {
return factory.fromStorage(storage);
} catch (Exception e) {
LOG.warning("Failed to load account: " + storage, e);
return null;
}
}
/**
@@ -191,45 +221,112 @@ public final class Accounts {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
if (!config().isAddedLittleSkin()) {
AuthlibInjectorServer littleSkin = new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/");
if (Boolean.TRUE.equals(storage.get("selected"))) {
selectedAccount.set(account);
if (config().getAuthlibInjectorServers().stream().noneMatch(it -> littleSkin.getUrl().equals(it.getUrl()))) {
config().getAuthlibInjectorServers().add(0, littleSkin);
}
});
config().setAddedLittleSkin(true);
}
loadGlobalAccountStorages();
// load accounts
Account selected = null;
for (Map<Object, Object> storage : config().getAccountStorages()) {
Account account = parseAccount(storage);
if (account != null) {
account.setPortable(true);
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selected = account;
}
}
}
for (Map<Object, Object> storage : globalAccountStorages) {
Account account = parseAccount(storage);
if (account != null) {
accounts.add(account);
}
}
String selectedAccountIdentifier = config().getSelectedAccount();
if (selected == null && selectedAccountIdentifier != null) {
boolean portable = true;
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
portable = false;
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
}
for (Account account : accounts) {
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
if (portable == account.isPortable()) {
selected = account;
break;
} else if (selected == null) {
selected = account;
}
}
}
}
if (selected == null && !accounts.isEmpty()) {
selected = accounts.get(0);
}
selectedAccount.set(selected);
InvalidationListener listener = o -> {
// this method first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account account = selectedAccount.get();
if (accounts.isEmpty()) {
if (account == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
selectedAccount.set(null);
}
} else {
if (accounts.contains(account)) {
// valid
} else {
// the previously selected account is gone
selectedAccount.set(accounts.get(0));
}
}
};
selectedAccount.addListener(listener);
selectedAccount.addListener(onInvalidating(() -> {
Account account = selectedAccount.get();
if (account != null)
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
else
config().setSelectedAccount(null);
}));
accounts.addListener(listener);
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
Account selected = selectedAccount.get();
if (selected != null) {
Account finalSelected = selected;
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
finalSelected.logIn();
} catch (Throwable e) {
LOG.warning("Failed to log " + finalSelected + " in", e);
}
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
triggerAuthlibInjectorUpdateCheck();
}
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
@@ -238,7 +335,7 @@ public final class Accounts {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
@@ -248,10 +345,6 @@ public final class Accounts {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
@@ -313,9 +406,8 @@ public final class Accounts {
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
private static final 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_MICROSOFT, "account.methods.microsoft"));
@@ -329,7 +421,11 @@ public final class Accounts {
if (exception instanceof NoCharacterException) {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return i18n("account.failed.connect_authentication_server");
if (exception.getCause() instanceof SSLException) {
return i18n("account.failed.ssl");
} else {
return i18n("account.failed.connect_authentication_server");
}
} else if (exception instanceof ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} else if (exception instanceof RemoteAuthenticationException) {
@@ -370,6 +466,8 @@ public final class Accounts {
} else {
return i18n("account.methods.microsoft.error.unknown", errorCode);
}
} else if (exception instanceof MicrosoftService.XBox400Exception) {
return i18n("account.methods.microsoft.error.wrong_verify_method");
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
return i18n("account.methods.microsoft.error.no_character");
} else if (exception instanceof MicrosoftService.NoXuiException) {
@@ -22,70 +22,75 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class AuthlibInjectorServers implements Validation {
public final class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
private static final Set<AuthlibInjectorServer> servers = new CopyOnWriteArraySet<>();
public static Set<AuthlibInjectorServer> getServers() {
return servers;
}
private final List<String> urls;
public AuthlibInjectorServers(List<String> urls) {
private AuthlibInjectorServers(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return urls;
}
@Override
public void validate() throws JsonParseException {
if (urls == null)
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
public void validate() throws JsonParseException, TolerableValidationException {
if (this.urls == null) {
throw new JsonParseException("authlib-injectors.json -> urls cannot be null.");
}
}
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
private static AuthlibInjectorServers configInstance;
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
public static void init() {
Path configLocation;
Path jarPath = JarUtils.thisJarPath();
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
configLocation = jarPath.getParent().resolve(CONFIG_FILENAME);
} else {
configLocation = Paths.get(CONFIG_FILENAME);
}
configInstance = new AuthlibInjectorServers(Collections.emptyList());
if (Files.exists(configLocation)) {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
LOG.warning("Malformed authlib-injectors.json", e);
return;
}
if (!configInstance.urls.isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
for (String url : configInstance.urls) {
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
.thenAcceptAsync(Schedulers.javafx(), server -> {
config().getAuthlibInjectorServers().add(server);
servers.add(server);
})
.start();
}
}
}
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
for (String url : AuthlibInjectorServers.getConfigInstance().getUrls()) {
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
.thenAcceptAsync(Schedulers.javafx(), server -> config().getAuthlibInjectorServers().add(server))
.start();
}
}
}
public static AuthlibInjectorServers getConfigInstance() {
return configInstance;
}
}
@@ -51,7 +51,7 @@ public final class Config implements Cloneable, Observable {
public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder()
public static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
@@ -112,6 +112,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("proxyPassword")
private StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("x")
private DoubleProperty x = new SimpleDoubleProperty();
@SerializedName("y")
private DoubleProperty y = new SimpleDoubleProperty();
@SerializedName("width")
private DoubleProperty width = new SimpleDoubleProperty();
@@ -119,19 +125,19 @@ public final class Config implements Cloneable, Observable {
private DoubleProperty height = new SimpleDoubleProperty();
@SerializedName("theme")
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>();
@SerializedName("localization")
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("autoDownloadThreads")
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(false);
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true);
@SerializedName("downloadThreads")
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
@SerializedName("downloadType")
private StringProperty downloadType = new SimpleStringProperty("mcbbs");
private StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID);
@SerializedName("autoChooseDownloadType")
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
@@ -140,13 +146,16 @@ public final class Config implements Cloneable, Observable {
private StringProperty versionListSource = new SimpleStringProperty("balanced");
@SerializedName("configurations")
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty();
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("fontFamily")
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
private StringProperty fontFamily = new SimpleStringProperty();
@SerializedName("fontSize")
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@@ -155,7 +164,7 @@ public final class Config implements Cloneable, Observable {
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(100);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@@ -163,6 +172,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -182,6 +197,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("preferredLoginType")
private StringProperty preferredLoginType = new SimpleStringProperty();
@SerializedName("animationDisabled")
private BooleanProperty animationDisabled = new SimpleBooleanProperty();
@SerializedName("shownTips")
private ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
@@ -364,6 +385,30 @@ public final class Config implements Cloneable, Observable {
return proxyPass;
}
public double getX() {
return x.get();
}
public DoubleProperty xProperty() {
return x;
}
public void setX(double x) {
this.x.set(x);
}
public double getY() {
return y.get();
}
public DoubleProperty yProperty() {
return y;
}
public void setY(double y) {
this.y.set(y);
}
public double getWidth() {
return width.get();
}
@@ -472,10 +517,22 @@ public final class Config implements Cloneable, Observable {
return versionListSource;
}
public ObservableMap<String, Profile> getConfigurations() {
public MapProperty<String, Profile> getConfigurations() {
return configurations;
}
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
@@ -516,15 +573,15 @@ public final class Config implements Cloneable, Observable {
this.launcherFontFamily.set(launcherFontFamily);
}
public int getLogLines() {
public Integer getLogLines() {
return logLines.get();
}
public void setLogLines(int logLines) {
public void setLogLines(Integer logLines) {
this.logLines.set(logLines);
}
public IntegerProperty logLinesProperty() {
public ObjectProperty<Integer> logLinesProperty() {
return logLines;
}
@@ -532,6 +589,18 @@ public final class Config implements Cloneable, Observable {
return authlibInjectorServers;
}
public boolean isAddedLittleSkin() {
return addedLittleSkin.get();
}
public BooleanProperty addedLittleSkinProperty() {
return addedLittleSkin;
}
public void setAddedLittleSkin(boolean addedLittleSkin) {
this.addedLittleSkin.set(addedLittleSkin);
}
public int getConfigVersion() {
return configVersion.get();
}
@@ -568,6 +637,18 @@ public final class Config implements Cloneable, Observable {
return preferredLoginType;
}
public boolean isAnimationDisabled() {
return animationDisabled.get();
}
public BooleanProperty animationDisabledProperty() {
return animationDisabled;
}
public void setAnimationDisabled(boolean animationDisabled) {
this.animationDisabled.set(animationDisabled);
}
public boolean isTitleTransparent() {
return titleTransparent.get();
}
@@ -579,4 +660,20 @@ public final class Config implements Cloneable, Observable {
public void setTitleTransparent(boolean titleTransparent) {
this.titleTransparent.set(titleTransparent);
}
public String getPromptedVersion() {
return promptedVersion.get();
}
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
public void setPromptedVersion(String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
public ObservableMap<String, Object> getShownTips() {
return shownTips;
}
}
@@ -17,21 +17,20 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.*;
import java.util.Map;
import java.util.logging.Level;
import java.util.Locale;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ConfigHolder {
@@ -46,6 +45,7 @@ public final class ConfigHolder {
private static Config configInstance;
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
public static Config config() {
if (configInstance == null) {
@@ -61,18 +61,26 @@ public final class ConfigHolder {
return globalConfigInstance;
}
public static Path configLocation() {
return configLocation;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public synchronized static void init() throws IOException {
public static boolean isOwnerChanged() {
return ownerChanged;
}
public static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configLocation = locateConfig();
LOG.log(Level.INFO, "Config location: " + configLocation);
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
@@ -80,6 +88,9 @@ public final class ConfigHolder {
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
Locale.setDefault(config().getLocalization().getLocale());
I18n.setLocale(configInstance.getLocalization());
LOG.setLogRetention(globalConfig().getLogRetention());
Settings.init();
if (newlyCreated) {
@@ -90,7 +101,7 @@ public final class ConfigHolder {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
LOG.warning("Failed to set hidden attribute of " + configLocation, e);
}
}
}
@@ -110,11 +121,10 @@ public final class ConfigHolder {
}
private static Path locateConfig() {
Path exePath = Paths.get("");
Path exePath = Paths.get("").toAbsolutePath();
try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath();
if (Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
Path jarPath = JarUtils.thisJarPath();
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
@@ -144,18 +154,26 @@ public final class ConfigHolder {
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
&& "root".equals(System.getProperty("user.name"))
&& !"root".equals(Files.getOwner(configLocation).getName())) {
ownerChanged = true;
}
} catch (IOException e1) {
LOG.warning("Failed to get owner");
}
try {
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
ConfigUpgrader.upgradeConfig(deserialized, raw);
ConfigUpgrader.upgradeConfig(deserialized, content);
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
LOG.warning("Malformed config.", e);
}
}
@@ -168,14 +186,14 @@ public final class ConfigHolder {
try {
writeToConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
LOG.error("Failed to save config", e);
}
});
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (configLocation) {
Files.write(configLocation, content.getBytes(UTF_8));
FileUtils.saveSafely(configLocation, content);
}
}
@@ -200,7 +218,7 @@ public final class ConfigHolder {
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
LOG.warning("Malformed config.", e);
}
}
@@ -212,14 +230,14 @@ public final class ConfigHolder {
try {
writeToGlobalConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
LOG.error("Failed to save config", e);
}
});
private static void writeToGlobalConfig(String content) throws IOException {
LOG.info("Saving global config");
synchronized (GLOBAL_CONFIG_PATH) {
Files.write(GLOBAL_CONFIG_PATH, content.getBytes(UTF_8));
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
}
}
@@ -17,113 +17,89 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
final class ConfigUpgrader {
private static final int VERSION = 0;
private ConfigUpgrader() {
}
private static final int CURRENT_VERSION = 2;
/**
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
* This method is for the compatibility with old HMCL versions.
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
* @return true if config version is upgraded
* @param rawContent raw json content of the config settings without modification
*/
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
boolean upgraded;
if (deserialized.getConfigVersion() < VERSION) {
deserialized.setConfigVersion(VERSION);
// TODO: Add upgrade code here.
upgraded = true;
} else {
upgraded = false;
static void upgradeConfig(Config deserialized, String rawContent) {
int configVersion = deserialized.getConfigVersion();
if (configVersion == CURRENT_VERSION) {
return;
}
upgradeV2(deserialized, rawJson);
upgradeV3(deserialized, rawJson);
if (configVersion > CURRENT_VERSION) {
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
deserialized.setConfigVersion(CURRENT_VERSION);
return;
}
return upgraded;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
/**
* Upgrade configuration of HMCL 2.x
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
String selected = rawJson.containsKey("selectedAccount") ? null
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
if (configVersion < 1) {
// Upgrade configuration of HMCL 2.x: Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
String selected = rawJson.containsKey("selectedAccount") ? null
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
Map<Object, Object> storage = new HashMap<>();
storage.put("type", "offline");
storage.put("username", key);
storage.put("uuid", value);
if (key.equals(selected)) {
storage.put("selected", true);
}
deserialized.getAccountStorages().add(storage);
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
Map<Object, Object> storage = new HashMap<>();
storage.put("type", "offline");
storage.put("username", key);
storage.put("uuid", value);
if (key.equals(selected)) {
storage.put("selected", true);
}
deserialized.getAccountStorages().add(storage);
});
});
});
});
});
}
/**
* Upgrade configuration of HMCL earlier than 3.1.70
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
// Upgrade configuration of HMCL earlier than 3.1.70
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
if (!rawJson.containsKey("downloadType")) {
tryCast(rawJson.get("downloadtype"), Number.class)
.map(Number::intValue)
.ifPresent(id -> {
if (id == 0) {
deserialized.setDownloadType("mojang");
} else if (id == 1) {
deserialized.setDownloadType("bmclapi");
}
});
if (!rawJson.containsKey("downloadType")) {
tryCast(rawJson.get("downloadtype"), Number.class)
.map(Number::intValue)
.ifPresent(id -> {
if (id == 0) {
deserialized.setDownloadType("mojang");
} else if (id == 1) {
deserialized.setDownloadType("bmclapi");
}
});
}
}
tryCast(rawJson.get("selectedAccount"), String.class)
.ifPresent(selected -> {
deserialized.getAccountStorages().stream()
.filter(storage -> {
Object type = storage.get("type");
if ("offline".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("username"));
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
} else {
return false;
}
})
.findFirst()
.ifPresent(storage -> storage.put("selected", true));
});
deserialized.setConfigVersion(CURRENT_VERSION);
}
}
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
@@ -54,10 +55,9 @@ public final class DownloadProviders {
private static final MojangDownloadProvider MOJANG;
private static final BMCLAPIDownloadProvider BMCLAPI;
private static final BMCLAPIDownloadProvider MCBBS;
public static final String DEFAULT_PROVIDER_ID = "balanced";
public static final String DEFAULT_RAW_PROVIDER_ID = "mcbbs";
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
private static final InvalidationListener observer;
@@ -68,21 +68,19 @@ public final class DownloadProviders {
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
MCBBS = new BMCLAPIDownloadProvider("https://download.mcbbs.net");
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", BMCLAPI),
pair("mcbbs", MCBBS)
pair("bmclapi", BMCLAPI)
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
providersById = mapOf(
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
pair("mirror", new AutoDownloadProvider(MCBBS, fileProvider)));
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
observer = FXUtils.observeWeak(() -> {
FetchTask.setDownloadExecutorConcurrency(
@@ -101,12 +99,11 @@ public final class DownloadProviders {
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
});
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
if (!rawProviders.containsKey(downloadType)) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
return;
}
if (!rawProviders.containsKey(config().getDownloadType())) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
}
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
fileDownloadProvider.setDownloadProviderCandidates(
@@ -156,6 +153,8 @@ public final class DownloadProviders {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
@@ -21,42 +21,23 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Type;
import java.net.Proxy;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public class GlobalConfig implements Cloneable, Observable {
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
.create();
public final class GlobalConfig implements Cloneable, Observable {
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) {
return null;
}
@@ -66,17 +47,19 @@ public class GlobalConfig implements Cloneable, Observable {
return instance;
}
private IntegerProperty agreementVersion = new SimpleIntegerProperty();
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
private StringProperty multiplayerToken = new SimpleStringProperty();
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
private final Map<String, Object> unknownFields = new HashMap<>();
private transient ObservableHelper helper = new ObservableHelper(this);
private final transient ObservableHelper helper = new ObservableHelper(this);
public GlobalConfig() {
PropertyUtils.attachListener(this, helper);
@@ -93,7 +76,7 @@ public class GlobalConfig implements Cloneable, Observable {
}
public String toJson() {
return CONFIG_GSON.toJson(this);
return Config.CONFIG_GSON.toJson(this);
}
@Override
@@ -113,47 +96,45 @@ public class GlobalConfig implements Cloneable, Observable {
this.agreementVersion.set(agreementVersion);
}
public boolean isMultiplayerRelay() {
return multiplayerRelay.get();
public int getPlatformPromptVersion() {
return platformPromptVersion.get();
}
public BooleanProperty multiplayerRelayProperty() {
return multiplayerRelay;
public IntegerProperty platformPromptVersionProperty() {
return platformPromptVersion;
}
public void setMultiplayerRelay(boolean multiplayerRelay) {
this.multiplayerRelay.set(multiplayerRelay);
public void setPlatformPromptVersion(int platformPromptVersion) {
this.platformPromptVersion.set(platformPromptVersion);
}
public int getMultiplayerAgreementVersion() {
return multiplayerAgreementVersion.get();
public int getLogRetention() {
return logRetention.get();
}
public IntegerProperty multiplayerAgreementVersionProperty() {
return multiplayerAgreementVersion;
public IntegerProperty logRetentionProperty() {
return logRetention;
}
public void setMultiplayerAgreementVersion(int multiplayerAgreementVersion) {
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
public void setLogRetention(int logRetention) {
this.logRetention.set(logRetention);
}
public String getMultiplayerToken() {
return multiplayerToken.get();
public ObservableSet<String> getUserJava() {
return userJava;
}
public StringProperty multiplayerTokenProperty() {
return multiplayerToken;
public ObservableSet<String> getDisabledJava() {
return disabledJava;
}
public void setMultiplayerToken(String multiplayerToken) {
this.multiplayerToken.set(multiplayerToken);
}
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"multiplayerToken",
"multiplayerAgreementVersion"
"platformPromptVersion",
"logRetention",
"userJava",
"disabledJava"
));
@Override
@@ -164,9 +145,14 @@ public class GlobalConfig implements Cloneable, Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
if (!src.getUserJava().isEmpty())
jsonObject.add("userJava", context.serialize(src.getUserJava()));
if (!src.getDisabledJava().isEmpty())
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
}
@@ -182,9 +168,22 @@ public class GlobalConfig implements Cloneable, Observable {
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
for (JsonElement element : userJava.getAsJsonArray()) {
config.userJava.add(element.getAsString());
}
}
JsonElement disabledJava = obj.get("disabledJava");
if (disabledJava != null && disabledJava.isJsonArray()) {
for (JsonElement element : disabledJava.getAsJsonArray()) {
config.disabledJava.add(element.getAsString());
}
}
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (!knownFields.contains(entry.getKey())) {
@@ -1,164 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.SerializedName;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class HMCLAccounts {
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
private HMCLAccounts() {
}
public static HMCLAccount getAccount() {
return account.get();
}
public static ObjectProperty<HMCLAccount> accountProperty() {
return account;
}
public static void setAccount(HMCLAccount account) {
HMCLAccounts.account.set(account);
}
public static Task<Void> login() {
String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID());
String scope = "openid offline_access";
return Task.supplyAsync(() -> {
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
mapOf(
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
pair("response_type", "id_token code"),
pair("response_mode", "form_post"),
pair("scope", scope),
pair("redirect_uri", session.getRedirectURI()),
pair("nonce", nonce)
)));
String code = session.waitFor();
// Authorization Code -> Token
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
.getString();
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
OAuth.AuthorizationResponse.class);
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
.header("Token-Type", response.tokenType)
.header("Access-Token", response.accessToken)
.header("Authorization-Provider", "microsoft")
.authorization("Bearer", session.getIdToken())
.getJson(HMCLAccountProfile.class);
return new HMCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken);
}).thenAcceptAsync(Schedulers.javafx(), account -> {
setAccount(account);
});
}
public static class HMCLAccount implements HttpRequest.Authorization {
private final String provider;
private final String nickname;
private final String email;
private final String role;
private final String idToken;
private final String tokenType;
private final String accessToken;
private final String refreshToken;
public HMCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) {
this.provider = provider;
this.nickname = nickname;
this.email = email;
this.role = role;
this.idToken = idToken;
this.tokenType = tokenType;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public String getProvider() {
return provider;
}
public String getNickname() {
return nickname;
}
public String getEmail() {
return email;
}
public String getRole() {
return role;
}
public String getIdToken() {
return idToken;
}
@Override
public String getTokenType() {
return tokenType;
}
@Override
public String getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
}
private static class HMCLAccountProfile {
@SerializedName("ID")
String id;
@SerializedName("Provider")
String provider;
@SerializedName("Email")
String email;
@SerializedName("NickName")
String nickname;
@SerializedName("Role")
String role;
}
}
@@ -0,0 +1,25 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
/**
* @author Glavo
*/
public enum JavaVersionType {
DEFAULT, AUTO, VERSION, DETECTED, CUSTOM
}
@@ -43,5 +43,9 @@ public enum LauncherVisibility {
/**
* Hide the launcher and reopen it when game closes.
*/
HIDE_AND_REOPEN
HIDE_AND_REOPEN;
public boolean isDaemon() {
return this != CLOSE;
}
}
@@ -20,17 +20,18 @@ package org.jackhuang.hmcl.setting;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@@ -130,8 +131,11 @@ public final class Profiles {
if (!initialized)
return;
// update storage
config().getConfigurations().clear();
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
for (Profile profile : profiles) {
newConfigurations.put(profile.getName(), profile);
}
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
}
/**
@@ -204,7 +208,7 @@ public final class Profiles {
return selectedVersion.get();
}
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
private static final List<Consumer<Profile>> versionsListeners = new ArrayList<>(4);
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
@@ -29,7 +29,7 @@ import java.net.Proxy;
import java.net.Proxy.Type;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private ProxyManager() {
@@ -20,11 +20,10 @@ package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
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 final class Settings {
@@ -46,14 +45,12 @@ public final class Settings {
}
private Settings() {
config().localizationProperty().addListener(unused -> updateSystemLocale());
updateSystemLocale();
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
Profiles.init();
AuthlibInjectorServers.init();
AnimationUtils.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
@@ -79,8 +76,4 @@ public final class Settings {
return null;
}
}
private static void updateSystemLocale() {
Locale.setDefault(config().getLocalization().getLocale());
}
}
@@ -23,19 +23,25 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
@@ -50,6 +56,35 @@ public class Theme {
Color.web("#B71C1C") // red
};
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Optional<Font> font;
private static Optional<Font> tryLoadFont() {
//noinspection OptionalAssignedToNull
if (font != null) {
return font;
}
Path path = Paths.get("font.ttf");
if (!Files.isRegularFile(path)) {
path = Paths.get("font.otf");
}
if (Files.isRegularFile(path)) {
try {
return font = Optional.ofNullable(Font.loadFont(path.toAbsolutePath().toUri().toURL().toExternalForm(), 0));
} catch (MalformedURLException ignored) {
}
}
return font = Optional.empty();
}
public static Theme getTheme() {
Theme theme = config().getTheme();
return theme == null ? BLUE : theme;
}
private final Color paint;
private final String color;
private final String name;
@@ -73,47 +108,80 @@ public class Theme {
}
public boolean isLight() {
return Color.web(color).grayscale().getRed() >= 0.5;
return paint.grayscale().getRed() >= 0.5;
}
public Color getForegroundColor() {
return isLight() ? Color.BLACK : Color.WHITE;
}
private static String rgba(Color color, double opacity) {
return String.format("rgba(%d, %d, %d, %.1f)",
(int) Math.ceil(color.getRed() * 256),
(int) Math.ceil(color.getGreen() * 256),
(int) Math.ceil(color.getBlue() * 256),
opacity);
}
public String[] getStylesheets(String overrideFontFamily) {
Color textFill = getForegroundColor();
String css = "/assets/css/blue.css";
String fontFamily;
if (System.getProperty("hmcl.font.override") != null) {
fontFamily = System.getProperty("hmcl.font.override");
} else if (overrideFontFamily != null) {
fontFamily = overrideFontFamily;
} else {
fontFamily = null;
String fontFamily = overrideFontFamily == null
? System.getProperty("hmcl.font.override", System.getenv("HMCL_FONT"))
: overrideFontFamily;
String fontStyle = null;
if (fontFamily == null) {
Optional<Font> font = tryLoadFont();
if (font.isPresent()) {
fontFamily = font.get().getFamily();
fontStyle = font.get().getStyle();
}
}
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%base-red%", Integer.toString((int)Math.ceil(paint.getRed() * 256)))
.replace("%base-green%", Integer.toString((int)Math.ceil(paint.getGreen() * 256)))
.replace("%base-blue%", Integer.toString((int)Math.ceil(paint.getBlue() * 256)))
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse("")));
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = "/assets/css/blue.css";
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
Color textFill = getForegroundColor();
StringBuilder themeBuilder = new StringBuilder(512);
themeBuilder.append(".root {")
.append("-fx-base-color:").append(color).append(';')
.append("-fx-base-darker-color: derive(-fx-base-color, -10%);")
.append("-fx-base-check-color: derive(-fx-base-color, 30%);")
.append("-fx-rippler-color:").append(rgba(paint, 0.3)).append(';')
.append("-fx-base-rippler-color: derive(").append(rgba(paint, 0.3)).append(", 100%);")
.append("-fx-base-disabled-text-fill:").append(rgba(textFill, 0.7)).append(";")
.append("-fx-base-text-fill:").append(getColorDisplayName(getForegroundColor())).append(";")
.append("-theme-thumb:").append(rgba(paint, 0.7)).append(";");
if (fontFamily == null)
// https://github.com/HMCL-dev/HMCL/pull/3423
themeBuilder.append("-fx-font-family: -fx-base-font-family;");
else
themeBuilder.append("-fx-font-family:\"").append(fontFamily).append("\";");
if (fontStyle != null && !fontStyle.isEmpty())
themeBuilder.append("-fx-font-style:\"").append(fontStyle).append("\";");
themeBuilder.append('}');
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
css = "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(themeBuilder.toString().getBytes(StandardCharsets.UTF_8));
else
try {
File temp = File.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
FileUtils.writeText(temp, themeBuilder.toString(), Charset.defaultCharset());
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create theme stylesheet. Fallback to blue theme.", e);
}
}
return new String[]{
css,
"/assets/css/root.css"
};
return new String[]{css, "/assets/css/root.css"};
}
public static Theme custom(String color) {
@@ -125,50 +193,67 @@ public class Theme {
public static Optional<Theme> getTheme(String name) {
if (name == null)
return Optional.empty();
else if (name.equalsIgnoreCase("blue"))
return Optional.of(custom("#5C6BC0"));
else if (name.equalsIgnoreCase("darker_blue"))
return Optional.of(custom("#283593"));
else if (name.equalsIgnoreCase("green"))
return Optional.of(custom("#43A047"));
else if (name.equalsIgnoreCase("orange"))
return Optional.of(custom("#E67E22"));
else if (name.equalsIgnoreCase("purple"))
return Optional.of(custom("#9C27B0"));
else if (name.equalsIgnoreCase("red"))
return Optional.of(custom("#F44336"));
if (name.startsWith("#"))
else if (name.startsWith("#"))
try {
Color.web(name);
return Optional.of(custom(name));
} catch (IllegalArgumentException ignore) {
}
else {
String color = null;
switch (name.toLowerCase(Locale.ROOT)) {
case "blue":
return Optional.of(BLUE);
case "darker_blue":
color = "#283593";
break;
case "green":
color = "#43A047";
break;
case "orange":
color = "#E67E22";
break;
case "purple":
color = "#9C27B0";
break;
case "red":
color = "#F44336";
}
if (color != null)
return Optional.of(new Theme(name, color));
}
return Optional.empty();
}
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
return c != null ? String.format("#%02X%02X%02X", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)) : null;
}
private static ObjectBinding<Color> FOREGROUND_FILL;
public static ObjectBinding<Color> foregroundFillBinding() {
return BindingMapping.of(config().themeProperty())
.map(Theme::getForegroundColor);
if (FOREGROUND_FILL == null)
FOREGROUND_FILL = Bindings.createObjectBinding(
() -> Theme.getTheme().getForegroundColor(),
config().themeProperty()
);
return FOREGROUND_FILL;
}
public static ObjectBinding<Color> blackFillBinding() {
return Bindings.createObjectBinding(() -> BLACK);
public static Color blackFill() {
return BLACK;
}
public static ObjectBinding<Color> whiteFillBinding() {
return Bindings.createObjectBinding(() -> Color.WHITE);
public static Color whiteFill() {
return Color.WHITE;
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
@Override
public void write(JsonWriter out, Theme value) throws IOException {
out.value(value.getName().toLowerCase());
out.value(value.getName().toLowerCase(Locale.ROOT));
}
@Override
@@ -0,0 +1,49 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.ui.FXUtils;
public enum VersionIconType {
DEFAULT("/assets/img/grass.png"),
GRASS("/assets/img/grass.png"),
CHEST("/assets/img/chest.png"),
CHICKEN("/assets/img/chicken.png"),
COMMAND("/assets/img/command.png"),
OPTIFINE("/assets/img/optifine.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
// Please append new items at last
private final String resourceUrl;
VersionIconType(String resourceUrl) {
this.resourceUrl = resourceUrl;
}
public Image getIcon() {
return FXUtils.newBuiltinImage(resourceUrl);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,29 +22,23 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.*;
import java.util.stream.Collectors;
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
@@ -60,7 +54,7 @@ public final class VersionSetting implements Cloneable {
this.global = global;
}
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
@@ -84,38 +78,43 @@ public final class VersionSetting implements Cloneable {
// java
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
private final ObjectProperty<JavaVersionType> javaVersionTypeProperty = new SimpleObjectProperty<>(this, "javaVersionType", JavaVersionType.AUTO);
public StringProperty javaProperty() {
return javaProperty;
public ObjectProperty<JavaVersionType> javaVersionTypeProperty() {
return javaVersionTypeProperty;
}
/**
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
public JavaVersionType getJavaVersionType() {
return javaVersionTypeProperty.get();
}
public void setJava(String java) {
javaProperty.set(java);
public void setJavaVersionType(JavaVersionType javaVersionType) {
javaVersionTypeProperty.set(javaVersionType);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
private final StringProperty javaVersionProperty = new SimpleStringProperty(this, "javaVersion", "");
public StringProperty javaVersionProperty() {
return javaVersionProperty;
}
public String getJavaVersion() {
return javaVersionProperty.get();
}
public void setJavaVersion(String java) {
javaVersionProperty.set(java);
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setJavaVersionType(JavaVersionType.CUSTOM);
setJavaVersion("");
setDefaultJavaPath(null);
}
public boolean isJavaAutoSelected() {
return "Auto".equals(getJava());
}
public void setJavaAutoSelected() {
setJava("Auto");
setJavaVersionType(JavaVersionType.AUTO);
setJavaVersion("");
setDefaultJavaPath(null);
}
@@ -341,6 +340,20 @@ public final class VersionSetting implements Cloneable {
minecraftArgsProperty.set(minecraftArgs);
}
private final StringProperty environmentVariablesProperty = new SimpleStringProperty(this, "environmentVariables", "");
public StringProperty environmentVariablesProperty() {
return environmentVariablesProperty;
}
public String getEnvironmentVariables() {
return environmentVariablesProperty.get();
}
public void setEnvironmentVariables(String env) {
environmentVariablesProperty.set(env);
}
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
public BooleanProperty noJVMArgsProperty() {
@@ -392,6 +405,20 @@ public final class VersionSetting implements Cloneable {
notCheckGameProperty.set(notCheckGame);
}
private final BooleanProperty notPatchNativesProperty = new SimpleBooleanProperty(this, "notPatchNatives", false);
public BooleanProperty notPatchNativesProperty() {
return notPatchNativesProperty;
}
public boolean isNotPatchNatives() {
return notPatchNativesProperty.get();
}
public void setNotPatchNatives(boolean notPatchNatives) {
notPatchNativesProperty.set(notPatchNatives);
}
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
public BooleanProperty showLogsProperty() {
@@ -469,7 +496,6 @@ public final class VersionSetting implements Cloneable {
widthProperty.set(width);
}
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public IntegerProperty heightProperty() {
@@ -540,6 +566,20 @@ public final class VersionSetting implements Cloneable {
processPriorityProperty.set(processPriority);
}
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
public Renderer getRenderer() {
return rendererProperty.get();
}
public ObjectProperty<Renderer> rendererProperty() {
return rendererProperty;
}
public void setRenderer(Renderer renderer) {
this.rendererProperty.set(renderer);
}
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
public boolean isUseNativeGLFW() {
@@ -568,6 +608,20 @@ public final class VersionSetting implements Cloneable {
this.useNativeOpenAL.set(useNativeOpenAL);
}
private final ObjectProperty<VersionIconType> versionIcon = new SimpleObjectProperty<>(this, "versionIcon", VersionIconType.DEFAULT);
public VersionIconType getVersionIcon() {
return versionIcon.get();
}
public ObjectProperty<VersionIconType> versionIconProperty() {
return versionIcon;
}
public void setVersionIcon(VersionIconType versionIcon) {
this.versionIcon.set(versionIcon);
}
// launcher settings
/**
@@ -589,60 +643,74 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version) {
return getJavaVersion(gameVersion, version, true);
}
public JavaRuntime getJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
switch (getJavaVersionType()) {
case DEFAULT:
return JavaRuntime.getDefault();
case AUTO:
return JavaManager.findSuitableJava(gameVersion, version);
case CUSTOM:
try {
return JavaManager.getJava(Paths.get(getJavaDir()));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java not found
}
case VERSION: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
}
}).thenSupplyAsync(() -> {
try {
if ("Default".equals(getJava())) {
return JavaVersion.fromCurrentEnvironment();
} else if (isJavaAutoSelected()) {
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
} else if (isUsesCustomJavaDir()) {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
runInFX(() -> {
setJava("Default");
});
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
} catch (InterruptedException e) {
throw new CancellationException();
}
});
}
int majorVersion = -1;
try {
majorVersion = Integer.parseInt(javaVersion);
} catch (NumberFormatException ignored) {
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
if (majorVersion < 0) {
LOG.warning("Invalid Java version: " + javaVersion);
return null;
}
final int finalMajorVersion = majorVersion;
Collection<JavaRuntime> allJava = JavaManager.getAllJava().stream()
.filter(it -> it.getParsedVersion() == finalMajorVersion)
.collect(Collectors.toList());
return JavaManager.findSuitableJava(allJava, gameVersion, version);
}
case DETECTED: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
try {
String defaultJavaPath = getDefaultJavaPath();
if (StringUtils.isNotBlank(defaultJavaPath)) {
JavaRuntime java = JavaManager.getJava(Paths.get(defaultJavaPath).toRealPath());
if (java != null && java.getVersion().equals(javaVersion)) {
return java;
}
}
} catch (IOException | InvalidPathException ignored) {
}
for (JavaRuntime java : JavaManager.getAllJava()) {
if (java.getVersion().equals(javaVersion)) {
return java;
}
}
return null;
}
default:
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
}
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
javaVersionProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
@@ -653,9 +721,11 @@ public final class VersionSetting implements Cloneable {
postExitCommand.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
environmentVariablesProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
notPatchNativesProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
@@ -664,19 +734,22 @@ public final class VersionSetting implements Cloneable {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
processPriorityProperty.addListener(listener);
rendererProperty.addListener(listener);
useNativeGLFW.addListener(listener);
useNativeOpenAL.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
versionIcon.addListener(listener);
}
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setJavaVersionType(getJavaVersionType());
versionSetting.setJavaVersion(getJavaVersion());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
@@ -688,9 +761,11 @@ public final class VersionSetting implements Cloneable {
versionSetting.setPostExitCommand(getPostExitCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setEnvironmentVariables(getEnvironmentVariables());
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setNotPatchNatives(isNotPatchNatives());
versionSetting.setShowLogs(isShowLogs());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
@@ -699,10 +774,12 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setProcessPriority(getProcessPriority());
versionSetting.setRenderer(getRenderer());
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
versionSetting.setVersionIcon(getVersionIcon());
return versionSetting;
}
@@ -715,6 +792,7 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("usesGlobal", src.isUsesGlobal());
obj.addProperty("javaArgs", src.getJavaArgs());
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
obj.addProperty("environmentVariables", src.getEnvironmentVariables());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
@@ -725,12 +803,12 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
obj.addProperty("postExitCommand", src.getPostExitCommand());
obj.addProperty("serverIp", src.getServerIp());
obj.addProperty("java", src.getJava());
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
@@ -741,13 +819,44 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
obj.addProperty("javaVersionType", src.getJavaVersionType().name());
String java;
switch (src.getJavaVersionType()) {
case DEFAULT:
java = "Default";
break;
case AUTO:
java = "Auto";
break;
case CUSTOM:
java = "Custom";
break;
default:
java = src.getJavaVersion();
break;
}
obj.addProperty("java", java);
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
return obj;
}
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
if (index == null)
return defaultValue;
int idx = index.getAsInt();
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
}
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
if (!(json instanceof JsonObject))
return null;
JsonObject obj = (JsonObject) json;
@@ -759,6 +868,7 @@ public final class VersionSetting implements Cloneable {
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
vs.setEnvironmentVariables(Optional.ofNullable(obj.get("environmentVariables")).map(JsonElement::getAsString).orElse(""));
vs.setMaxMemory(maxMemoryN);
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
@@ -767,8 +877,8 @@ public final class VersionSetting implements Cloneable {
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
vs.setJavaDir(Optional.ofNullable(obj.get("javaDir")).map(JsonElement::getAsString).orElse(""));
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse(""));
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
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(""));
@@ -776,14 +886,49 @@ public final class VersionSetting implements Cloneable {
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(LauncherVisibility.HIDE.ordinal())]);
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
vs.setJavaVersionType(javaVersionType);
vs.setJavaVersion(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(null));
} else {
String java = Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("");
switch (java) {
case "Default":
vs.setJavaVersionType(JavaVersionType.DEFAULT);
break;
case "Auto":
vs.setJavaVersionType(JavaVersionType.AUTO);
break;
case "Custom":
vs.setJavaVersionType(JavaVersionType.CUSTOM);
break;
default:
vs.setJavaVersion(java);
}
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
}));
return vs;
}
@@ -800,5 +945,25 @@ public final class VersionSetting implements Cloneable {
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
}
private <E extends Enum<E>> E parseJsonPrimitive(JsonPrimitive primitive, Class<E> clazz, E defaultValue) {
if (primitive == null)
return defaultValue;
else {
E[] enumConstants = clazz.getEnumConstants();
if (primitive.isNumber()) {
int index = primitive.getAsInt();
return index >= 0 && index < enumConstants.length ? enumConstants[index] : defaultValue;
} else {
String name = primitive.getAsString();
for (E enumConstant : enumConstants) {
if (enumConstant.name().equalsIgnoreCase(name)) {
return enumConstant;
}
}
return defaultValue;
}
}
}
}
}
@@ -9,8 +9,8 @@ public final class AwtUtils {
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
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);
@@ -19,22 +19,26 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
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.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountListPage;
@@ -45,26 +49,28 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
public static final Screen SCREEN = Screen.getPrimary();
private static InvalidationListener stageSizeChangeListener;
private static DoubleProperty stageX = new SimpleDoubleProperty();
private static DoubleProperty stageY = new SimpleDoubleProperty();
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
@@ -75,7 +81,7 @@ public final class Controllers {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
@@ -87,11 +93,10 @@ public final class Controllers {
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
accountListPage.accountsProperty().bindContent(Accounts.getAccounts());
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
return accountListPage;
});
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
private Controllers() {
@@ -120,11 +125,6 @@ public final class Controllers {
return rootPage.get();
}
// FXThread
public static MultiplayerPage getMultiplayerPage() {
return multiplayerPage.get();
}
// FXThread
public static LauncherSettingsPage getSettingsPage() {
return settingsPage.get();
@@ -146,21 +146,101 @@ public final class Controllers {
}
public static void onApplicationStop() {
config().setHeight(stageHeight.get());
config().setWidth(stageWidth.get());
stageHeight = null;
stageWidth = null;
stageSizeChangeListener = null;
if (stageX != null) {
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
stageX = null;
}
if (stageY != null) {
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
stageY = null;
}
if (stageHeight != null) {
config().setHeight(stageHeight.get());
stageHeight = null;
}
if (stageWidth != null) {
config().setWidth(stageWidth.get());
stageWidth = null;
}
}
public static void initialize(Stage stage) {
Logging.LOG.info("Start initializing application");
LOG.info("Start initializing application");
Controllers.stage = stage;
stage.setHeight(config().getHeight());
stageHeight.bind(stage.heightProperty());
stage.setWidth(config().getWidth());
stageWidth.bind(stage.widthProperty());
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
DoubleProperty targetProperty;
switch (sourceProperty.getName()) {
case "x": {
targetProperty = stageX;
break;
}
case "y": {
targetProperty = stageY;
break;
}
case "width": {
targetProperty = stageWidth;
break;
}
case "height": {
targetProperty = stageHeight;
break;
}
default: {
targetProperty = null;
}
}
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()) {
targetProperty.set(sourceProperty.get());
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
double initWidth = Math.max(MIN_WIDTH, config().getWidth());
double initHeight = Math.max(MIN_HEIGHT, config().getHeight());
{
double initX = config().getX() * SCREEN.getBounds().getWidth();
double initY = config().getY() * SCREEN.getBounds().getHeight();
boolean invalid = true;
double border = 20D;
for (Screen screen : Screen.getScreens()) {
Rectangle2D bound = screen.getBounds();
if (bound.getMinX() + border <= initX + initWidth && initX <= bound.getMaxX() - border && bound.getMinY() + border <= initY && initY <= bound.getMaxY() - border) {
invalid = false;
break;
}
}
if (invalid) {
initX = (0.5D - initWidth / SCREEN.getBounds().getWidth() / 2) * SCREEN.getBounds().getWidth();
initY = (0.5D - initHeight / SCREEN.getBounds().getHeight() / 2) * SCREEN.getBounds().getHeight();
}
stage.setX(initX);
stage.setY(initY);
stageX.set(initX);
stageY.set(initY);
}
stage.setHeight(initHeight);
stage.setWidth(initWidth);
stageHeight.set(initHeight);
stageWidth.set(initWidth);
stage.xProperty().addListener(weakListener);
stage.yProperty().addListener(weakListener);
stage.heightProperty().addListener(weakListener);
stage.widthProperty().addListener(weakListener);
stage.setOnCloseRequest(e -> Launcher.stopApplication());
@@ -172,27 +252,45 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
Lang.thread(JavaManager::initialize, "Search Java", true);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
stage.setMinHeight(450 + 2 + 40 + 16); // bg height + border width*2 + toolbar height + shadow width*2
stage.setMinWidth(800 + 2 + 16); // bg width + border width*2 + shadow width*2
stage.setMinWidth(MIN_WIDTH);
stage.setMinHeight(MIN_HEIGHT);
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
stage.getIcons().add(newImage("/assets/img/icon.png"));
FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
if (!Architecture.SYSTEM_ARCH.isX86() && globalConfig().getPlatformPromptVersion() < 1) {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.osx_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
(Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64
|| Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW
|| Architecture.SYSTEM_ARCH == Architecture.MIPS64EL)) {
Controllers.dialog(i18n("fatal.unsupported_platform.loongarch"), null, MessageType.INFO, continueAction);
} else {
Controllers.dialog(i18n("fatal.unsupported_platform"), null, MessageType.WARNING, continueAction);
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
agreementPane.setBody(new Label(i18n("launcher.agreement.hint")));
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
agreementLink.setOnAction(e -> FXUtils.openLink(Metadata.EULA_URL));
agreementLink.setExternalLink(Metadata.EULA_URL);
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
yesButton.getStyleClass().add("dialog-accept");
yesButton.setOnAction(e -> {
@@ -201,9 +299,7 @@ public final class Controllers {
});
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
noButton.getStyleClass().add("dialog-cancel");
noButton.setOnAction(e -> {
System.exit(1);
});
noButton.setOnAction(e -> javafx.application.Platform.exit());
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}
@@ -262,11 +358,7 @@ public final class Controllers {
return pane.getCompletableFuture();
}
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, null);
}
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, Consumer<Region> onCancel) {
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setExecutor(executor);
@@ -274,7 +366,7 @@ public final class Controllers {
return pane;
}
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, Consumer<Region> onCancel) {
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
@@ -294,9 +386,11 @@ public final class Controllers {
public static void onHyperlinkAction(String href) {
if (href.startsWith("hmcl://")) {
if ("hmcl://settings/feedback".equals(href)) {
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
switch (href) {
case "hmcl://settings/feedback":
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
break;
}
} else {
FXUtils.openLink(href);
@@ -311,9 +405,14 @@ public final class Controllers {
rootPage = null;
versionPage = null;
gameListPage = null;
downloadPage = null;
accountListPage = null;
settingsPage = null;
decorator = null;
stage = null;
scene = null;
onApplicationStop();
FXUtils.shutdown();
}
}
@@ -27,9 +27,9 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -37,21 +37,23 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public class CrashWindow extends Stage {
public CrashWindow(String text) {
public CrashWindow(CrashReport report) {
Label lblCrash = new Label();
if (UpdateChecker.isOutdated())
lblCrash.setText(i18n("launcher.crash_out_dated"));
if (report.getThrowable() instanceof InternalError)
lblCrash.setText(i18n("launcher.crash.java_internal_error"));
else if (UpdateChecker.isOutdated())
lblCrash.setText(i18n("launcher.crash.hmcl_out_dated"));
else
lblCrash.setText(i18n("launcher.crash"));
lblCrash.setWrapText(true);
TextArea textArea = new TextArea();
textArea.setText(text);
textArea.setText(report.getDisplayText());
textArea.setEditable(false);
Button btnContact = new Button();
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
btnContact.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
@@ -67,10 +69,10 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(newImage("/assets/img/icon.png"));
FXUtils.setIcon(this);
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
setOnCloseRequest(e -> javafx.application.Platform.exit());
}
}
@@ -23,78 +23,112 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
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.fxml.FXMLLoader;
import javafx.beans.value.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Logging;
import org.glavo.png.PNGType;
import org.glavo.png.PNGWriter;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class FXUtils {
private FXUtils() {
}
public static final int JAVAFX_MAJOR_VERSION;
static {
String jfxVersion = System.getProperty("javafx.version");
int majorVersion = -1;
if (jfxVersion != null) {
Matcher matcher = Pattern.compile("^(?<version>[0-9]+)").matcher(jfxVersion);
if (matcher.find()) {
majorVersion = Lang.parseInt(matcher.group(), -1);
}
}
JAVAFX_MAJOR_VERSION = majorVersion;
}
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
public static void shutdown() {
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
try {
Files.deleteIfExists(entry.getValue());
} catch (IOException e) {
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
}
remoteImageCache.remove(entry.getKey());
}
builtinImageCache.clear();
}
public static void runInFX(Runnable runnable) {
if (Platform.isFxApplicationThread()) {
runnable.run();
@@ -275,22 +309,16 @@ public final class FXUtils {
}
public static void smoothScrolling(ScrollPane scrollPane) {
JFXScrollPane.smoothScrolling(scrollPane);
if (AnimationUtils.isAnimationEnabled())
ScrollUtils.addSmoothScrolling(scrollPane);
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
try {
loader.load();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
public static void installFastTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 50, 5000, 0, tooltip);
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installFastTooltip(Node node, String tooltip) {
@@ -298,39 +326,13 @@ public final class FXUtils {
}
public static void installSlowTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 500, 5000, 0, tooltip);
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installSlowTooltip(Node node, String tooltip) {
installSlowTooltip(node, new Tooltip(tooltip));
}
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
runInFX(() -> {
try {
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
try {
// Java 9
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
});
}
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
animationKey = "FXUTILS.ANIMATION." + animationKey;
Object oldTimeline = node.getProperties().get(animationKey);
@@ -357,45 +359,103 @@ public final class FXUtils {
public static void openFolder(File file) {
if (!FileUtils.makeDirectory(file)) {
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
LOG.error("Unable to make directory " + file);
return;
}
String path = file.getAbsolutePath();
switch (OperatingSystem.CURRENT_OS) {
case OSX:
String openCommand;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommand = "explorer.exe";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommand = "/usr/bin/open";
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
openCommand = "/usr/bin/xdg-open";
else
openCommand = null;
thread(() -> {
if (openCommand != null) {
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
// explorer.exe always return 1
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
return;
else
LOG.warning("Open " + path + " failed with code " + exitCode);
} catch (Throwable e) {
LOG.warning("Unable to open " + path + " by executing " + openCommand, e);
}
break;
default:
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
}
});
}
// Fallback to java.awt.Desktop::open
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOG.error("Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
});
}
private static String which(String command) {
String path = System.getenv("PATH");
if (path == null)
return null;
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
try {
Path program = Paths.get(item, command);
if (Files.isExecutable(program))
return program.toRealPath().toString();
} catch (Throwable ignored) {
}
}
return null;
}
public static void showFileInExplorer(Path file) {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
String path = file.toAbsolutePath().toString();
String[] openCommands;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
openCommands = new String[]{
"dbus-send",
"--print-reply",
"--dest=org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
"array:string:" + file.toAbsolutePath().toUri(),
"string:"
};
else
openCommands = null;
if (openCommands != null) {
thread(() -> {
try {
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select", e);
int exitCode = SystemUtils.callExternalProcess(openCommands);
// explorer.exe always return 1
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
return;
else
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
} catch (Throwable e) {
LOG.warning("Unable to show " + path + " in explorer", e);
}
break;
default:
// Currently unsupported.
break;
// Fallback to open folder
openFolder(file.getParent().toFile());
});
} else {
// We do not have a universal method to show file in file manager.
openFolder(file.getParent().toFile());
}
}
@@ -418,96 +478,121 @@ public final class FXUtils {
if (link == null)
return;
if (java.awt.Desktop.isDesktopSupported()) {
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
}
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
return;
} catch (Throwable e) {
LOG.warning("An exception occurred while calling rundll32", e);
}
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
for (String browser : linuxBrowsers) {
String path = which(browser);
if (path != null) {
try {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
}
LOG.warning("No known browser found");
}
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
});
}
}
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
LOG.warning("Unable to open link: " + link, ex);
}
LOG.warning("Failed to open link: " + link, e);
}
});
}
public static void showWebDialog(String title, String content) {
showWebDialog(title, content, 800, 480);
}
public static void showWebDialog(String title, String content, int width, int height) {
try {
WebStage stage = new WebStage(width, height);
stage.getWebView().getEngine().loadContent(content);
stage.setTitle(title);
stage.showAndWait();
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
//noinspection ConstantConditions
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
final JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
frame.add(progressBar, BorderLayout.PAGE_START);
Schedulers.defaultScheduler().execute(() -> {
final JEditorPane pane = new JEditorPane("text/html", content);
pane.setEditable(false);
pane.addHyperlinkListener(event -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
openLink(event.getURL().toExternalForm());
}
});
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
});
});
frame.setVisible(true);
frame.toFront();
});
}
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
textField.focusedProperty().addListener((ChangeListener<Boolean>) listener);
property.addListener(listener);
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
}
public static void unbindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().unbindBidirectional(property);
bind(textField, property, SafeStringConverter.fromInteger());
}
public static void bindString(JFXTextField textField, Property<String> property) {
textField.textProperty().bindBidirectional(property);
bind(textField, property, null);
}
public static void unbindString(JFXTextField textField, Property<String> property) {
textField.textProperty().unbindBidirectional(property);
public static void unbind(JFXTextField textField, Property<?> property) {
TextFieldBindingListener<?> listener = new TextFieldBindingListener<>(textField, property, null);
textField.focusedProperty().removeListener((ChangeListener<Boolean>) listener);
property.removeListener(listener);
}
private static final class TextFieldBindingListener<T> implements ChangeListener<Boolean>, InvalidationListener {
private final int hashCode;
private final WeakReference<JFXTextField> textFieldRef;
private final WeakReference<Property<T>> propertyRef;
private final StringConverter<T> converter;
TextFieldBindingListener(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
this.textFieldRef = new WeakReference<>(textField);
this.propertyRef = new WeakReference<>(property);
this.converter = converter;
this.hashCode = System.identityHashCode(textField) ^ System.identityHashCode(property);
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean focused) { // On TextField changed
JFXTextField textField = textFieldRef.get();
Property<T> property = this.propertyRef.get();
if (textField != null && property != null && oldValue == Boolean.TRUE && focused == Boolean.FALSE) {
if (textField.validate()) {
String newText = textField.getText();
@SuppressWarnings("unchecked")
T newValue = converter == null ? (T) newText : converter.fromString(newText);
if (!Objects.equals(newValue, property.getValue()))
property.setValue(newValue);
} else {
// Rollback to old value
invalidated(null);
}
}
}
@Override
public void invalidated(Observable observable) { // On property change
JFXTextField textField = textFieldRef.get();
Property<T> property = this.propertyRef.get();
if (textField != null && property != null) {
T value = property.getValue();
textField.setText(converter == null ? (String) value : converter.toString(value));
}
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TextFieldBindingListener))
return false;
TextFieldBindingListener<?> other = (TextFieldBindingListener<?>) obj;
return this.hashCode == other.hashCode
&& this.textFieldRef.get() == other.textFieldRef.get()
&& this.propertyRef.get() == other.propertyRef.get();
}
}
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
@@ -533,16 +618,17 @@ public final class FXUtils {
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
public static <T extends Enum<T>> void bindEnum(JFXComboBox<T> comboBox, Property<T> property) {
unbindEnum(comboBox);
@SuppressWarnings("rawtypes")
ChangeListener<Number> listener = (a, b, newValue) ->
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
comboBox.getSelectionModel().select(property.getValue().ordinal());
T currentValue = property.getValue();
@SuppressWarnings("unchecked")
T[] enumConstants = (T[]) currentValue.getClass().getEnumConstants();
ChangeListener<Number> listener = (a, b, newValue) -> property.setValue(enumConstants[newValue.intValue()]);
comboBox.getSelectionModel().select(currentValue.ordinal());
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
}
@@ -553,15 +639,78 @@ public final class FXUtils {
*
* @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)}
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void unbindEnum(JFXComboBox<?> comboBox) {
@SuppressWarnings("rawtypes")
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
public static void unbindEnum(JFXComboBox<? extends Enum<?>> comboBox) {
@SuppressWarnings("unchecked")
ChangeListener<Number> listener = (ChangeListener<Number>) comboBox.getProperties().remove("FXUtils.bindEnum.listener");
if (listener != null)
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
int itemCount = children.length;
int childSelectedCount = 0;
for (BooleanProperty child : children) {
if (child.get())
childSelectedCount++;
}
allEnabled.set(childSelectedCount == itemCount);
class Listener implements InvalidationListener {
private int childSelectedCount;
private boolean updating = false;
public Listener(int childSelectedCount) {
this.childSelectedCount = childSelectedCount;
}
@Override
public void invalidated(Observable observable) {
if (updating)
return;
updating = true;
try {
boolean value = ((BooleanProperty) observable).get();
if (observable == allEnabled) {
for (BooleanProperty child : children) {
child.setValue(value);
}
childSelectedCount = value ? itemCount : 0;
} else {
if (value)
childSelectedCount++;
else
childSelectedCount--;
allEnabled.set(childSelectedCount == itemCount);
}
} finally {
updating = false;
}
}
}
InvalidationListener listener = new Listener(childSelectedCount);
WeakInvalidationListener weakListener = new WeakInvalidationListener(listener);
allEnabled.addListener(listener);
for (BooleanProperty child : children) {
child.addListener(weakListener);
}
}
public static void setIcon(Stage stage) {
String icon;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
icon = "/assets/img/icon.png";
} else {
icon = "/assets/img/icon@4x.png";
}
stage.getIcons().add(newBuiltinImage(icon));
}
/**
@@ -572,14 +721,138 @@ public final class FXUtils {
* @see org.jackhuang.hmcl.util.CrashReporter
* @see ResourceNotFoundError
*/
public static Image newImage(String url) {
public static Image newBuiltinImage(String url) {
try {
return new Image(url);
return builtinImageCache.computeIfAbsent(url, Image::new);
} catch (IllegalArgumentException e) {
throw new ResourceNotFoundError("Cannot access image: " + url, e);
}
}
/**
* 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.
* @param requestedWidth the image's bounding box width
* @param requestedHeight the image's bounding box height
* @param preserveRatio indicates whether to preserve the aspect ratio of
* the original image when scaling to fit the image within the
* specified bounding box
* @param smooth indicates whether to use a better quality filtering
* algorithm or a faster one when scaling this image to fit within
* the specified bounding box
* @return the image resource within the jar.
* @see org.jackhuang.hmcl.util.CrashReporter
* @see ResourceNotFoundError
*/
public static Image newBuiltinImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
try {
return new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth);
} catch (IllegalArgumentException e) {
throw new ResourceNotFoundError("Cannot access image: " + url, e);
}
}
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @return the image resource within the jar.
*/
public static Image newRemoteImage(String url) {
return newRemoteImage(url, 0, 0, false, false, false);
}
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @param requestedWidth the image's bounding box width
* @param requestedHeight the image's bounding box height
* @param preserveRatio indicates whether to preserve the aspect ratio of
* the original image when scaling to fit the image within the
* specified bounding box
* @param smooth indicates whether to use a better quality filtering
* algorithm or a faster one when scaling this image to fit within
* the specified bounding box
* @return the image resource within the jar.
*/
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
Path currentPath = remoteImageCache.get(url);
if (currentPath != null) {
if (Files.isReadable(currentPath)) {
try (InputStream inputStream = Files.newInputStream(currentPath)) {
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
} catch (IOException e) {
LOG.warning("An exception encountered while reading data from cached image file.", e);
}
}
// The file is unavailable or unreadable.
remoteImageCache.remove(url);
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
image.progressProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
Task.runAsync(() -> {
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
OutputStream outputStream = Files.newOutputStream(newPath);
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
) {
writer.write(PNGJavaFXUtils.asArgbImage(image));
} catch (IOException e) {
try {
Files.delete(newPath);
} catch (IOException e2) {
e2.addSuppressed(e);
throw e2;
}
throw e;
}
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
}
}).start();
}
});
return image;
}
public static JFXButton newRaisedButton(String text) {
JFXButton button = new JFXButton(text);
button.getStyleClass().add("jfx-button-raised");
button.setButtonType(JFXButton.ButtonType.RAISED);
return button;
}
public static JFXButton newBorderButton(String text) {
JFXButton button = new JFXButton(text);
button.getStyleClass().add("jfx-button-border");
button.setButtonType(JFXButton.ButtonType.RAISED);
return button;
}
public static Label truncatedLabel(String text, int limit) {
Label label = new Label();
if (text.length() <= limit) {
label.setText(text);
} else {
label.setText(StringUtils.truncate(text, limit));
installFastTooltip(label, text);
}
return label;
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -624,10 +897,17 @@ public final class FXUtils {
}
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
Holder<Object> lastCell = new Holder<>();
return view -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.value && !isVisible())
return;
lastCell.value = this;
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(graphicBuilder.apply(item));
@@ -661,13 +941,6 @@ public final class FXUtils {
}
};
public static Runnable withJFXPopupClosing(Runnable runnable, JFXPopup popup) {
return () -> {
runnable.run();
popup.hide();
};
}
public static void onEscPressed(Node node, Runnable action) {
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
@@ -677,20 +950,13 @@ public final class FXUtils {
});
}
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
public static WritableImage toFXImage(BufferedImage image) {
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
PixelWriter pw = wr.getPixelWriter();
final int iw = image.getWidth();
final int ih = image.getHeight();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
public static void onClicked(Node node, Runnable action) {
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 1) {
action.run();
e.consume();
}
}
return wr;
});
}
public static void copyText(String text) {
@@ -698,10 +964,15 @@ public final class FXUtils {
content.putString(text);
Clipboard.getSystemClipboard().setContent(content);
Controllers.showToast(i18n("message.copied"));
if (!Controllers.isStopped()) {
Controllers.showToast(i18n("message.copied"));
}
}
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
if (segment.indexOf('<') < 0)
return Collections.singletonList(new Text(segment));
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
@@ -709,7 +980,7 @@ public final class FXUtils {
Element r = doc.getDocumentElement();
NodeList children = r.getChildNodes();
List<javafx.scene.Node> texts = new ArrayList<>();
List<Node> texts = new ArrayList<>();
for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node node = children.item(i);
@@ -717,9 +988,19 @@ public final class FXUtils {
Element element = (Element) node;
if ("a".equals(element.getTagName())) {
String href = element.getAttribute("href");
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
texts.add(hyperlink);
Text text = new Text(element.getTextContent());
onClicked(text, () -> {
String link = href;
try {
link = new URI(href).toASCIIString();
} catch (URISyntaxException ignored) {
}
hyperlinkAction.accept(link);
});
text.setCursor(Cursor.HAND);
text.setFill(Color.web("#0070E0"));
text.setUnderline(true);
texts.add(text);
} else if ("b".equals(element.getTagName())) {
Text text = new Text(element.getTextContent());
text.getStyleClass().add("bold");
@@ -735,7 +1016,7 @@ public final class FXUtils {
}
return texts;
} catch (SAXException | ParserConfigurationException | IOException e) {
LOG.log(Level.WARNING, "Failed to parse xml", e);
LOG.warning("Failed to parse xml", e);
return Collections.singletonList(new Text(segment));
}
}
@@ -746,4 +1027,11 @@ public final class FXUtils {
return tf;
}
public static String toWeb(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);
return String.format("#%02x%02x%02x", r, g, b);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2023 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,60 +20,64 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameCrashWindow extends Stage {
private final Version version;
private final String memory;
private final String total_memory;
private final String java;
private final LibraryAnalyzer analyzer;
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
private final TextFlow feedbackTextFlow = new TextFlow();
private final ManagedProcess managedProcess;
private final DefaultGameRepository repository;
@@ -81,107 +85,147 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;
private final List<Pair<String, Log4jLevel>> logs;
private final List<Log> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
this.version = version;
this.launchOptions = launchOptions;
this.logs = logs;
this.analyzer = LibraryAnalyzer.analyze(version);
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
total_memory = OperatingSystem.TOTAL_MEMORY + " MB";
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
? launchOptions.getJava().getVersion()
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
this.view = new View();
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
setScene(new Scene(view, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("game.crash.title"));
getIcons().add(newImage("/assets/img/icon.png"));
FXUtils.setIcon(this);
analyzeCrashReport();
}
@SuppressWarnings("unchecked")
private void analyzeCrashReport() {
loading.set(true);
CompletableFuture.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
Set<String> keywords = Collections.emptySet();
Task.allOf(Task.supplyAsync(() -> {
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
// Get the crash-report from the crash-reports/xxx, or the output of console.
String crashReport = null;
try {
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read crash report", e);
LOG.warning("Failed to read crash report", e);
}
if (crashReport == null) {
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
}
if (crashReport != null) {
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
return pair(CrashReportAnalyzer.anaylze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
}), Task.supplyAsync(() -> {
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
if (!Files.isReadable(latestLog)) {
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
return pair(
CrashReportAnalyzer.anaylze(rawLog),
keywords);
}).whenCompleteAsync((pair, exception) -> {
String log;
try {
log = FileUtils.readText(latestLog);
} catch (IOException e) {
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
return pair(CrashReportAnalyzer.anaylze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
})).whenComplete(Schedulers.javafx(), (taskResult, exception) -> {
loading.set(false);
if (exception != null) {
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
reason.set(i18n("game.crash.reason.unknown"));
LOG.warning("Failed to analyze crash report", exception);
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
} else {
List<CrashReportAnalyzer.Result> results = pair.getKey();
Set<String> keywords = pair.getValue();
StringBuilder reasonText = new StringBuilder();
for (CrashReportAnalyzer.Result result : results) {
EnumMap<CrashReportAnalyzer.Rule, CrashReportAnalyzer.Result> results = new EnumMap<>(CrashReportAnalyzer.Rule.class);
Set<String> keywords = new HashSet<>();
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
for (CrashReportAnalyzer.Result result : pair.getKey()) {
results.put(result.getRule(), result);
}
keywords.addAll(pair.getValue());
}
List<Node> segments = new ArrayList<>(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
LOG.info("Number of reasons: " + results.size());
if (results.size() > 1) {
segments.add(new Text("\n"));
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.multiple"), Controllers::onHyperlinkAction));
} else {
segments.add(new Text("\n\n"));
}
for (CrashReportAnalyzer.Result result : results.values()) {
String message;
switch (result.getRule()) {
case TOO_OLD_JAVA:
reasonText.append(i18n("game.crash.reason.too_old_java",
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))))
.append("\n");
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
break;
case MOD_RESOLUTION_CONFLICT:
case MOD_RESOLUTION_MISSING:
case MOD_RESOLUTION_COLLECTION:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("sourcemod")),
parseFabricModId(result.getMatcher().group("destmod")),
parseFabricModId(result.getMatcher().group("destmod"))));
parseFabricModId(result.getMatcher().group("destmod")));
break;
case MOD_RESOLUTION_MISSING_MINECRAFT:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("mod")),
result.getMatcher().group("version")));
result.getMatcher().group("version"));
break;
case MOD_FOREST_OPTIFINE:
case TWILIGHT_FOREST_OPTIFINE:
reasonText.append(i18n("game.crash.reason.mod", "OptiFine"));
case PERFORMANT_FOREST_OPTIFINE:
case JADE_FOREST_OPTIFINE:
case NEOFORGE_FOREST_OPTIFINE:
message = i18n("game.crash.reason.mod", "OptiFine");
LOG.info("Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
break;
default:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
.toArray()))
.append("\n");
.toArray());
break;
}
LOG.info("Crash cause: " + result.getRule() + ": " + message);
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
segments.add(new Text("\n\n"));
}
if (results.isEmpty()) {
if (!keywords.isEmpty()) {
reason.set(i18n("game.crash.reason.stacktrace", String.join(", ", keywords)));
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.stacktrace", String.join(", ", keywords))));
LOG.info("Crash reason unknown, but some log keywords have been found: " + String.join(", ", keywords));
} else {
reason.set(i18n("game.crash.reason.unknown"));
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
LOG.info("Crash reason unknown");
}
feedbackLabel.setVisible(true);
} else {
reason.set(reasonText.toString());
feedbackTextFlow.setVisible(false);
reasonTextFlow.getChildren().setAll(segments);
}
}
}, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException);
}).start();
}
private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?<modid>.*?) @ (?<version>.*?)}");
@@ -214,36 +258,36 @@ public class GameCrashWindow extends Stage {
}
private void showLogWindow() {
LogWindow logWindow = new LogWindow();
LogWindow logWindow = new LogWindow(managedProcess);
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
logWindow.showNormal();
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
if (managedProcess.getClasspath() != null)
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
logWindow.logLines(logs);
logWindow.show();
}
private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
CompletableFuture.supplyAsync(() ->
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.thenRunAsync(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
.handleAsync((result, exception) -> {
Alert alert;
if (exception == null) {
FXUtils.showFileInExplorer(logFile);
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
} else {
LOG.warning("Failed to export game crash info", exception);
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
}
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
}, Schedulers.javafx())
.exceptionally(e -> {
LOG.log(Level.WARNING, "Failed to export game crash info", e);
return null;
});
}
@@ -265,6 +309,9 @@ public class GameCrashWindow extends Stage {
case APPLICATION_ERROR:
title.setText(i18n("launch.failed.exited_abnormally"));
break;
case SIGKILL:
title.setText(i18n("launch.failed.sigkill"));
break;
}
titlePane.setAlignment(Pos.CENTER);
@@ -277,11 +324,21 @@ public class GameCrashWindow extends Stage {
infoPane.setPadding(new Insets(8));
infoPane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem launcher = new TwoLineListItem();
launcher.getStyleClass().setAll("two-line-item-second-large");
launcher.setTitle(i18n("launcher"));
launcher.setSubtitle(Metadata.VERSION);
TwoLineListItem version = new TwoLineListItem();
version.getStyleClass().setAll("two-line-item-second-large");
version.setTitle(i18n("archive.game_version"));
version.setTitle(i18n("game.version"));
version.setSubtitle(GameCrashWindow.this.version.getId());
TwoLineListItem total_memory = new TwoLineListItem();
total_memory.getStyleClass().setAll("two-line-item-second-large");
total_memory.setTitle(i18n("settings.physical_memory"));
total_memory.setSubtitle(GameCrashWindow.this.total_memory);
TwoLineListItem memory = new TwoLineListItem();
memory.getStyleClass().setAll("two-line-item-second-large");
memory.setTitle(i18n("settings.memory"));
@@ -295,14 +352,14 @@ public class GameCrashWindow extends Stage {
TwoLineListItem os = new TwoLineListItem();
os.getStyleClass().setAll("two-line-item-second-large");
os.setTitle(i18n("system.operating_system"));
os.subtitleProperty().bind(GameCrashWindow.this.os);
os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
TwoLineListItem arch = new TwoLineListItem();
arch.getStyleClass().setAll("two-line-item-second-large");
arch.setTitle(i18n("system.architecture"));
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
infoPane.getChildren().setAll(version, memory, java, os, arch);
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
}
HBox moddedPane = new HBox(8);
@@ -311,13 +368,15 @@ public class GameCrashWindow extends Stage {
moddedPane.setAlignment(Pos.CENTER_LEFT);
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
analyzer.getVersion(type).ifPresent(ver -> {
TwoLineListItem item = new TwoLineListItem();
item.getStyleClass().setAll("two-line-item-second-large");
item.setTitle(i18n("install.installer." + type.getPatchId()));
item.setSubtitle(ver);
moddedPane.getChildren().add(item);
});
if (!type.getPatchId().isEmpty()) {
analyzer.getVersion(type).ifPresent(ver -> {
TwoLineListItem item = new TwoLineListItem();
item.getStyleClass().setAll("two-line-item-second-large");
item.setTitle(i18n("install.installer." + type.getPatchId()));
item.setSubtitle(ver);
moddedPane.getChildren().add(item);
});
}
}
}
@@ -327,38 +386,50 @@ public class GameCrashWindow extends Stage {
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
TwoLineListItem javaDir = new TwoLineListItem();
javaDir.getStyleClass().setAll("two-line-item-second-large");
javaDir.setTitle(i18n("settings.game.java_directory"));
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory"));
TwoLineListItem reason = new TwoLineListItem();
reason.getStyleClass().setAll("two-line-item-second-large", "wrap-text");
reason.setTitle(i18n("game.crash.reason"));
reason.subtitleProperty().bind(GameCrashWindow.this.reason);
Label reasonTitle = new Label(i18n("game.crash.reason"));
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
reasonPane.setFitToWidth(true);
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
gameDirPane.getChildren().setAll(gameDir, javaDir, reason);
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
if (visible) {
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane, feedbackTextFlow));
} else {
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane));
}
});
}
HBox toolBar = new HBox();
{
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setButtonType(JFXButton.ButtonType.RAISED);
exportGameCrashInfoButton.getStyleClass().add("jfx-button-raised");
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnAction(e -> showLogWindow());
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
JFXButton logButton = new JFXButton(i18n("logwindow.title"));
logButton.setButtonType(JFXButton.ButtonType.RAISED);
logButton.getStyleClass().add("jfx-button-raised");
logButton.setOnMouseClicked(e -> showLogWindow());
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, feedbackLabel);
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
@@ -0,0 +1,266 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Cursor;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HTMLRenderer {
private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
return null;
try {
return new URI(href);
} catch (Throwable e) {
return null;
}
}
private final List<javafx.scene.Node> children = new ArrayList<>();
private final List<Node> stack = new ArrayList<>();
private boolean bold;
private boolean italic;
private boolean underline;
private boolean strike;
private boolean highlight;
private String headerLevel;
private Node hyperlink;
private final Consumer<URI> onClickHyperlink;
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
this.onClickHyperlink = onClickHyperlink;
}
private void updateStyle() {
bold = false;
italic = false;
underline = false;
strike = false;
highlight = false;
headerLevel = null;
hyperlink = null;
for (Node node : stack) {
String nodeName = node.nodeName();
switch (nodeName) {
case "b":
case "strong":
bold = true;
break;
case "i":
case "em":
italic = true;
break;
case "ins":
underline = true;
break;
case "del":
strike = true;
break;
case "mark":
highlight = true;
break;
case "a":
hyperlink = node;
break;
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
headerLevel = nodeName;
break;
}
}
}
private void pushNode(Node node) {
stack.add(node);
updateStyle();
}
private void popNode() {
stack.remove(stack.size() - 1);
updateStyle();
}
private void applyStyle(Text text) {
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
FXUtils.onClicked(text, () -> onClickHyperlink.accept(target));
text.setCursor(Cursor.HAND);
}
text.getStyleClass().add("html-hyperlink");
}
if (hyperlink != null || underline)
text.setUnderline(true);
if (strike)
text.setStrikethrough(true);
if (bold || highlight)
text.getStyleClass().add("html-bold");
if (italic)
text.getStyleClass().add("html-italic");
if (headerLevel != null)
text.getStyleClass().add("html-" + headerLevel);
}
private void appendText(String text) {
Text textNode = new Text(text);
applyStyle(textNode);
children.add(textNode);
}
private void appendImage(Node node) {
String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt");
if (imageUri != null) {
URI uri = URI.create(src);
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
double width = 0;
double height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
if (width <= 0 || height <= 0) {
width = 0;
height = 0;
}
}
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
FXUtils.onClicked(imageView, () -> onClickHyperlink.accept(target));
imageView.setCursor(Cursor.HAND);
}
}
children.add(imageView);
return;
}
}
if (!alt.isEmpty())
appendText(alt);
}
public void appendNode(Node node) {
if (node instanceof TextNode) {
appendText(((TextNode) node).text());
}
String name = node.nodeName();
switch (name) {
case "img":
appendImage(node);
break;
case "li":
appendText("\n \u2022 ");
break;
case "dt":
appendText(" ");
break;
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
case "tr":
if (!children.isEmpty())
appendText("\n\n");
break;
}
if (node.childNodeSize() > 0) {
pushNode(node);
for (Node childNode : node.childNodes()) {
appendNode(childNode);
}
popNode();
}
switch (name) {
case "br":
case "dd":
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
appendText("\n");
break;
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
}
@@ -18,9 +18,11 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.EventHandler;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
@@ -29,16 +31,20 @@ 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 javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -48,183 +54,389 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
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 StringProperty dependencyName = 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<>();
private final VersionIconType iconType;
private final Style style;
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
public InstallerItem(LibraryAnalyzer.LibraryType id) {
this(id.getPatchId());
private final ObjectProperty<Runnable> onInstall = new SimpleObjectProperty<>(this, "onInstall");
private final ObjectProperty<Runnable> onRemove = new SimpleObjectProperty<>(this, "onRemove");
public interface State {
}
public InstallerItem(String id) {
public static final class InstallableState implements State {
public static final InstallableState INSTANCE = new InstallableState();
private InstallableState() {
}
}
public static final class IncompatibleState implements State {
private final String incompatibleItemName;
private final String incompatibleItemVersion;
public IncompatibleState(String incompatibleItemName, String incompatibleItemVersion) {
this.incompatibleItemName = incompatibleItemName;
this.incompatibleItemVersion = incompatibleItemVersion;
}
public String getIncompatibleItemName() {
return incompatibleItemName;
}
public String getIncompatibleItemVersion() {
return incompatibleItemVersion;
}
}
public static final class InstalledState implements State {
private final String version;
private final boolean external;
private final boolean incompatibleWithGame;
public InstalledState(String version, boolean external, boolean incompatibleWithGame) {
this.version = version;
this.external = external;
this.incompatibleWithGame = incompatibleWithGame;
}
public String getVersion() {
return version;
}
public boolean isExternal() {
return external;
}
public boolean isIncompatibleWithGame() {
return incompatibleWithGame;
}
}
public enum Style {
LIST_ITEM,
CARD,
}
public InstallerItem(LibraryAnalyzer.LibraryType id, Style style) {
this(id.getPatchId(), style);
}
public InstallerItem(String id, Style style) {
this.id = id;
this.style = style;
switch (id) {
case "game":
imageUrl = "/assets/img/grass.png";
iconType = VersionIconType.GRASS;
break;
case "fabric":
case "fabric-api":
imageUrl = "/assets/img/fabric.png";
iconType = VersionIconType.FABRIC;
break;
case "forge":
imageUrl = "/assets/img/forge.png";
iconType = VersionIconType.FORGE;
break;
case "liteloader":
imageUrl = "/assets/img/chicken.png";
iconType = VersionIconType.CHICKEN;
break;
case "optifine":
imageUrl = "/assets/img/command.png";
iconType = VersionIconType.OPTIFINE;
break;
case "quilt":
case "quilt-api":
iconType = VersionIconType.QUILT;
break;
case "neoforge":
iconType = VersionIconType.NEO_FORGE;
break;
default:
imageUrl = null;
iconType = null;
break;
}
}
public void setState(String libraryVersion, boolean incompatibleWithGame, boolean removable) {
this.libraryVersion.set(libraryVersion);
this.incompatibleWithGame.set(incompatibleWithGame);
this.removable.set(removable);
}
public String getLibraryId() {
return id;
}
public ObjectProperty<InstalledState> versionProperty() {
return versionProperty;
}
public ObjectProperty<State> resolvedStateProperty() {
return resolvedStateProperty;
}
public ObjectProperty<Runnable> onInstallProperty() {
return onInstall;
}
public Runnable getOnInstall() {
return onInstall.get();
}
public void setOnInstall(Runnable onInstall) {
this.onInstall.set(onInstall);
}
public ObjectProperty<Runnable> onRemoveProperty() {
return onRemove;
}
public Runnable getOnRemove() {
return onRemove.get();
}
public void setOnRemove(Runnable onRemove) {
this.onRemove.set(onRemove);
}
@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 fabricApi = new InstallerItem(FABRIC_API);
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final static class InstallerItemGroup {
private final InstallerItem game;
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
private final InstallerItem[] libraries;
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
private Set<InstallerItem> getIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item) {
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
}
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
private void addIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item, InstallerItem... others) {
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
for (InstallerItem other : others) {
set.add(other);
getIncompatibles(incompatibleMap, other).add(item);
}
}
for (InstallerItem fabric : new InstallerItem[]{fabric, fabricApi}) {
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
return null;
}, optiFine.libraryVersion, forge.libraryVersion));
private void mutualIncompatible(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem... items) {
for (InstallerItem item : items) {
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
for (InstallerItem item2 : items) {
if (item2 != item) {
set.add(item2);
}
}
}
}
public InstallerItemGroup(String gameVersion, Style style) {
game = new InstallerItem(MINECRAFT, style);
InstallerItem fabric = new InstallerItem(FABRIC, style);
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
InstallerItem forge = new InstallerItem(FORGE, style);
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
InstallerItem quilt = new InstallerItem(QUILT, style);
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
InstallerItem item = entry.getKey();
Set<InstallerItem> incompatibleItems = entry.getValue();
Observable[] bindings = new Observable[incompatibleItems.size() + 1];
bindings[0] = item.versionProperty;
int i = 1;
for (InstallerItem other : incompatibleItems) {
bindings[i++] = other.versionProperty;
}
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
InstalledState itemVersion = item.versionProperty.get();
if (itemVersion != null) {
return itemVersion;
}
for (InstallerItem other : incompatibleItems) {
InstalledState otherVersion = other.versionProperty.get();
if (otherVersion != null) {
return new IncompatibleState(other.id, otherVersion.version);
}
}
return InstallableState.INSTANCE;
}, bindings));
}
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() == null) return FABRIC.getPatchId();
else return null;
}, fabric.libraryVersion));
if (gameVersion != null) {
game.versionProperty.set(new InstalledState(gameVersion, false, false));
}
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
for (InstallerItem item : all) {
if (!item.resolvedStateProperty.isBound()) {
item.resolvedStateProperty.bind(item.versionProperty);
}
}
if (gameVersion == null) {
this.libraries = all;
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
} else {
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
}
}
public InstallerItem getGame() {
return game;
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi};
return libraries;
}
}
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
private static final WeakListenerHolder holder = new WeakListenerHolder();
InstallerItemSkin(InstallerItem control) {
super(control);
HBox hbox = new HBox();
hbox.getStyleClass().add("md-list-cell");
hbox.setPadding(new Insets(8));
RipplerContainer container = new RipplerContainer(hbox);
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
} else {
pane = new HBox();
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
getChildren().setAll(container);
hbox.setAlignment(Pos.CENTER_LEFT);
pane.pseudoClassStateChanged(LIST_ITEM, control.style == Style.LIST_ITEM);
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
if (control.imageUrl != null) {
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
if (control.iconType != null) {
ImageView view = new ImageView(control.iconType.getIcon());
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
hbox.getChildren().add(node);
node.getStyleClass().add("installer-item-image");
pane.getChildren().add(node);
if (control.style == Style.CARD) {
VBox.setMargin(node, new Insets(8, 0, 16, 0));
}
}
Label nameLabel = new Label();
nameLabel.getStyleClass().add("installer-item-name");
nameLabel.setMouseTransparent(true);
hbox.getChildren().add(nameLabel);
nameLabel.setPrefWidth(80);
pane.getChildren().add(nameLabel);
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
Label label = new Label();
label.setMouseTransparent(true);
hbox.getChildren().add(label);
label.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(label, Priority.ALWAYS);
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) {
Label statusLabel = new Label();
statusLabel.getStyleClass().add("installer-item-status");
statusLabel.setMouseTransparent(true);
pane.getChildren().add(statusLabel);
HBox.setHgrow(statusLabel, Priority.ALWAYS);
statusLabel.textProperty().bind(Bindings.createStringBinding(() -> {
State state = control.resolvedStateProperty.get();
if (state instanceof InstalledState) {
InstalledState s = (InstalledState) state;
if (s.incompatibleWithGame) {
return i18n("install.installer.change_version", s.version);
}
if (s.external) {
return i18n("install.installer.external_version", s.version);
}
return i18n("install.installer.version", s.version);
} else if (state instanceof InstallableState) {
return i18n("install.installer.not_installed");
} else if (state instanceof IncompatibleState) {
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
} else {
return i18n("install.installer.version", version);
throw new AssertionError("Unknown state type: " + state.getClass());
}
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
}, control.resolvedStateProperty));
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, 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);
HBox buttonsContainer = new HBox();
buttonsContainer.setSpacing(8);
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
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);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
removeButton.getStyleClass().add("toggle-icon4");
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.setOnAction(e -> {
Runnable onRemove = control.getOnRemove();
if (onRemove != null)
onRemove.run();
});
buttonsContainer.getChildren().add(removeButton);
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
JFXButton installButton = new JFXButton();
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
control.resolvedStateProperty.get() instanceof InstallableState ?
SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1) :
SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1),
control.resolvedStateProperty
));
installButton.getStyleClass().add("toggle-icon4");
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
if (control.getOnInstall() == null) {
return false;
}
State state = control.resolvedStateProperty.get();
if (state instanceof InstallableState) {
return true;
}
if (state instanceof InstalledState) {
return !((InstalledState) state).external;
}
return false;
}, control.resolvedStateProperty, control.onInstall));
installButton.managedProperty().bind(installButton.visibleProperty());
installButton.setOnAction(e -> {
Runnable onInstall = control.getOnInstall();
if (onInstall != null)
onInstall.run();
});
buttonsContainer.getChildren().add(installButton);
FXUtils.onChangeAndOperate(installButton.visibleProperty(), clickable -> {
if (clickable) {
container.onMouseClickedProperty().bind(control.action);
hbox.setCursor(Cursor.HAND);
container.setOnMouseClicked(event -> {
Runnable onInstall = control.getOnInstall();
if (onInstall != null && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
onInstall.run();
event.consume();
}
});
pane.setCursor(Cursor.HAND);
} else {
container.onMouseClickedProperty().unbind();
container.onMouseClickedProperty().set(null);
hbox.setCursor(Cursor.DEFAULT);
container.setOnMouseClicked(null);
pane.setCursor(Cursor.DEFAULT);
}
});
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -56,7 +55,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
}
VBox vBox = new VBox();
@@ -70,16 +69,16 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
btnAdd.setGraphic(SVG.PLUS.createIcon(Theme.whiteFill(), -1, -1));
btnAdd.setOnAction(e -> skinnable.add());
JFXButton btnRefresh = new JFXButton();
FXUtils.setLimitWidth(btnRefresh, 40);
FXUtils.setLimitHeight(btnRefresh, 40);
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1, -1));
btnRefresh.setOnAction(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
@@ -25,165 +25,163 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayDeque;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
*
* @author huangyuhui
*/
public final class LogWindow extends Stage {
private final ArrayDeque<Log> logs = new ArrayDeque<>();
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
private final CircularArrayList<Log> logs;
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
{
for (Log4jLevel level : Log4jLevel.values()) {
levelCountMap.put(level, new SimpleIntegerProperty());
levelShownMap.put(level, new SimpleBooleanProperty(true));
}
};
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) {
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
put(level, property);
}
}
};
private final LogWindowImpl impl = new LogWindowImpl();
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
private boolean stopCheckLogCount = false;
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
getIcons().add(newImage("/assets/img/icon.png"));
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
}
public void logLine(String line, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(line), level);
private final LogWindowImpl impl;
private final ManagedProcess gameProcess;
public LogWindow(ManagedProcess gameProcess) {
this(gameProcess, new CircularArrayList<>());
}
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
this.logs = logs;
this.impl = new LogWindowImpl();
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
for (SimpleBooleanProperty property : levelShownMap.values()) {
property.addListener(o -> shakeLogs());
}
this.gameProcess = gameProcess;
}
public void logLine(Log log) {
Log4jLevel level = log.getLevel();
logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
if (!stopCheckLogCount) checkLogCount();
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
checkLogCount();
autoScroll();
}
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
this.impl.showCrashReport.set(true);
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
Platform.runLater(() -> {
impl.listView.scrollTo(log);
});
break;
}
public void logLines(List<Log> logs) {
for (Log log : logs) {
Log4jLevel level = log.getLevel();
this.logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
}
show();
}
public void showNormal() {
this.impl.showCrashReport.set(false);
show();
checkLogCount();
autoScroll();
}
private void shakeLogs() {
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList()));
autoScroll();
}
private void checkLogCount() {
while (logs.size() > config().getLogLines()) {
int nRemove = logs.size() - Log.getLogLines();
if (nRemove <= 0)
return;
ObservableList<Log> items = impl.listView.getItems();
int itemsSize = items.size();
int count = 0;
for (int i = 0; i < nRemove; i++) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
// TODO: fix O(n)
impl.listView.getItems().remove(0);
}
if (itemsSize > count && items.get(count) == removedLog)
count++;
}
items.remove(0, count);
}
private static class Log {
private final String log;
private final Log4jLevel level;
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
private void autoScroll() {
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
}
public class LogWindowImpl extends Control {
private final class LogWindowImpl extends Control {
private ListView<Log> listView = new JFXListView<>();
private BooleanProperty autoScroll = new SimpleBooleanProperty();
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private JFXComboBox<String> cboLines = new JFXComboBox<>();
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
LogWindowImpl() {
getStyleClass().add("log-window");
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
});
for (int i = 0; i < LEVELS.length; i++) {
buttonText[i] = new SimpleStringProperty();
showLevel[i] = new SimpleBooleanProperty(true);
}
if (!flag)
cboLines.getSelectionModel().select(0);
cboLines.getItems().setAll(500, 2000, 5000, 10000);
cboLines.setValue(Log.getLogLines());
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
for (int i = 0; i < 5; ++i) {
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
levelShownMap.get(levels[i]).bind(showLevel.get(i));
for (int i = 0; i < LEVELS.length; ++i) {
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
}
}
@@ -200,25 +198,47 @@ public final class LogWindow extends Stage {
thread(() -> {
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList()));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to export logs", e);
LOG.warning("Failed to export logs", e);
return;
}
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
});
FXUtils.showFileInExplorer(logFile);
});
}
private void onExportGameCrashInfo() {
if (exportGameCrashInfoCallback == null) return;
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
private void onExportDump(SpinnerPane pane) {
assert SystemUtils.supportJVMAttachment();
pane.setLoading(true);
thread(() -> {
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> pane.setLoading(false));
});
}
@Override
@@ -227,26 +247,19 @@ public final class LogWindow extends Stage {
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + backgroundColor + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(buttonText);
button.setSelected(true);
showLevel.bind(button.selectedProperty());
return button;
}
private final Set<ListCell<Log>> selected = new HashSet<>();
protected LogWindowSkin(LogWindowImpl control) {
LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
@@ -271,13 +284,16 @@ public final class LogWindow extends Stage {
{
HBox hBox = new HBox(3);
hBox.getChildren().setAll(
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
);
for (int i = 0; i < LEVELS.length; i++) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(control.buttonText[i]);
button.setSelected(true);
control.showLevel[i].bind(button.selectedProperty());
hBox.getChildren().add(button);
}
borderPane.setRight(hBox);
}
@@ -290,11 +306,14 @@ public final class LogWindow extends Stage {
if (!listView.getItems().isEmpty() && control.autoScroll.get())
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
listView.setStyle("-fx-font-family: " + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
+ "; -fx-font-size: " + config().getFontSize() + "px;");
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region)listView.lookup(".clipped-container");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
@@ -302,26 +321,76 @@ public final class LogWindow extends Stage {
setPadding(new Insets(2));
setWrapText(true);
setGraphic(null);
setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
if (!event.isControlDown()) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().setSelected(false);
}
}
}
selected.clear();
}
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().setSelected(true);
}
event.consume();
});
}
@Override
protected void updateItem(Log item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.value && !isVisible())
return;
lastCell.value = this;
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
if (empty) {
setText(null);
} else {
setText(item.log);
setText(item.getLog());
}
}
});
listView.setOnKeyPressed(event -> {
if (event.isControlDown() && event.getCode() == KeyCode.C) {
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
}
}
FXUtils.copyText(stringBuilder.toString());
}
});
VBox.setVgrow(listView, Priority.ALWAYS);
vbox.getChildren().add(listView);
}
@@ -329,11 +398,6 @@ public final class LogWindow extends Stage {
{
BorderPane bottom = new BorderPane();
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
bottom.setLeft(exportGameCrashInfoButton);
HBox hBox = new HBox(3);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
@@ -343,15 +407,25 @@ public final class LogWindow extends Stage {
autoScrollCheckBox.setSelected(true);
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
SpinnerPane exportDumpPane = new SpinnerPane();
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
exportDumpButton.setDisable(true);
}
exportDumpPane.setContent(exportDumpButton);
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
vbox.getChildren().add(bottom);
}
+111 -490
View File
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
@@ -25,21 +25,106 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
public final class SVG {
private SVG() {
public enum SVG {
GEAR("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"),
GEAR_OUTLINE("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"),
BACK("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"),
CANCEL("M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z"),
CHAT("M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z"),
CLOSE("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"),
COPY("M15 16H7V6h8m.4-2H6.6A1.6 1.6 90 005 5.6V16.4a1.6 1.6 90 001.6 1.6h8.8A1.6 1.6 90 0017 16.4V5.6A1.6 1.6 90 0015.4 4M13 1H3.6A1.6 1.6 90 002 2.6V13h2V3H13V1Z"),
DOTS_VERTICAL("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"),
DOTS_HORIZONTAL("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"),
DELETE("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"),
DELETE_OUTLINE("M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z"),
ACCOUNT_EDIT("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"),
EXPAND("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"),
COLLAPSE("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"),
NAVIGATE("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"),
ROCKET_LAUNCH_OUTLINE("M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z"),
LAUNCH_OUTLINE("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"),
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"),
PENCIL("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"),
PENCIL_OUTLINE("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"),
REFRESH("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"),
FOLDER_OPEN("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"),
FOLDER_OUTLINE("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"),
UPDATE("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"),
CLOSE_CIRCLE("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"),
CHECK_CIRCLE("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"),
INFO_CIRCLE("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
HELP_CIRCLE("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"),
HELP_CIRCLE_OUTLINE("M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z"),
ALERT("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"),
ALERT_OUTLINE("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"),
PLUS("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"),
PLUS_CIRCLE_OUTLINE("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"),
MINUS("M19 13H5V11H19V13Z"),
IMPORT_ICON("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"),
EXPORT("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z"),
OPEN_IN_NEW("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
TRIANGLE("M1,21H23L12,2"),
HOME("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"),
VIEW_LIST("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"),
CHECK("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"),
ARROW_RIGHT("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z"),
WRENCH("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"),
WRENCH_OUTLINE("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"),
UPLOAD("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z"),
HANGER("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"),
PUZZLE("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"),
CUBE("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"),
PACK("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"),
TEXTURE_BOX("M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z"),
GAMEPAD("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"),
FIRE("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"),
MONITOR_SCREENSHOT("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"),
TEXTURE("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"),
ALPHA_CIRCLE_OUTLINE("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"),
BETA_CIRCLE_OUTLINE("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"),
RELEASE_CIRCLE_OUTLINE("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"),
INFORMATION_OUTLINE("M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"),
HAND_HEAR_OUTLINE("M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z"),
MOJANG("m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z"),
MICROSOFT("M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z"),
ACCOUNT_OUTLINE("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z"),
ACCOUNT_GROUP_OUTLINE("M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z"),
ACCOUNT_ARROW_RIGHT_OUTLINE("M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z"),
STYLE_OUTLINE("M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z"),
DOWNLOAD_OUTLINE("M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z"),
APPLICATION_OUTLINE("M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z"),
EARTH("M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
BELL("M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21"),
CONTENT_SAVE_MOVE_OUTLINE("M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z"),
ACCOUNT("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"),
SERVER("M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z"),
MESSAGE_ALERT_OUTLINE("M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z"),
CHECK_CIRCLE_OUTLINE("M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"),
CLOSE_CIRCLE_OUTLINE("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z"),
CLOCK_OUTLINE("M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z"),
MAGNIFY("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"),
MENU_UP("M7,15L12,10L17,15H7Z"),
MENU_DOWN("M7,10L12,15L17,10H7Z"),
RESTORE("M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z"),
BUG("M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"),
DISCORD("M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z"),
LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"),
THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"),
THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"),
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"),
JAVA("m 12.400746,23.498132 c 0,0 -1.047682,0.609185 0.745753,0.816159 2.173248,0.247515 3.284943,0.212301 5.680103,-0.241112 0,0 0.629462,0.394742 1.509642,0.737207 -5.369636,2.301245 -12.1518319,-0.133351 -7.934431,-1.312254 z m -0.656135,-3.003246 c 0,0 -1.175708,0.870569 0.619861,1.056206 2.321545,0.238977 4.155521,0.25925 7.32844,-0.35207 0,0 0.43849,0.444887 1.128765,0.688135 -6.492002,1.897965 -13.7233551,0.149351 -9.077066,-1.392271 z m 5.531806,-5.094318 c 1.322937,1.523496 -0.347807,2.894427 -0.347807,2.894427 0,0 3.359626,-1.733668 1.816908,-3.905821 -1.441364,-2.024926 -2.545591,-3.030987 3.43644,-6.5004569 0,0 -9.389665,2.3449859 -4.905541,7.5129179 z m 7.101193,10.318794 c 0,0 0.775626,0.639057 -0.854578,1.133019 C 20.42373,27.79123 10.623314,28.075018 7.9006204,26.88973 6.9222859,26.464048 8.75733,25.873001 9.3345153,25.749244 9.9362392,25.619093 10.280844,25.642568 10.280844,25.642568 9.1926195,24.875486 3.2457961,27.147927 7.2604887,27.798718 18.208875,29.573994 27.21766,26.999632 24.37761,25.718316 Z M 12.904316,17.383886 c 0,0 -4.9855587,1.184229 -1.765696,1.614178 1.359213,0.182426 4.069103,0.140825 6.594422,-0.07042 2.063359,-0.173899 4.135252,-0.544104 4.135252,-0.544104 0,0 -0.727616,0.311527 -1.253592,0.671064 -5.062375,1.331456 -14.8414526,0.711604 -12.02594,-0.649727 2.38129,-1.151156 4.316621,-1.019929 4.316621,-1.019929 z m 8.943706,4.998298 c 5.146658,-2.673583 2.766435,-5.24368 1.106361,-4.898013 -0.407551,0.08428 -0.58892,0.1579 -0.58892,0.1579 0,0 0.151501,-0.236845 0.439557,-0.339265 3.284942,-1.155424 5.812393,3.406524 -1.060485,5.213807 0,0 0.08003,-0.07147 0.103478,-0.134425 z M 18.744451,2.2855 c 0,0 2.849653,2.8506848 -2.703489,7.2344656 -4.453183,3.5164124 -1.015676,5.5221324 -0.0021,7.8127104 C 13.439893,14.987688 11.532301,12.92329 12.811497,11.001852 14.690284,8.1810394 19.893487,6.8143757 18.743384,2.2855 Z M 13.41002,29.628384 c 4.939684,0.315794 12.525242,-0.174974 12.70448,-2.512485 0,0 -0.345672,0.886571 -4.081906,1.589641 -4.216334,0.793754 -9.416337,0.700936 -12.4996379,0.192025 0,0 0.6315969,0.522768 3.8781309,0.730807 z");
private final String path;
SVG(String path) {
this.path = path;
}
public interface SVGIcon {
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
public String getPath() {
return path;
}
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
if (fill != null)
path.fillProperty().bind(fill);
private static Node createIcon(SVGPath path, double width, double height) {
if (width < 0 || height < 0) {
StackPane pane = new StackPane(path);
pane.setAlignment(Pos.CENTER);
@@ -54,488 +139,24 @@ public final class SVG {
return svg;
}
// default fill: white, width: 24, height 24
public Node createIcon(ObservableValue<? extends Paint> fill, double width, double height) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().bind(fill);
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z",
fill, width, height);
return createIcon(p, 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);
}
public static Node cancel(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z",
fill, width, height);
}
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",
fill, width, height);
}
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z",
fill, width, height);
}
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z",
fill, width, height);
}
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z",
fill, width, height);
}
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill,
width, height);
}
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);
}
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
}
public static Node collapse(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
}
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
fill, width, height);
}
public static Node rocketLaunchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z",
fill, width, height);
}
public static Node launchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
fill, width, height);
}
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z",
fill, width, height);
}
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z",
fill, width, height);
}
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);
}
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z",
fill, width, height);
}
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);
}
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z",
fill, width, height);
}
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z",
fill, width, height);
}
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
fill, width, height);
}
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z",
fill, width, height);
}
public static Node helpCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z",
fill, width, height);
}
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
}
public static Node alertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16", fill, width, height);
}
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
public static Node plusCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z",
fill, width, height);
}
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z",
fill, width, height);
}
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z",
fill, width, height);
}
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
fill, width, height);
}
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1,21H23L12,2", fill, width, height);
}
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
}
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z",
fill, width, height);
}
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
}
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width,
height);
}
public static Node wrench(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z",
fill, width, height);
}
public static Node 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 Node createIcon(Paint fill, double width, double height) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().set(fill);
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
return createIcon(p, 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 textureBox(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z",
fill, width, height);
}
public static Node gamepad(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z",
fill, width, height);
}
public static Node fire(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z",
fill, width, height);
}
public static Node monitorScreenshot(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2",
fill, width, height);
}
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z",
fill, width, height);
}
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z",
fill, width, height);
}
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
fill, width, height);
}
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
fill, width, height);
}
public static Node informationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z",
fill, width, height);
}
public static Node handHearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z",
fill, width, height);
}
public static Node mojang(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z",
fill, width, height);
}
public static Node microsoft(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z",
fill, width, height);
}
public static Node accountOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z",
fill, width, height);
}
public static Node accountGroupOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z",
fill, width, height);
}
public static Node accountArrowRightOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z",
fill, width, height);
}
public static Node styleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z",
fill, width, height);
}
public static Node downloadOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z",
fill, width, height);
}
public static Node applicationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z",
fill, width, height);
}
public static Node earth(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
fill, width, height);
}
public static Node bell(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21",
fill, width, height);
}
public static Node contentSaveMoveOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z",
fill, width, height);
}
public static Node account(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z",
fill, width, height);
}
public static Node server(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z",
fill, width, height);
}
public static Node messageAlertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z",
fill, width, height);
}
public static Node checkCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z",
fill, width, height);
}
public static Node closeCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z",
fill, width, height);
}
public static Node clockOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z",
fill, width, height);
}
public static Node magnify(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z",
fill, width, height);
}
public static Node menuUp(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M7,15L12,10L17,15H7Z",
fill, width, height);
}
public static Node menuDown(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M7,10L12,15L17,10H7Z",
fill, width, height);
}
public static Node restore(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z",
fill, width, height);
}
public static Node bug(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z",
fill, width, height);
}
public static Node discord(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
fill, width, height);
}
public static Node lan(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z",
fill, width, height);
}
public static Node thumbUpOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z",
fill, width, height);
}
public static Node thumbDownOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z",
fill, width, height);
}
public static Node selectAll(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath(
"M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z",
fill, width, height);
}
}
@@ -0,0 +1,215 @@
// Copy from https://github.com/palexdev/MaterialFX/blob/c8038ce2090f5cddf923a19d79cc601db86a4d17/materialfx/src/main/java/io/github/palexdev/materialfx/utils/ScrollUtils.java
/*
* Copyright (C) 2022 Parisi Alessandro
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
*
* MaterialFX is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MaterialFX 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.util.Duration;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
/**
* Utility class for ScrollPanes.
*/
final class ScrollUtils {
public enum ScrollDirection {
UP(-1), RIGHT(-1), DOWN(1), LEFT(1);
final int intDirection;
ScrollDirection(int intDirection) {
this.intDirection = intDirection;
}
public int intDirection() {
return intDirection;
}
}
private ScrollUtils() {
}
/**
* Determines if the given ScrollEvent comes from a trackpad.
* <p></p>
* Although this method works in most cases, it is not very accurate.
* Since in JavaFX there's no way to tell if a ScrollEvent comes from a trackpad or a mouse
* we use this trick: I noticed that a mouse scroll has a delta of 32 (don't know if it changes depending on the device or OS)
* and trackpad scrolls have a way smaller delta. So depending on the scroll direction we check if the delta is lesser than 10
* (trackpad event) or greater(mouse event).
*
* @see ScrollEvent#getDeltaX()
* @see ScrollEvent#getDeltaY()
*/
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
switch (scrollDirection) {
case UP:
case DOWN:
return Math.abs(event.getDeltaY()) < 10;
case LEFT:
case RIGHT:
return Math.abs(event.getDeltaX()) < 10;
default:
return false;
}
}
/**
* Determines the scroll direction of the given ScrollEvent.
* <p></p>
* Although this method works fine, it is not very accurate.
* In JavaFX there's no concept of scroll direction, if you try to scroll with a trackpad
* you'll notice that you can scroll in both directions at the same time, both deltaX and deltaY won't be 0.
* <p></p>
* For this method to work we assume that this behavior is not possible.
* <p></p>
* If deltaY is 0 we return LEFT or RIGHT depending on deltaX (respectively if lesser or greater than 0).
* <p>
* Else we return DOWN or UP depending on deltaY (respectively if lesser or greater than 0).
*
* @see ScrollEvent#getDeltaX()
* @see ScrollEvent#getDeltaY()
*/
public static ScrollDirection determineScrollDirection(ScrollEvent event) {
double deltaX = event.getDeltaX();
double deltaY = event.getDeltaY();
if (deltaY == 0.0) {
return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
} else {
return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
}
}
//================================================================================
// ScrollPanes
//================================================================================
/**
* Adds a smooth scrolling effect to the given scroll pane,
* calls {@link #addSmoothScrolling(ScrollPane, double)} with a
* default speed value of 1.
*/
public static void addSmoothScrolling(ScrollPane scrollPane) {
addSmoothScrolling(scrollPane, 1);
}
/**
* Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
* Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
* with a default trackPadAdjustment of 7.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
addSmoothScrolling(scrollPane, speed, 7);
}
/**
* Adds a smooth scrolling effect to the given scroll pane with the given
* scroll speed and the given trackPadAdjustment.
* <p></p>
* The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
* This is kind of a workaround and it's not perfect, but at least it's way better than before.
* The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
smoothScroll(scrollPane, speed, trackPadAdjustment);
}
private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
final double[] frictions = {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};
final double[] derivatives = new double[frictions.length];
AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
Timeline timeline = new Timeline();
AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
final EventHandler<ScrollEvent> scrollHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
scrollDirection.set(determineScrollDirection(event));
if (isTrackPad(event, scrollDirection.get())) {
atomicSpeed.set(speed / trackPadAdjustment);
} else {
atomicSpeed.set(speed);
}
derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
if (timeline.getStatus() == Status.STOPPED) {
timeline.play();
}
event.consume();
}
};
if (scrollPane.getContent().getParent() != null) {
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
}
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
}
if (newValue != null) {
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
}
});
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];
Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
double size = sizeFunction.apply(scrollPane.getContent().getLayoutBounds());
double value;
switch (scrollDirection.get()) {
case LEFT:
case RIGHT:
value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
scrollPane.hvalueProperty().set(value);
break;
case UP:
case DOWN:
value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
scrollPane.vvalueProperty().set(value);
break;
}
if (Math.abs(dy) < 0.001) {
timeline.stop();
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
}
}
@@ -0,0 +1,33 @@
package org.jackhuang.hmcl.ui;
import javax.swing.*;
public final class SwingUtils {
static {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
}
private SwingUtils() {
}
public static void initLookAndFeel() {
// Make sure the static constructor is called
}
public static void showInfoDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
}
public static void showWarningDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
}
public static void showErrorDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -69,7 +68,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
root.getContent().add(scrollPane);
}
@@ -86,32 +85,32 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
return stackPane;
}
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
public static JFXButton createToolbarButton2(String text, SVG.SVGIcon creator, Runnable onClick) {
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(creator.createIcon(Theme.blackFillBinding(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1, -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
public static JFXButton createDecoratorButton(String tooltip, SVG svg, 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)));
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class TooltipInstaller {
public static final TooltipInstaller INSTALLER;
static {
TooltipInstaller installer = null;
try {
installer = new NewInstaller();
} catch (Throwable e) {
try {
installer = new OldInstaller();
} catch (Throwable e2) {
e2.addSuppressed(e);
LOG.warning("Failed to initialize TooltipInstaller", e2);
}
}
INSTALLER = installer != null ? installer : new TooltipInstaller();
}
TooltipInstaller() {
}
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
Tooltip.install(node, tooltip);
}
// For Java 8
private static final class OldInstaller extends TooltipInstaller {
private static final Constructor<?> createTooltipBehavior;
private static final Method installTooltipBehavior;
static {
try {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
createTooltipBehavior.setAccessible(true);
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installTooltipBehavior.setAccessible(true);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
installTooltipBehavior.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
LOG.warning("Failed to set tooltip show delay", e);
Tooltip.install(node, tooltip);
}
}
}
// For Java 9+
private static final class NewInstaller extends TooltipInstaller {
private static final MethodHandle setTooltipShowDelay;
private static final MethodHandle setTooltipShowDuration;
private static final MethodHandle setTooltipHideDelay;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodType methodType = MethodType.methodType(void.class, Duration.class);
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
setTooltipShowDelay.invokeExact(tooltip, showDelay);
setTooltipShowDuration.invokeExact(tooltip, showDuration);
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
} catch (Throwable e) {
LOG.warning("Failed to set tooltip show delay", e);
}
Tooltip.install(node, tooltip);
}
}
}
@@ -20,57 +20,72 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import java.util.logging.Level;
import java.io.IOException;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class UpgradeDialog extends JFXDialogLayout {
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
try {
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + remoteVersion.getChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String viewURL = engine.getLoadWorker().getMessage().trim();
if (!viewURL.startsWith(CHANGELOG_URL)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(viewURL);
}
});
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed", e);
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
if (node == null)
throw new IOException("Cannot find #nowchange in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
FXUtils.openLink(uri.toString());
});
do {
renderer.appendNode(node);
node = node.nextSibling();
} while (node != null);
return renderer.render();
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane(result);
scrollPane.setFitToWidth(true);
setBody(scrollPane);
} else {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
setBody(webView);
}
}).start();
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
openInBrowser.setExternalLink(url);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnAction(e -> updateRunnable.run());
setActions(updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
setActions(openInBrowser, updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
}
@@ -24,11 +24,11 @@ import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>();
private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() {
}
@@ -0,0 +1,75 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WebPage extends SpinnerPane implements DecoratorPage {
private final ObjectProperty<DecoratorPage.State> stateProperty;
public WebPage(String title, String content) {
this.stateProperty = new SimpleObjectProperty<>(DecoratorPage.State.fromTitle(title));
this.setBackground(new Background(new BackgroundFill(Color.WHITE, null, null)));
Task.supplyAsync(() -> {
Document document = Jsoup.parseBodyFragment(content);
HTMLRenderer renderer = new HTMLRenderer(uri -> {
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
FXUtils.openLink(uri.toString());
}, null);
});
renderer.appendNode(document);
return renderer.render();
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setContent(result);
setContent(scrollPane);
setFailedReason(null);
} else {
LOG.warning("Failed to load content", exception);
setFailedReason(i18n("web.failed"));
}
})).start();
}
@Override
public ReadOnlyObjectProperty<DecoratorPage.State> stateProperty() {
return stateProperty;
}
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
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 {
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() {
this(800, 480);
}
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
getIcons().add(newImage("/assets/img/icon.png"));
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
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() {
return webView;
}
}
@@ -22,32 +22,28 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static javafx.beans.binding.Bindings.createStringBinding;
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private final Canvas canvas;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@Override
protected void invalidated() {
@@ -55,17 +51,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
if (account == null) {
titleProperty().unbind();
subtitleProperty().unbind();
imageView.imageProperty().unbind();
tooltip.textProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
tooltip.setText(i18n("account.create"));
TexturesLoader.unbindAvatar(canvas);
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkinImage());
} else {
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
subtitleProperty().bind(accountSubtitle(account));
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.textProperty().bind(accountTooltip(account));
TexturesLoader.bindAvatar(canvas, account);
}
}
};
@@ -74,9 +72,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
canvas = new Canvas(32, 32);
setLeftGraphic(canvas);
setActionButtonVisible(false);
@@ -20,8 +20,6 @@ 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.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
@@ -37,8 +35,6 @@ 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;
@@ -49,18 +45,16 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
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.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItem extends RadioButton {
@@ -68,7 +62,6 @@ public class AccountListItem extends RadioButton {
private final Account account;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
public AccountListItem(Account account) {
this.account = account;
@@ -76,13 +69,14 @@ public class AccountListItem extends RadioButton {
setUserData(account);
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
String portableSuffix = account.isPortable() ? ", " + i18n("account.portable") : "";
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
subtitle.bind(Bindings.concat(
loginTypeName, ", ", i18n("account.injector.server"), ": ",
Bindings.createStringBinding(server::getName, server)));
Bindings.createStringBinding(server::getName, server), portableSuffix));
} else {
subtitle.set(loginTypeName);
subtitle.set(loginTypeName + portableSuffix);
}
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
@@ -93,8 +87,6 @@ public class AccountListItem extends RadioButton {
account.getUsername().isEmpty() ? characterName :
Bindings.concat(account.getUsername(), " - ", characterName));
}
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
}
@Override
@@ -113,31 +105,27 @@ public class AccountListItem extends RadioButton {
} catch (CancellationException e1) {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
LOG.warning("Failed to refresh " + account + " with password", e1);
throw e1;
}
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
LOG.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 if (account instanceof OfflineAccount) {
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 if (account instanceof OfflineAccount || account.canUploadSkin()) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
@@ -153,7 +141,7 @@ public class AccountListItem extends RadioButton {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (!(account instanceof YggdrasilAccount)) {
if (!account.canUploadSkin()) {
return null;
}
@@ -167,19 +155,19 @@ public class AccountListItem extends RadioButton {
return refreshAsync()
.thenRunAsync(() -> {
BufferedImage skinImg;
try {
skinImg = ImageIO.read(selectedFile);
Image skinImg;
try (FileInputStream input = new FileInputStream(selectedFile)) {
skinImg = new Image(input);
} catch (IOException e) {
throw new InvalidSkinException("Failed to read skin image", e);
}
if (skinImg == null) {
throw new InvalidSkinException("Failed to read skin image");
if (skinImg.isError()) {
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
})
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
@@ -220,16 +208,4 @@ public class AccountListItem extends RadioButton {
public StringProperty subtitleProperty() {
return subtitle;
}
public Image getImage() {
return image.get();
}
public void setImage(Image image) {
this.image.set(image);
}
public ObjectProperty<Image> imageProperty() {
return image;
}
}
@@ -21,15 +21,17 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
@@ -40,10 +42,9 @@ 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;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItemSkin extends SkinBase<AccountListItem> {
public final class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) {
super(skinnable);
@@ -64,9 +65,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
Canvas canvas = new Canvas(32, 32);
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
Label title = new Label();
title.getStyleClass().add("title");
@@ -84,16 +84,51 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
item.getStyleClass().add("two-line-list-item");
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
center.getChildren().setAll(canvas, item);
root.setCenter(center);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnMove = new JFXButton();
SpinnerPane spinnerMove = new SpinnerPane();
spinnerMove.getStyleClass().add("small-spinner-pane");
btnMove.setOnAction(e -> {
Account account = skinnable.getAccount();
Accounts.getAccounts().remove(account);
if (account.isPortable()) {
account.setPortable(false);
if (!Accounts.getAccounts().contains(account))
Accounts.getAccounts().add(account);
} else {
account.setPortable(true);
if (!Accounts.getAccounts().contains(account)) {
int idx = 0;
for (int i = Accounts.getAccounts().size() - 1; i >= 0; i--) {
if (Accounts.getAccounts().get(i).isPortable()) {
idx = i + 1;
break;
}
}
Accounts.getAccounts().add(idx, account);
}
}
});
btnMove.getStyleClass().add("toggle-icon4");
if (skinnable.getAccount().isPortable()) {
btnMove.setGraphic(SVG.EARTH.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global"));
} else {
btnMove.setGraphic(SVG.EXPORT.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable"));
}
spinnerMove.setContent(btnMove);
right.getChildren().add(spinnerMove);
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
btnRefresh.setOnMouseClicked(e -> {
btnRefresh.setOnAction(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
.whenComplete(Schedulers.javafx(), ex -> {
@@ -106,14 +141,14 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
.start();
});
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh"));
spinnerRefresh.setContent(btnRefresh);
right.getChildren().add(spinnerRefresh);
JFXButton btnUpload = new JFXButton();
SpinnerPane spinnerUpload = new SpinnerPane();
btnUpload.setOnMouseClicked(e -> {
btnUpload.setOnAction(e -> {
Task<?> uploadTask = skinnable.uploadSkin();
if (uploadTask != null) {
spinnerUpload.showSpinner();
@@ -123,20 +158,30 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
}
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
btnUpload.setGraphic(SVG.HANGER.createIcon(Theme.blackFill(), -1, -1));
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 btnCopyUUID = new JFXButton();
SpinnerPane spinnerCopyUUID = new SpinnerPane();
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
btnUpload.getStyleClass().add("toggle-icon4");
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnCopyUUID.setGraphic(SVG.COPY.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
spinnerCopyUUID.setContent(btnCopyUUID);
right.getChildren().add(spinnerCopyUUID);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
btnRemove.setGraphic(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), -1, -1));
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
@@ -45,8 +44,10 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.net.URI;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
@@ -95,30 +96,22 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
VBox boxMethods = new VBox();
{
boxMethods.getStyleClass().add("advanced-list-box-content");
boxMethods.getChildren().add(new ClassTitle(i18n("account.create")));
boxMethods.getChildren().add(new ClassTitle(i18n("account.create").toUpperCase(Locale.ROOT)));
FXUtils.setLimitWidth(boxMethods, 200);
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG::account));
offlineItem.setLeftGraphic(wrap(SVG.ACCOUNT));
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
boxMethods.getChildren().add(offlineItem);
AdvancedListItem mojangItem = new AdvancedListItem();
mojangItem.getStyleClass().add("navigation-drawer-item");
mojangItem.setActionButtonVisible(false);
mojangItem.setTitle(i18n("account.methods.yggdrasil"));
mojangItem.setLeftGraphic(wrap(SVG::mojang));
mojangItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
boxMethods.getChildren().add(mojangItem);
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setActionButtonVisible(false);
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftGraphic(wrap(SVG::microsoft));
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
@@ -126,21 +119,29 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG::server));
item.setLeftGraphic(wrap(SVG.SERVER));
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
btnRemove.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 14, 14));
item.setRightGraphic(btnRemove);
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
String host = "";
try {
host = URI.create(server.getUrl()).getHost();
} catch (IllegalArgumentException e) {
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
}
item.subtitleProperty().set(host);
Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
FXUtils.installFastTooltip(item, tooltip);
@@ -157,7 +158,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
addAuthServerItem.setTitle(i18n("account.injector.add"));
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
addAuthServerItem.setActionButtonVisible(false);
addAuthServerItem.setLeftGraphic(wrap(SVG::plusCircleOutline));
addAuthServerItem.setLeftGraphic(wrap(SVG.PLUS_CIRCLE_OUTLINE));
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
}
@@ -179,7 +180,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
Bindings.bindContent(list.getChildren(), skinnable.items);
scrollPane.setContent(list);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
setCenter(scrollPane);
}
Loaded 100 of 743 files, more files were not shown because too many files have changed in this diff. Show more