Compare commits

...
Author SHA1 Message Date
辞庐andGlavo 17abfc1a55 添加Xbox封禁相关提示 (#3860)
---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-31 22:53:17 +08:00
Glavo caa98c8956 新增“原理图管理”界面 (#3951) 2025-05-31 21:53:16 +08:00
General_K1ng 6124b6711f 在切换目录时确保主页版本列表切换菜单正确更新 (#3949) 2025-05-31 21:48:24 +08:00
Glavo 3b3cc54cb5 Bump HMCLauncher to 3.6.0.2 (#3953) 2025-05-31 16:36:23 +08:00
Glavo 45a40ee675 优化 Linux 平台硬件检测 (#3946) 2025-05-31 11:38:28 +08:00
Glavo 4a75f1d462 修复通过 fc-match 获取字体时未正确处理 TTC 文件的问题 (#3950) 2025-05-30 22:33:19 +08:00
Glavo e0805fc25f Fix #3940: 修复未正确解析 IPv6 服务器地址的问题 (#3942) 2025-05-29 19:34:53 +08:00
General_K1ng f223d2bc41 fix(ui): 修复版本列表为空时显示的问题 (#3944) 2025-05-29 19:26:42 +08:00
3gf8jv4dv 5ab75b6a84 chore(locale): add translations of new text for spanish and russian (#3941) 2025-05-26 21:47:44 +08:00
Glavoand3gf8jv4dv 362969db95 使用 Java 8 启动启动器时弹出弃用警告 (#3938)
* 使用 Java 8 启动启动器时弹出弃用警告

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

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

* 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>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-05-26 09:48:55 +08:00
Glavo fb37e3b0df 修复导出启动器时未正确从 .hmcl/background 中查找壁纸的问题 (#3936) 2025-05-25 13:16:40 +08:00
Glavo ad312a61fe 清理高通骁龙 SoC 名称 (#3935) 2025-05-25 13:05:54 +08:00
Glavo 58a4700a46 创建 WindowsVersion 工具类 (#3932) 2025-05-23 21:11:35 +08:00
Glavo 68c8a30bd7 导出整合包时将启动器配置集中存放在 .hmcl 文件夹中 (#3930) 2025-05-23 18:08:57 +08:00
Glavo bb37e3fafa Use StringUtils.isASCII to check if a string contains non-ASCII characters (#3931) 2025-05-23 00:25:28 +08:00
Glavo 942f7b703e 清理 CPU 名称 (#3928) 2025-05-21 14:22:27 +08:00
Burning_TNTandGlavo 267da46050 将日志 caller 信息中的 'org.jackhuang.hmcl.' 缩短为 '@.' (#3919)
* Shorten 'org.jackhuang.hmcl.' in logs to @.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-21 00:08:41 +08:00
Glavo 7c40dca3ea Bump JavaFX to 17.0.15/24.0.1 (#3927) 2025-05-20 23:55:24 +08:00
Glavo 48e3f79965 将 OS X 重命名为 macOS (#3878) 2025-05-20 19:34:35 +08:00
Glavo c5f56eb5c4 清理 OperatingSystem (#3925) 2025-05-20 19:03:48 +08:00
Burning_TNT 0900ad3e5a Fix #3757: 切换游戏文件夹后,全局游戏设置应重新读取设置 (#3818) 2025-05-20 13:41:25 +08:00
Glavo d347b0c6cb 优化系统版本检测 (#3924) 2025-05-20 13:33:57 +08:00
Glavo 2f3bf61d94 调用 PowerShell 时添加 NoProfile 参数 (#3923) 2025-05-20 12:54:04 +08:00
3gf8jv4dv 5479607597 fix(i18n): refine tasklistpane i18n (#3600)
The current TaskListPane may not look different for Chinese users, but
the grammar is not quite right for English users.

My previous series of PRs did not address this aspect due to the
associated hard coding of the i18n key.

I believe that revisions in this aspect can further improve the
experience of Indo-European speakers.

All languages except Japanese are ready for this PR.
2025-05-20 11:53:01 +08:00
Glavo 4291ae5f16 在 Linux 平台通过 fc-match 查询默认字体 (#3922) 2025-05-20 00:25:56 +08:00
e74yp8andGlavo 1ac677365b Fix #3865 游戏版本名称在游戏版本更换时报红 (#3874)
---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-19 12:00:16 +08:00
Glavo b079e50916 修复启动器在部分 Linux 发行版上无法正确识别用户地区的问题 (#3921) 2025-05-19 11:06:45 +08:00
e74yp8 f986df9654 Fix #3896: 下载界面对于多行信息显示错误 (#3901) 2025-05-18 23:11:34 +08:00
RainChanandGlavo c576c48f9d Fix #3852: 修复无法一键更新整合包的问题 (#3888)
* Fix #3852 修复无法一键更新整合包

* 删除未使用的import

* fix checkstyle

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-17 07:54:13 +08:00
Glavo 5bb035c884 在非 x86 平台检测 CPU 物理核心数 (#3917) 2025-05-17 00:20:17 +08:00
Glavo 973b7a9716 启动时在日志中记录 CPU 信息 (#3914) 2025-05-16 14:50:43 +08:00
Glavo 6e05b5ee58 使用 JNA 查询注册表 (#3913) 2025-05-14 22:18:02 +08:00
Glavo 76ed9353bd 重构 Theme (#3910)
* update

* update

* update

* update

* update

* update
2025-05-14 14:18:50 +08:00
Glavo 9c0f823705 优化代理设置 (#3876) 2025-05-13 16:39:43 +08:00
e74yp8 b69602b735 Fix #3218: 修复复制游戏版本里的“复制存档”选项失效问题 (#3868) 2025-05-12 23:21:20 +08:00
3gf8jv4dv 7cdf4504d1 fix(locale): add i18n key curse.category.7669 (#3906) 2025-05-11 23:07:29 +08:00
Burning_TNT d95efc4cdc Use org.intellij.lang.annotations.Language (#3907) 2025-05-11 23:07:06 +08:00
Glavo 48ca2d6ee6 在 SystemInfo 中采集内存信息 (#3903) 2025-05-10 12:06:11 +08:00
Glavo 1da98d6d67 启动时在日志中记录当前平台的所有 GPU (#3893) 2025-05-09 10:08:08 +08:00
Glavo 06a89dd6a9 尝试从 HMCL.jar 所在文件夹加载默认字体 (#3899) 2025-05-08 22:36:33 +08:00
3gf8jv4dv 4eee79da17 chore(locale): revise invalid username prompt for spanish and russian (#3897) 2025-05-06 21:48:41 +08:00
Glavo 9788338b65 更新“关于”页面 (#3895) 2025-05-06 21:48:05 +08:00
3gf8jv4dv fb8da249b6 chore(locale): add/revise prompt about create hmcl dir failure (#3879) 2025-05-04 23:14:53 +08:00
Glavo 2d8b1df465 在 Windows 平台通过 GeoID 检测用户所处国家/区域 (#3891) 2025-05-04 23:13:40 +08:00
Glavo 1ab7ab0750 使用 JNA 调用本机 API (#3890) 2025-05-04 22:31:50 +08:00
3gf8jv4dv f283254ca0 fix(locale): revise version.empty translation (#3812)
Revise `.launch` and `.hint` to make text match the actual situation.
2025-05-04 09:31:35 +08:00
Wulian233andGlavo 2ea96fdaa7 Fix #3864 游戏内容下载的分页按钮不重置 (#3866)
* Fix #3864

* Update DownloadListPage.java

* Update DownloadListPage.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-03 04:45:47 +08:00
Glavo b32d3c354a Move authlib-injector.jar to .hmcl/dependencies/universal (#3886) 2025-05-03 04:45:25 +08:00
e74yp8 cd57b163a3 Fix #3851: 当用户名为中文名时,离线账户 - LittleSkin 换肤 功能失效 (#3883) 2025-05-02 22:21:55 +08:00
Glavo cc5484f9ed Fix #3881: 打开启动器时应当创建 HMCL_GLOBAL_DIRECTORY (#3882) 2025-05-01 11:59:34 +08:00
Glavo bfa9c1a697 修复启动器未压缩日志的问题 (#3877) 2025-04-30 14:56:03 +08:00
Glavo 9a37d412a3 使用工作路径下的 .hmcl 文件夹存放所有配置文件 (#3875) 2025-04-30 14:50:51 +08:00
Glavo dc5ebcf64d 使用 libs.versions.toml 管理依赖版本 (#3873) 2025-04-29 13:41:45 +08:00
Glavo 038f56b048 Bump Shadow Plugin to 8.3.6 (#3871) 2025-04-29 10:13:02 +08:00
Glavo 528f79e008 使用 Java 11 构建 buildSrc (#3870) 2025-04-29 10:04:29 +08:00
Glavo 75d0ec34f5 将构建项目所需的最低 JDK 版本升级至 11 (#3805) 2025-04-29 09:52:06 +08:00
Glavo d70712a18f Fix #3546: 修复游戏崩溃窗口乱码的问题 (#3867) 2025-04-27 22:15:30 +08:00
Glavo 05c5b53c52 自动探测日志文件编码 (#3863) 2025-04-27 20:39:49 +08:00
Glavo e98c7dfef1 Fix #3400: 逐行过滤日志中的 token (#3856) 2025-04-27 15:41:09 +08:00
3gf8jv4dv 8ba0e16dd9 chore(locale): add new translations for russian and spanish (#3838)
For relevant text of offline account restrictions.
2025-04-26 16:40:12 +08:00
Glavo 77397f6722 优化离线账户中包含非 ASCII 字符时的提示 (#3853) 2025-04-26 13:19:38 +08:00
Glavo c9ba789b7c Bump Gradle to 8.14 (#3857) 2025-04-26 00:05:20 +08:00
Glavo 91135b961e 右键主页“启动游戏”按钮时展开版本列表菜单 (#3854) 2025-04-25 00:31:52 +08:00
3gf8jv4dvandGlavo 435790aa03 fix(i18n): show installable translations according to different styles (#3624)
* fix(i18n): show installable translations according to different styles

when install a new instance (安装新游戏): Do not install (不安装)
on Edit Instance → Loaders (自动安装页面): Not installed (未安装)

resolves https://github.com/HMCL-dev/HMCL/issues/3297

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-25 00:14:45 +08:00
Glavo c252d7027e 更新 Issue 模板 (#3847) 2025-04-22 23:59:24 +08:00
Glavo 9d0c8da310 拆分 HMCLauncher 至独立项目 (#3840) 2025-04-20 11:52:29 +08:00
Burning_TNTandGlavo 84b3312ebb Fix #3825: 修复 NeoForge 愚人节版本无法正常解析的问题 (#3826)
* Fix #2825.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-19 11:49:42 +08:00
Burning_TNTandGlavo f351d1d133 Close #3829: 将模组/整合包/资源包的默认下载源调整为 Modrinth (#3830)
* Close #3829: Use Modrinth as default download source.

* Update HMCLLocalizedDownloadListPage.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-18 22:49:02 +08:00
Glavo 5ca1120223 Bump jsoup to 1.19.1 (#3835) 2025-04-18 22:42:10 +08:00
Glavo 6aa4019221 Bump Gson to 2.13.0 (#3834) 2025-04-18 22:35:05 +08:00
Glavo 73fb8bda10 优化默认 JVM 参数 (#3824) 2025-04-15 03:46:33 +08:00
Glavo cc16f84992 默认限制离线账户功能 (#3823) 2025-04-14 20:34:01 +08:00
辞庐 b4945a150e 统一导出和导入整合包页面样式 (#3822) 2025-04-13 22:34:22 +08:00
Wulian233andGlavo 8bcbe74d82 优化整合包导出类型选择页面 (#3800)
* 优化整合包导出类型页面

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-12 22:10:28 +08:00
Glavo 69c2429e92 Bump mesa-loader-windows to 25.0.3 (#3819) 2025-04-12 20:36:05 +08:00
Glavo 88e83dc44a 修复自定义背景图片路径包含无效字符时启动器崩溃的问题 (#3817) 2025-04-12 17:51:28 +08:00
Burning_TNT 87c2126e5a Fix #3471: 模组管理内“安装到当前版本”与“下载到本地文件夹”相同 (#3772) 2025-04-12 17:39:58 +08:00
Liyan Zhao abf1ec3eb8 反序列化账户信息时优先将数字解析为 Long (#3080) 2025-04-12 13:01:49 +08:00
Burning_TNT 139fb41149 Fix #3147: 修复 LiteLoader 下载相关问题 (#3776)
* Fix #3147

* Fix: Use 'release' classifier on snapshot versions. Speed up loading time.

* Use https.
2025-04-12 12:07:26 +08:00
Glavo 6c986921d6 Delete unused constants (#3816) 2025-04-12 12:05:24 +08:00
3gf8jv4dv b8e6d6fc96 fix(locale): adjust the wording of the unofficial hmcl build prompts (#3813)
The display condition for `download.curseforge.unavailable` is whether
HMCL has a CurseForge API secret built in, not its update channel.
2025-04-12 11:39:40 +08:00
3gf8jv4dv 75e4aee8aa fix(locale): improve world backup ui i18n (#3815) 2025-04-12 11:38:50 +08:00
Glavo abda9f50e1 Fix #3807: 修复 Fabric Metadata 无效时 FabricInstallTask 抛出 NPE 的问题 (#3814) 2025-04-12 02:13:14 +08:00
Burning_TNT c33ef5170b Fix #3644: 修复下载 - 模组界面无法正常翻页、Category 会在切换下载源时出错的问题 (#3768) 2025-04-09 11:35:56 +08:00
Glavo a3cb871928 修复 RISC-V 平台游戏识别 CPU 名称出错的问题 (#3806) 2025-04-09 01:27:38 +08:00
Glavo 62e965c6fe 使用 Kala Compress 替代 commons-compress (#3481) 2025-04-08 17:36:36 +08:00
Glavo 99a031cea4 Gradle run task 应使用 HMCL_JAVA_HOME 所指向的 Java (#3804) 2025-04-08 12:29:14 +08:00
Zkitefly f97b9b9382 Update unlisted-versions.json (#3799) 2025-04-06 11:29:17 +08:00
Zkitefly a8929560df Update PURCHASE_URL (#3797) 2025-04-05 21:04:55 +08:00
Glavo 22c8c59b5d 优化 CacheRepository (#3794) 2025-04-05 06:08:02 +08:00
ZkiteflyandGlavo 78b043c19c 添加 unlisted-versions-of-minecraft (#3247)
* 添加 unlisted-versions-of-minecraft

* Update BMCLAPIDownloadProvider.java

* Update VersionsPage.java

* Update VersionsPage.java

* Update BMCLAPIDownloadProvider.java

* update name

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-04 22:49:31 +08:00
3gf8jv4dv fd51fb7d26 chore(locale): adjust the use of quotation marks in spanish (#3685)
Replace the quotation marks with guillemets.
2025-04-04 22:38:05 +08:00
Burning_TNT 43c59dad9c Fix: There are two progress bars when updating HMCL (#3793) 2025-04-04 20:54:08 +08:00
Zkitefly 81b0547610 更新链接为 Metadata 中的常量,优化代码可维护性 (#3674)
* 更新链接为 Metadata 中的常量,优化代码可维护性

* update
2025-04-04 18:47:42 +08:00
997a16486b 优化下载窗口标题 (#3771)
* update

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

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

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

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

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

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

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

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

* Apply suggestions from code review

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

* update

* update

* update

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-04 00:47:37 +08:00
Wulian233andGlavo 36ebdab698 折叠高级设置中基于特定系统不可用的选项 (#3734)
* 隐藏高级设置中基于特定系统不可用的选项

* 不支持的系统使用折叠选项

* 完成~待审

* 修改文本描述

* 修正翻译

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-03 23:08:34 +08:00
Glavo a6471bca09 Add 25w14craftmine to versions.txt (#3781) 2025-04-02 00:35:05 +08:00
Burning_TNT cb6320ffae Fix #3627: 修复离线账户切换模型时左侧 3D 预览不会即时更新的问题 (#3769) 2025-04-01 15:43:01 +08:00
Burning_TNT 7cf520cb48 Fix #3589: 修复 Windows 平台部分语言编码下无法打开 Minecraft 中文 Wiki 的问题 (#3770) 2025-04-01 15:40:05 +08:00
Minecraft-温迪andGlavo 7dc922d4bd Fix #3777: 修复使用 Rosetta 2 转译运行时无法正确识别系统架构的问题 (#3778)
* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-01 15:31:09 +08:00
Glavo 8e57729349 Update versions.txt (#3744) 2025-03-26 22:55:44 +08:00
Glavo 66ed87eb1e 修复 cslApi 为 null 时启动器崩溃的问题 (#3762) 2025-03-24 02:52:17 +08:00
ShulkerSakuraandGlavo 27ffc8eec5 Add MacOS Style Icon (#3755)
* Add MacOS Style Icon

* MacOS Style Alt Icon

* Scaled Mac Style Icon

* Delete unused icons

* Rename icon

* update

* Optimize icon file

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-22 19:13:32 +08:00
Glavo e2f50c92e9 修复无法解析部分 Java 版本号的问题 (#3759)
* 修复无法解析部分 Java 版本号的问题

* update

* update
2025-03-22 12:17:13 +08:00
Glavo e8ff42c4b2 修复取消导出整合包时弹出报错对话框的问题 (#3752)
* 修复取消导出整合包时弹出报错对话框的问题

* Update
2025-03-21 11:01:37 +08:00
Glavo 44ca2cdb55 更新主页公告 (#3751)
* 更新主页公告

* update

* update
2025-03-20 21:25:30 +08:00
Glavo 77bbee02f9 消除 JEP 498 引入的弃用警告 (#3750)
* 消除 JEP 498 引入的弃用警告

* Fix checkstyle
2025-03-20 16:59:46 +08:00
Glavo e4c7b137f1 Add --enable-native-access for javafx.graphics (#3749)
* Add --enable-native-access for javafx.graphics

* fix checkstyle

* update
2025-03-20 16:21:17 +08:00
Glavo efd088e014 在日志中记录编码信息 (#3747)
* 在日志中记录 Code Page

* update

* update
2025-03-20 10:02:16 +08:00
Glavo 2ae266491c 更新关于页面头像 (#3745) 2025-03-18 20:11:07 +08:00
Glavo 0912613cfc 优化 ScrollUtils (#3432)
* 优化 ScrollUtils

* update
2025-03-18 13:31:47 +08:00
Glavo d9e3816b8e 优化账户列表页面 (#3736) 2025-03-13 22:18:23 +08:00
GlavoandZkitefly bdaf72fe2f 修复 ModrinthRemoteModRepository::getRemoteVersionByLocalFile 未捕获 NoSuchFileException 的问题 (#3735)
---------

Co-authored-by: Zkitefly <2573874409@qq.com>
2025-03-11 21:58:18 +08:00
Glavo 7efbbefdcf HTMLRenderer 应当避免插入过多换行符 (#3733) 2025-03-10 22:14:46 +08:00
Glavo ad71216bfc 为部分页面添加半透明背景遮罩 (#3730) 2025-03-09 13:02:00 +08:00
Wulian233 57903da09b Close #3728: 为更新日志对话框添加平滑滚动 (#3729) 2025-03-08 22:35:51 +08:00
3gf8jv4dv cf56c8035b fix: add scrollbar for GameCrashWindow (#3723)
Shows a scroll bar when needed to prevent user misinterpretation
when there is too much information.
2025-03-08 16:32:54 +08:00
Glavo dbf519b435 Fix #3720: 修复备份文件路径被错误地设置为源世界的问题 (#3722) 2025-03-08 16:18:49 +08:00
Glavo 696b9bd7ef Fix #3694: 显示对话框时隐藏焦点 (#3717) 2025-03-08 08:47:19 +08:00
3gf8jv4dv ec5236368a chore(locale): add new translations of world parts for es and ru (#3714) 2025-03-07 21:39:39 +08:00
3gf8jv4dv 8d3794d828 fix(i18n): split game.version to make different scenarios appropriate (#3709)
- `game.version` indicates the currently used game instance
in the game crash window, not the game version.

- Change the i18n key previously used in ModListPageSkin to
`mods.game.version`, to be consistent with the world section.
2025-03-07 21:06:34 +08:00
3gf8jv4dv d1b59ef553 fix(locale): revise spanish (#3713)
Fix some mistakes. :\
2025-03-07 20:20:04 +08:00
3gf8jv4dv 79f29e143e fix(locale): clean up language typos (#3707) 2025-03-07 19:35:05 +08:00
Glavo e7ffb0b271 优化世界管理页面 (#3711)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
2025-03-07 19:19:04 +08:00
3gf8jv4dv cda2f6fb82 chore(locale): improve spanish translation (#3712)
- Add translation of the `game.crash` part.
- Improve further the translation of the remaining parts.
2025-03-07 17:34:40 +08:00
3gf8jv4dv 114aa4ce94 chore(locale): add missing translations for spanish (#3710)
Several i18n keys were added during the 3.6.12 semester.
This patch adds those translations.
2025-03-07 17:12:34 +08:00
Glavo 0ff194b391 Fix #3485: 修复游戏 Java 路径显示错误的问题 (#3708) 2025-03-06 12:30:13 +08:00
3gf8jv4dvandGlavo 025bde66d6 chore(locale): improve russian translation (game crash part) (#3704)
* chore(locale): improve russian translation (game crash part)

- Re-translate something from English
- Fix and unify the style of text left by different contributors
- Fix some inconsistent/incorrect terminology
- Add missing translations

* Update I18N_ru.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-06 12:00:37 +08:00
Glavo 872c1943b7 修复 Javadoc 中的无效链接 (#3703) 2025-03-05 19:00:22 +08:00
Glavo d268d923d7 更新全部 SVG 图标至 Material Symbols (#3687) 2025-03-05 16:39:21 +08:00
3gf8jv4dv d523ccd312 fix(ui&locale): optimize the create offline account dialog (#3695)
- Adjust lblErrorMessage for invalid username to avoid ui truncation
- Improve the wording of warnings
2025-03-05 16:05:02 +08:00
3gf8jv4dv 78a5ade35a fix(locale): fix english translation of game.crash.reason.file_changed (#3699)
My mistake in https://github.com/HMCL-dev/HMCL/pull/3367
2025-03-05 16:02:56 +08:00
Glavo f011a14798 在 DDE 上禁用自绘窗口动画 (#3701) 2025-03-05 16:01:48 +08:00
Glavo f4701c678e Revert "启用高帧率动画 (#3655)" (#3700)
This reverts commit a45b636b1c.
2025-03-05 14:45:02 +08:00
Zkitefly e61fb27c61 更新 PURCHASE_URL (#3675)
* Update YggdrasilService.java

* Update YggdrasilService.java
2025-03-05 02:54:39 +08:00
3gf8jv4dv d2f58a89a2 fix(locale): remove duplicate extensions (#3692)
Oops... Didn't notice this before.

The issue first appeared in https://github.com/HMCL-dev/HMCL/pull/2468
2025-03-03 15:58:53 +08:00
3gf8jv4dvandGlavo 5174e0a67a chore(locale): revise english (#3688)
* chore(locale): revise english

`settings.launcher.launcher_log.reveal`
Considering that HMCL is a cross-platform launcher and not all systems
use the term `Explorer`, this should be changed to a more generic term.

`update.channel.dev.hint` and `update.channel.nightly.hint`
Revise text style.

Misc
Resolve rendering issues on some text editors.

* Update I18N.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-03 10:41:03 +08:00
3gf8jv4dv 4225e98d6b chore(locale): improve russian translation (curseforge category) (#3686)
- Add translations
- Some proper nouns (mod name) keep the original name
- Some proper nouns (game content) follow the game and wiki
2025-03-03 02:12:46 +08:00
Glavo 0540f3c9cd 在世界管理界面显示世界图标 (#3689)
* 在世界管理界面显示世界图标

* update

* update
2025-03-03 02:12:07 +08:00
Glavo 4292d47fe8 Bump Gson to 2.12.1 (#3691) 2025-03-02 23:58:07 +08:00
3gf8jv4dv 4e98d80855 chore(locale): improve russian translation (setting, launch, menu, etc) (#3684)
Fully revision, except for the `curse` and `game.crash.reason` parts.

- Re-translate something from English
- Fix some inconsistent terminology
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
- Revise some incorrect/outdated translations
2025-03-02 23:56:55 +08:00
Glavo 578aaafb0a 使用 DDE 桌面时启用窗口动画 (#3682) 2025-03-02 00:28:44 +08:00
3gf8jv4dv 60deea64c8 chore(locale): improve russian translation (mods & nbt) (#3678)
- add `mods.check_updates.empty`
- add nbt translations
2025-03-01 21:05:08 +08:00
3gf8jv4dv 7e1cd4319b chore(locale): improve russian translation (modpack) (#3679)
- Revise some translations based on English
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
2025-03-01 21:04:54 +08:00
Glavo 318da840f9 修复对话框关闭动画缺失的问题 (#3676) 2025-03-01 21:01:51 +08:00
Glavo 0c7046d764 优化 TransitionPane (#3680)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* Fix checkstyle
2025-03-01 20:50:31 +08:00
Glavo bdf4886445 禁止标题栏中返回键获取焦点 (#3677) 2025-03-01 10:20:39 +08:00
Glavo 4a758d045d 使更多标签支持双击复制 (#3673) 2025-03-01 01:47:56 +08:00
Glavo 1059a4b3d4 支持关闭鼠标进入 RipplerContainer 时的渐变动画 (#3670)
* update

* fix checkstyle
2025-02-28 23:56:42 +08:00
Glavo a25eb14da4 优化 TransitionPane (#3668) 2025-02-28 23:41:40 +08:00
Glavo 349b663c83 禁止标题栏按钮获取焦点 (#3666) 2025-02-28 01:54:53 +08:00
Glavo 04781da3a7 清理 FXUtils (#3667) 2025-02-28 01:53:31 +08:00
Glavo 96e1ddff79 Close #3664: 支持双击复制世界信息 (#3665)
* Close #3664: 支持双击复制世界信息

* update

* update
2025-02-27 23:58:20 +08:00
3gf8jv4dv e8502890a4 chore(locale): improve russian translation (game setting / management) (#3661)
- Re-translate something from English
- Fix some inconsistent terminology
- Revise text to fit UI
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
2025-02-27 21:19:31 +08:00
3gf8jv4dv 4d0619a9df chore(locale): improve russian translation (javafx dialog / search) (#3662) 2025-02-27 21:18:57 +08:00
3gf8jv4dv fba14737f9 chore(locale): revise unofficial hint for russian (#3660)
- add `unofficial.hint`
- fix typo in `account.methods.microsoft.snapshot`
2025-02-27 20:56:59 +08:00
3gf8jv4dv 5de98399c5 chore(locale): add wiki translation for russian (#3659) 2025-02-27 20:56:41 +08:00
Glavo 7adfa0e3d1 启动时尝试从 HMCL_DIRECTORY 中加载字体 (#3663)
* 启动时尝试从 HMCL_DIRECTORY 中加载字体

* update

* update
2025-02-27 15:05:51 +08:00
Glavo a45b636b1c 启用高帧率动画 (#3655)
* 启用高帧率动画

* update
2025-02-27 09:51:06 +08:00
Glavo 50c22e1f43 添加最小化和还原动画 (#3657)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* fix checkstyle
2025-02-26 20:39:49 +08:00
Glavo 3b38c22398 修复标题过长时窗口异常的问题 (#3654) 2025-02-26 02:00:27 +08:00
Glavo 618243350e 优化 NBT 查看器 (#3653)
* update

* update

* update
2025-02-26 01:40:36 +08:00
Glavo ff4940e79f 添加关闭启动器时的淡出动画 (#3652)
* 添加关闭启动器时的淡出动画

* update
2025-02-26 01:04:12 +08:00
Glavo 1c163934ee Fix #2048: 当微软账户 Token 过期时应当刷新账户 (#3651) 2025-02-25 20:50:49 +08:00
Glavo c513d00c50 添加启动动画 (#3442)
* update

* update

* update
2025-02-25 18:20:44 +08:00
3gf8jv4dv 2c9549b36b fix(ui): limit width of FileSelector in offline skin pane (#3623)
make the text around the selector not be covered.

resolves https://github.com/HMCL-dev/HMCL/issues/3296
2025-02-25 17:58:31 +08:00
Glavo 3389e0a934 Close #3649: 点击账户卡片空白处时切换账户 (#3650)
* Close #3649: 点击账户卡片空白处时切换账户

* update
2025-02-25 17:36:51 +08:00
3gf8jv4dv 78149449a9 chore(locale): improve russian translation (modrinth category) (#3647)
- Add missing translations
- Fix existing translations
2025-02-25 17:33:07 +08:00
3gf8jv4dv b792b23ad4 chore(locale): improve russian translation (web) (#3648) 2025-02-25 17:32:27 +08:00
3gf8jv4dv 881b0e9127 chore(locale): improve russian translation (account part) (#3642)
- Re-translate some translations based on English
- Fix some inconsistent terminology
  - Such as Account, учетная запись > аккаунт
- Add some missing translations
- Reorder some keys to ensure they match with English
- Adjust terminology related to LittleSkin to ensure consistency with
the terminology used on the site
2025-02-23 10:48:45 +08:00
3gf8jv4dv 77e06c8820 chore(locale): adjust english translation of account.portable (#3643)
Match the style of the account type shown for the account item
on account list page
2025-02-23 10:48:03 +08:00
Glavo bc2405cf12 修复在 macOS 平台无法找到通过 DMG 安装的 Java 的问题 (#3645) 2025-02-23 10:47:25 +08:00
Glavo 72b5428f8b 修复 NeoForge 版本识别错误的问题 (#3638) 2025-02-22 17:28:25 +08:00
Glavo e7e8d45497 禁止整合包复制全局游戏设置 (#3639)
* 禁止整合包复制全局游戏设置

* update
2025-02-22 14:35:54 +08:00
3gf8jv4dv 82dbe18d78 fix(i18n): fix a missing i18n for LittleSkin term (#3637)
The word "LittleSkin" appears twice in OfflineAccountSkinPane, but one
of them is hardcoded. Add i18n support for the hardcoded one.
2025-02-22 08:36:34 +08:00
3gf8jv4dv ecb7ef0867 fix(locale): remove accidental japanese characters from russian (#3636)
This bug first appeared in https://github.com/HMCL-dev/HMCL/pull/2412
2025-02-22 00:50:59 +08:00
Glavo d40742a624 Bump jsoup to 1.18.3 (#3632) 2025-02-19 16:28:46 +08:00
Glavo 6f8002a0cf Fix typo in FXUtils (#3631) 2025-02-19 15:34:08 +08:00
3gf8jv4dv 3d3896c5c4 fix(i18n): add i18n support for steve & alex option in offline skin pane (#3630)
Some Slavic Minecraft communities have translated Steve and Alex,
such as Russian and Ukrainian. They need a translation.

ref:
- https://ru.minecraft.wiki/w/%D0%A1%D0%BA%D0%B8%D0%BD
- https://uk.minecraft.wiki/w/%D0%A1%D0%BA%D1%96%D0%BD
2025-02-19 15:30:34 +08:00
Glavoand3gf8jv4dv a24f8e2ec0 在游戏设置中添加“复制全局游戏设置”选项 (#3628)
* update

* update

* Fix checkstyle

* update

* fix checkstyle

* 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>

* Fix FXUtils.bindEnum

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-02-19 15:29:50 +08:00
Glavo fce08a6923 删除全局游戏设置页面的离线账户更换皮肤提示 (#3629) 2025-02-19 13:38:51 +08:00
Glavo 5fa01c8838 修复在非官方支持的平台上 Minecraft 1.21 与 Sodium 不兼容的问题 (#3622)
* update

* update

* update
2025-02-18 23:58:16 +08:00
Glavo 15a3fb5f59 优化 VersionsPage 搜索框 (#3625)
* update

* update

* update

* update
2025-02-18 23:51:28 +08:00
Glavo d5ab14f476 Fix #3295: 修复离线账户选择 Steve/Alex 皮肤时模型错误的问题 (#3626)
* Fix #3295: 修复离线账户选择 Steve/Alex 皮肤时模型错误的问题

* update
2025-02-18 22:46:52 +08:00
Glavo 554ecfd6d5 Fix #3445: 点击安装整合包时无条件关闭整合包下载对话框 (#3621) 2025-02-17 23:35:58 +08:00
Glavoand3gf8jv4dv c2abe755f4 优化整合包下载对话框 (#3620)
* update

* 优化整合包下载对话框

* update

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

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-02-17 22:44:25 +08:00
Glavo c0410c46ff 在日志中记录 PROCESSOR_IDENTIFIER (#3619) 2025-02-16 21:10:58 +08:00
Glavo da93b4c12d 修复无法识别 Intel64 架构的问题 (#3618) 2025-02-16 17:23:14 +08:00
Zkitefly c16d6e8d5f 添加 NeoForge 的 BMCLAPI 代理支持 (#3605) 2025-02-15 16:38:30 +08:00
Glavo 6df73aeb68 修复语言设置中英语选项名称错误的问题 (#3612) 2025-02-15 12:19:09 +08:00
3gf8jv4dv 23d11eca81 fix(locale): adjust time formats for various languages (#3613)
- Change to a standard and commonly used format.
- Rename i18n key as it is used in more than one place.
2025-02-15 12:16:42 +08:00
Glavo 0e307f3b71 简化 AdvancedListBox (#3611) 2025-02-12 21:53:10 +08:00
Glavo 33b791f2b0 更新日期时间格式 (#3610)
* Revert "improv(i18n): refine time formats on ui (#3608)"

This reverts commit 741f65d37c.

* update
2025-02-12 19:37:05 +08:00
3gf8jv4dv de01946ab2 fix(locale): fix a typo in zh-TW language (#3606) 2025-02-12 17:35:03 +08:00
3gf8jv4dv 5526bac70e fix(locale): change Socks to SOCKS (#3607)
ref: https://en.wikipedia.org/wiki/SOCKS
2025-02-12 17:33:31 +08:00
3gf8jv4dv 741f65d37c improv(i18n): refine time formats on ui (#3608)
- unify time formats using DateTimeFormatter, deprecate `world.time`
- change FormatStyle to make the time detail appropriate
- fix incorrect time format in English locale
2025-02-12 17:32:36 +08:00
Haowei Wen ecafc9d6f0 Fix height computation in ComponentListCell 2025-02-11 23:51:50 +08:00
Glavo 8795d522b0 支持临时隐藏预览版提示 (#3603) 2025-02-11 19:00:03 +08:00
Glavo d8e7e0e97e 更新 mod 数据 (#3601)
* 更新 mod 数据

* update

* update

* update
2025-02-10 22:04:48 +08:00
Glavo 8236dfc7e5 Fix #3595: patchNative may throw NPE when gameVersion is null (#3602) 2025-02-10 19:59:25 +08:00
Glavo 8a15b7d274 修复使用自定义字体时未正确设置字重的问题 (#3598) 2025-02-10 00:41:31 +08:00
3gf8jv4dv fce4c13515 fix(locale): fix zh-tw locale for game crash window buttons (#3596) 2025-02-09 09:28:00 +08:00
Glavo 2f36506099 Fix #3508: Java 下载对话框中切换包类型时不应重置版本字段 (#3594)
* Fix #3508: Java 下载对话框中切换包类型时不应重置版本字段

* fix checkstyle
2025-02-08 18:02:46 +08:00
Glavo 5e60eaf595 修复 macOS 平台 HMCLauncher.sh 不会显示 Java 下载页面的问题 (#3549) 2025-01-29 07:35:19 +08:00
Glavo f70fb4ae08 Update README (#3548) 2025-01-29 07:27:17 +08:00
3gf8jv4dv 38adb48051 Fix qq badge in readme (#3544)
ref: https://shields.io/blog/simple-icons-14
2025-01-25 16:38:55 +08:00
Glavo f972c49762 Fix #3539: 安装 MultiMC 整合包后应当设置图标 (#3541) 2025-01-24 09:11:22 +08:00
Glavo 5afeb3d019 优化 HMCLauncher.sh (#3538) 2025-01-23 18:21:29 +08:00
Burning_TNT 38897fbd2a Fix #3063 修复本地模组管理界面水平滚动条显示异常的问题 (#3463) 2025-01-23 13:51:55 +08:00
ZkiteflyandGlavo 0562042218 Update HMCLauncher.sh (#3265)
* Update HMCLauncher.sh

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-01-23 13:17:33 +08:00
Glavo c5e7ee8aef 优化对 Linux RISC-V 64 平台的支持 (#3536) 2025-01-23 10:23:27 +08:00
Glavo 35b288b800 Fix #3529: 修复在 FreeBSD x86-64 平台未正确替换 LWJGL 本地库的问题 (#3531)
* Fix #3529: 修复在 FreeBSD x86-64 平台未正确替换 LWJGL 本地库的问题

* update

* update

* update
2025-01-20 14:03:11 +08:00
Zkitefly a609d32081 更新版权年份至2025 (#3527) 2025-01-16 11:26:31 +08:00
Haowei Wen 72d8b75643 模组列表页中按下 Esc 时取消选中 (#3523)
* 模组列表页中按下 Esc 时取消选中

目前在模组列表页中选中模组后,按下 Esc 是不会有任何反应的,因为 [ListView](https://github.com/openjdk/jfx/blob/5b074c4c2b4f539301813322a6589cec28121553/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/behavior/ListViewBehavior.java#L108) 会处理这个事件,认为是取消编辑,而不论是否处于编辑模式中。

这个 PR 添加了 `FXUtils.ignoreEvent()` 函数,用于忽略掉对特定事件的处理。然后,我们就可以让 ListView 不处理 Esc 按下事件,然后在模组页添加 Esc 处理逻辑(必须让 ListView 忽略掉这个事件,不然就会被它优先处理)。

理论上所有用了 ListView / JFXListView 的地方都会有这个问题,即当焦点在 ListView 中时,按下 Esc 不会触发正常的事件处理(如返回上一页、关闭对话框)。比如在游戏下载页面中,对某个版本按下右键后,焦点就会转移到其上,此时再按 Esc 就毫无反应。

* 修复 Esc 在版本下载页/模组下载页/数据包页不工作
2025-01-16 11:15:29 +08:00
3gf8jv4dvandyushijinhun 01f9a452f1 Update ISSUE_TEMPLATE [skip ci] (#3446)
* Update ISSUE_TEMPLATE [skip ci]

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:40:49 +08:00
3gf8jv4dvandyushijinhun f2edc8001c Revise Spanish & English (#3465)
* Revise Spanish & English

Overhaul for Spanish. I cannot guarantee it is 100% correct, but I
promise to proofread it. During the overhaul some more mistakes in the
English language were discovered, so let's fix it.

All translations have been revised except for the `game.crash.reason`
series.

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:37:19 +08:00
3gf8jv4dvandyushijinhun d2df515325 Revise README [skip ci] (#3447)
* Revise README [skip ci]

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:32:42 +08:00
3gf8jv4dv 95e39fe87a gha: revise release description [skip ci] (#3474)
unify text style
2025-01-06 03:12:13 +08:00
lijiaxuanand3gf8jv4dv 662bf1a9d4 Update I18N.properties English Translations (#3477)
* Update I18N.properties English Translations

更新了英文语言翻译文件,修复了一些语法错误并且让一些句子更加通顺

* Update I18N.properties

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

* Update I18N.properties

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

* Update I18N.properties

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

* Update I18N.properties

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

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

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

* Update I18N.properties

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

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

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

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-01-06 02:15:58 +08:00
Zkiteflyand3gf8jv4dv bfda297838 检查安装时的非ASCII字符名称 (#3244)
* 检查安装时的非ASCII字符名称

* i18n

* 添加导出模组列表信息功能

* Revert "添加导出模组列表信息功能"

This reverts commit 6505d1c284.

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

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

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

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

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

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

---------

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>
2025-01-05 01:31:44 +08:00
ZkiteflyandGlavo d80f61e2e7 update getAssetIndex() (#3252)
* update getAssetIndex()

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-01-04 21:33:48 +08:00
Zkitefly 1f7b156423 Press ESC in search box to exit search (#3519) 2025-01-04 01:38:11 +08:00
Haowei Wen 56d20a5d08 Fix problems with window resizing in Linux 2025-01-04 01:35:36 +08:00
Haowei Wen b5cea83a8a Workaround for JDK-8344372
https://bugs.openjdk.org/browse/JDK-8344372
2025-01-03 20:15:03 +08:00
Haowei Wen 397edaec5d Press ESC in search box to exit search 2025-01-03 17:08:19 +08:00
Zkitefly 39b8de4824 fix 2025-01-03 17:08:19 +08:00
Zkitefly 5197157a97 添加搜索功能 2025-01-03 17:08:19 +08:00
Glavo d61826ba4c 优化 Release Note (#3516) 2025-01-01 05:37:30 +08:00
Glavo 85886da770 Bump Mesa to 24.3.2 (#3515)
* Bump Mesa to 24.3.2

* update
2025-01-01 05:14:14 +08:00
Haowei Wen 6f53da159a Refine DownloadPage
* Fix cursor is not hand when hovering on ModItem
* Fix clicking on the ModItem in ModVersion dialog would open a new dialog again
* Fix ESC doesn't work in ModVersion dialog
2024-12-31 19:56:38 +08:00
Haowei Wen 78e15d130b Update memory status every 1s, fix #3440
This commit also fixes Lang.getTimer() returns a non-daemon Timer,
preventing Java from exiting after closing the launcher.
2024-12-30 20:14:04 +08:00
Haowei Wen 7d12ef6b09 Fix memory slider indicator is covered by progress bar 2024-12-30 19:39:42 +08:00
Haowei Wen 260f4b6c5d Fix ESC not working in input dialog 2024-12-30 00:17:00 +08:00
Haowei Wen 104534d12f Fix style 2024-12-30 00:07:43 +08:00
Haowei Wen 81b3911dc9 Fix value in text field might not be saved when pressing ESC
https://github.com/HMCL-dev/HMCL/pull/1869#issuecomment-2335027530
2024-12-30 00:02:32 +08:00
Haowei Wen c5b8aabc57 Refactor VersionSetting.clone() & .addListener(), fix javaVersionType is not saved 2024-12-29 22:36:40 +08:00
Haowei Wen f0a064afdd Fix sometimes version specific setting is lost 2024-12-29 22:18:15 +08:00
Haowei Wen 8a4f6af193 Fix NPE due to the order of attaching specificSettingsListener 2024-12-29 21:40:09 +08:00
Haowei Wen e63ee49eaa Remove VersionSetting.global property as it's unused 2024-12-29 20:59:47 +08:00
Haowei Wen d5408c38b3 Copy global settings when specialize version setting 2024-12-29 20:18:58 +08:00
Haowei Wen 5fd52bb808 Move skin cache directory to hmcl dir, close #2970 2024-12-29 18:28:23 +08:00
白铭骢 (Mingcong Bai) cb2b3525b8 fix(DefaultLauncher): also set LIBGL_KOPPER_DRI2=1 for Zink mode (#3503)
The amdgpu DDX is missing support for modifiers, causing Zink to fail.
Disable DRI3 to workaround this issue.

Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093

Signed-off-by: Mingcong Bai <jeffbai@aosc.io>
2024-12-25 19:55:42 +08:00
Glavo 4c6fdf49de Bump JetBrains Annotations to 26.0.1 (#3493) 2024-12-13 00:29:34 +08:00
Burning_TNT 981b55a10e Fix #3456 在更新预览界面没法拖动启动器窗口 (#3464) 2024-12-10 16:20:02 +08:00
Glavo 8460df7d5d 修复 OAuth 登录时轮询过于频繁的问题 (#3490)
* 修复 OAuth 登录时轮询过于频繁的问题

* update
2024-12-09 17:47:42 +08:00
Glavo 727e6746d2 Update versions.txt (#3484)
* Update versions.txt

* update
2024-12-04 12:29:17 +08:00
Glavo 8e0f0e2a38 使用 ONE_PASS_BOX 阴影代替高斯模糊 (#3458)
* 使用 ONE_PASS_BOX 代替高斯模糊

* update

* update

* update
2024-12-02 21:24:58 +08:00
Glavo 3df52b0e8f Fix typo in check-update.yml (#3479) 2024-11-29 20:06:08 +08:00
Glavo 6f0e133bb0 添加 banshanjdk-8 下载地址 (#3476)
* 添加 banshanjdk-8 下载地址

* update

* fix i18n

* fix typo
2024-11-28 20:34:12 +08:00
Glavo 36da64f796 支持加载 WebP 格式的背景图片 (#3457)
* 支持加载 WebP 格式的背景图片

* update

* update

* update

* update
2024-11-28 00:35:08 +08:00
Glavo 4c242d0c80 不再生成 MD5 checksum (#3473) 2024-11-24 14:10:58 +08:00
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
399 changed files with 29999 additions and 10812 deletions

No files matched your search

+25 -19
View File
@@ -1,6 +1,6 @@
name: Bug 反馈 | Bug Feedback
name: Bug 反馈 | Bug Report
description:
反馈一个 HMCL 错误。| Bug Feedback.
反馈一个 HMCL 错误。| File a bug report for HMCL.
title: "[Bug] "
labels: bug
body:
@@ -8,29 +8,35 @@ body:
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) 中获取帮助。
* 该问题确实是 **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.
* The issue is indeed **a bug 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 server](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
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 server](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.
description: |
请输入你遇到问题的平台。
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.
label: 问题描述 | Bug Description
description: |
请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。
Please describe the bug you met in as much detail as possible. Additionally, describe the steps to reproduce this bug.
placeholder: |
1. 点击 HMCL 上的某个按钮 | Click a button named ...
2. 向下翻页 | Scroll down
@@ -42,12 +48,12 @@ body:
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!**
If your launcher crashes, please fill in (or drag the file in) the following input field with the crash report.
If your launcher does not crash, please DO NOT EXIT your launcher, click "Export Launcher Logs" in the "Settings → General → Debug" of the launcher, and drag the exported log to the following input field.
**ATTENTION: The crash report or log file is the key to resolving the bug. Please upload them!**
validations:
required: true
+4 -4
View File
@@ -1,11 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群 | QQ Groups
- name: QQ 群 | QQ Group
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
- name: Discord 频道 | Discord Channel
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ group of Hello Minecraft! Launcher.
- name: Discord 服务器 | Discord Server
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
about: Hello Minecraft! Launcher 的官方 Discord 服务器。| The official Discord server of Hello Minecraft! Launcher.
- name: 其他反馈 | Others
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
+19 -6
View File
@@ -1,22 +1,35 @@
name: 新功能 | Feature Request
description: 为 HMCL 提出新功能 | A new feature for HMCL
description: 为 HMCL 提出新功能。| Suggest a new feature or enhancement for HMCL.
title: "[Feature] "
labels: enhancement
body:
- type: markdown
attributes:
value: 请确认 Issues 列表无重复的项目。| Please make sure that no duplicated issues has already been delivered.
value: |
请确认 Issues 列表无重复的项目。
Please make sure that no duplicate issues have already been submitted.
- type: textarea
id: description
id: summary
attributes:
label: 描述 | Description
description: 请详细描述你想加入的新功能。| Please describe the new feature detaily.
label: 概述 | Summary
description: |
请介绍你想加入的新功能。
Please describe the new feature.
validations:
required: true
- type: textarea
id: reason
attributes:
label: 原因 | Reason
description: 请描述该功能带来的好处及原因。| Please describe why you want to add the feature into HMCL.
description: |
请描述该功能带来的好处及原因。
Please describe why you want to add the feature or enhancement to HMCL.
validations:
required: true
- type: textarea
id: description
attributes:
label: 详情 | Description
description: |
在这里可以补充描述该功能的具体实现方式或建议。(可选)
Describe implementation details or suggestions here. (Optional)
-46
View File
@@ -1,46 +0,0 @@
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
+16 -10
View File
@@ -50,16 +50,19 @@ jobs:
if: ${{ env.continue == 'true' }}
run: |
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 "*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 | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> 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: |
@@ -69,6 +72,7 @@ jobs:
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
prerelease: true
stable-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
needs: dev-check-update
@@ -114,16 +118,18 @@ jobs:
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 "*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 | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> 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@v1
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
-1
View File
@@ -5,7 +5,6 @@ on:
pull_request:
paths-ignore:
- '**.md'
- 'HMCLauncher/**'
jobs:
build:
+2 -1
View File
@@ -20,6 +20,7 @@ minecraft-exported-crash-info*
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/buildSrc/build/
# idea
.idea
@@ -41,7 +42,6 @@ minecraft-exported-crash-info*
.nb-gradle
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
@@ -52,3 +52,4 @@ minecraft-exported-crash-info*
# test
/hmcl.json
/.hmcl.json
/.hmcl/
+3 -1
View File
@@ -1,2 +1,4 @@
/data.csv
/data.json
/data.json
/mod.json
/modpack.json
+135 -50
View File
@@ -1,3 +1,4 @@
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -8,7 +9,7 @@ import java.security.spec.PKCS8EncodedKeySpec
import java.util.zip.ZipFile
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
alias(libs.plugins.shadow)
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
@@ -25,25 +26,31 @@ val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
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") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
version = "$versionRoot.$buildNumber"
dependencies {
implementation(project(":HMCLCore"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
if (launcherExe == null) {
implementation("org.glavo.hmcl:HMCLauncher:3.6.0.2")
}
}
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"
@@ -88,11 +95,6 @@ val java11 = sourceSets.create("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"
@@ -105,20 +107,29 @@ tasks.jar {
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
tasks.shadowJar {
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
exclude("META-INF/maven/**")
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
minimize {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2024 huangyuhui.",
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
@@ -140,8 +151,9 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
"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(" ")
"jdk.attach/sun.tools.attach",
).joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED"
)
System.getenv("GITHUB_SHA")?.also {
@@ -149,58 +161,50 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
}
}
if (launcherExe != null) {
into("assets") {
from(file(launcherExe))
}
}
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 {
fun convertToBSS(resource: String) {
doFirst {
val cssFile = File(projectDir, "src/main/resources/$resource")
val bssFile = File(projectDir, "build/compiled-resources/${resource.substring(0, resource.length - 4)}.bss")
bssFile.parentFile.mkdirs()
javaexec {
classpath = sourceSets["main"].compileClasspath
mainClass.set("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(tasks["java11Classes"])
into("assets") {
from(project.layout.buildDirectory.file("openjfx-dependencies.json"))
}
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
}
val makeExecutables = tasks.create("makeExecutables") {
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
inputs.file(jarPath)
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
val jarContent = jarPath.readBytes()
ZipFile(jarPath).use { zipFile ->
for (extension in extensions) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")
output.outputStream().use { outputStream ->
zipFile.getInputStream(entry).use { it.copyTo(outputStream) }
outputStream.write(jarContent)
}
createChecksum(output)
}
}
}
}
@@ -208,11 +212,92 @@ tasks.build {
dependsOn(makeExecutables)
}
tasks.create<JavaExec>("run") {
fun parseToolOptions(options: String?): MutableList<String> {
if (options == null)
return mutableListOf()
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.register<JavaExec>("run") {
dependsOn(tasks.jar)
group = "application"
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
vmOptions += "-Dhmcl.offline.auth.restricted=false"
jvmArgs(vmOptions)
val hmclJavaHome = System.getenv("HMCL_JAVA_HOME")
if (hmclJavaHome != null) {
this.executable(
file(hmclJavaHome).resolve("bin")
.resolve(if (System.getProperty("os.name").lowercase().startsWith("windows")) "java.exe" else "java")
)
}
doFirst {
logger.quiet("HMCL_JAVA_OPTS: {}", vmOptions)
logger.quiet("HMCL_JAVA_HOME: {}", hmclJavaHome ?: System.getProperty("java.home"))
}
}
// mcmod data
tasks.register<ParseModDataTask>("parseModData") {
inputFile.set(layout.projectDirectory.file("mod.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
}
tasks.register<ParseModDataTask>("parseModPackData") {
inputFile.set(layout.projectDirectory.file("modpack.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

-123
View File
@@ -1,123 +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/>.
#
import json
import codecs
import re
import sys
from urllib.parse import urlparse, parse_qs
S = ';'
MOD_SEPARATOR = ','
CURSEFORGE_PATTERN1 = re.compile(
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, CURSEFORGE_PATTERN4]:
match = pattern.match(res.path)
if match:
return match.group('modid')
return ''
MCMOD_PATTERN = re.compile(
r'^http(s)?://www\.mcmod\.cn/(class|modpack)/(?P<modid>\d+)\.html$')
def parseMcmod(url):
match = MCMOD_PATTERN.match(url)
if match:
return match.group('modid')
return ''
skip = [
'Minecraft',
'The Building Game'
]
if __name__ == '__main__':
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:
chinese_name = mod['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)
if S in sub_name:
print('Error! ' + chinese_name)
exit(1)
black_lists = [
'Wood Converter'
]
curseforge_id = ''
mcmod_id = ''
links = mod['links']['list']
if 'curseforge' in links and links['curseforge'] and sub_name not in black_lists and chinese_name not in black_lists:
for link in links['curseforge']:
curseforge_id = parseCurseforge(link['url'])
if curseforge_id != '':
break
if curseforge_id == '':
print('Error curseforge ' + chinese_name)
exit(1)
if 'mcmod' in links and links['mcmod']:
mcmod_id = parseMcmod(links['mcmod'][0]['url'])
if mcmod_id == '':
print('Error mcmod ' + chinese_name)
exit(1)
mod_id = []
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)
exit(1)
mod_id.append(id)
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
outfile.write(
f'{curseforge_id}{S}{mcmod_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
print('Success!')
@@ -38,11 +38,14 @@ import org.jackhuang.hmcl.util.StringUtils;
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.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.nio.file.Files;
@@ -53,6 +56,7 @@ import java.util.Collections;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -86,7 +90,7 @@ public final class Launcher extends Application {
}
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& 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)
@@ -100,7 +104,7 @@ public final class Launcher extends Application {
return;
}
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
@@ -148,7 +152,7 @@ public final class Launcher extends Application {
|| configPath.startsWith("/var/cache/")
|| configPath.startsWith("/dev/shm/")
|| configPath.contains("/Trash/");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
return configPath.startsWith("/var/folders/")
|| configPath.startsWith("/private/var/folders/")
|| configPath.startsWith("/tmp/")
@@ -186,8 +190,10 @@ public final class Launcher extends Application {
ArrayList<String> files = new ArrayList<>();
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_DIRECTORY))
files.add(Metadata.HMCL_DIRECTORY.toString());
if (Files.exists(Metadata.HMCL_GLOBAL_DIRECTORY))
files.add(Metadata.HMCL_GLOBAL_DIRECTORY.toString());
if (Files.exists(Metadata.HMCL_CURRENT_DIRECTORY))
files.add(Metadata.HMCL_CURRENT_DIRECTORY.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
@@ -222,28 +228,46 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
LOG.info("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 + ')'));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
}
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Code Page: " + OperatingSystem.CODE_PAGE);
}
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName());
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: " + System.getProperty("user.dir"));
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
LOG.info("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY);
LOG.info("HMCL Current Directory: " + Metadata.HMCL_CURRENT_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");
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"));
LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
try {
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
if ("Metaspace".equals(bean.getName())) {
long bytes = bean.getUsage().getUsed();
LOG.info("Metaspace: " + MEGABYTES.formatBytes(bytes));
break;
}
}
} catch (NoClassDefFoundError ignored) {
}
LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
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"));
}
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
@@ -272,7 +296,7 @@ public final class Launcher extends Application {
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
Lang.executeDelayed(System::gc, TimeUnit.SECONDS, 5, true);
});
}
+62 -14
View File
@@ -20,10 +20,10 @@ package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.FractureiserDetector;
import org.jackhuang.hmcl.util.ModuleHelper;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
@@ -32,6 +32,7 @@ import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -57,20 +58,22 @@ public final class Main {
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
LOG.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
checkDirectoryPath();
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
initIcon();
checkJavaFX();
verifyJavaFX();
detectFractureiser();
addEnableNativeAccess();
enableUnsafeMemoryAccess();
Launcher.main(args);
}
@@ -80,8 +83,34 @@ public final class Main {
System.exit(exitCode);
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
}
}
} catch (IOException e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
}
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
} catch (IOException e) {
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
}
}
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
AwtUtils.setAppleIcon(image);
}
@@ -94,13 +123,6 @@ public final class Main {
}
}
private static void detectFractureiser() {
if (FractureiserDetector.detect()) {
LOG.error("Detected that this computer is infected by fractureiser");
showErrorAndExit(i18n("fatal.fractureiser"));
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
@@ -125,10 +147,36 @@ public final class Main {
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
} catch (ClassNotFoundException e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
}
private static void enableUnsafeMemoryAccess() {
// https://openjdk.org/jeps/498
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
trySetMemoryAccessWarned.setAccessible(true);
trySetMemoryAccessWarned.invoke(null);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
@@ -28,7 +28,8 @@ import java.nio.file.Paths;
* Stores metadata about this application.
*/
public final class Metadata {
private Metadata() {}
private Metadata() {
}
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
@@ -37,18 +38,25 @@ public final class Metadata {
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
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 ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = DOCS_URL + "/groups.html";
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 CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY;
public static final Path HMCL_GLOBAL_DIRECTORY;
public static final Path HMCL_CURRENT_DIRECTORY;
public static final Path DEPENDENCIES_DIRECTORY;
static {
String hmclHome = System.getProperty("hmcl.home");
@@ -56,16 +64,22 @@ public final class Metadata {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
@@ -1,86 +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.countly;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Locale;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Countly {
private String deviceId;
private String endpoint;
private String serverURL;
public void sendMetric(String metrics) throws IOException {
HttpRequest.GET(serverURL + endpoint,
pair("begin_session", "1"),
pair("session_id", "1"),
pair("metrics", metrics),
pair("device_id", deviceId),
pair("timestamp", Long.toString(System.currentTimeMillis())),
pair("tz", Integer.toString(getTimezoneOffset())),
pair("hour", Integer.toString(currentHour())),
pair("dow", Integer.toString(currentDayOfWeek())),
pair("app_key", APP_KEY),
pair("sdk_name", "java-native"),
pair("sdk_version", "20.11.1"))
.getString();
}
private static int getTimezoneOffset() {
return ZonedDateTime.now().getOffset().getTotalSeconds() / 60;
}
private static String getLocale() {
final Locale locale = Locale.getDefault();
return locale.getLanguage() + "_" + locale.getCountry();
}
private static int currentHour() {
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
private int currentDayOfWeek() {
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (day) {
case Calendar.SUNDAY:
return 0;
case Calendar.MONDAY:
return 1;
case Calendar.TUESDAY:
return 2;
case Calendar.WEDNESDAY:
return 3;
case Calendar.THURSDAY:
return 4;
case Calendar.FRIDAY:
return 5;
case Calendar.SATURDAY:
return 6;
}
return 0;
}
private static final String APP_KEY = "";
}
@@ -5,14 +5,8 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class CrashReport {
@@ -20,24 +14,16 @@ public class CrashReport {
private final Throwable throwable;
private final String stackTrace;
private boolean nonFatal;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
nonFatal = false;
}
public Throwable getThrowable() {
return this.throwable;
}
public CrashReport setNonFatal() {
nonFatal = true;
return this;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
@@ -48,23 +34,6 @@ public class CrashReport {
return true;
}
public Map<String, Object> getMetrics(long runningTime) {
return mapOf(
pair("_run", runningTime),
pair("_app_version", Metadata.VERSION),
pair("_os", OperatingSystem.SYSTEM_NAME),
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
pair("_disk_current", getDiskAvailable()),
pair("_disk_total", getDiskTotal()),
pair("_ram_current", getMemoryAvailable()),
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
pair("_error", stackTrace),
pair("_logs", LOG.getLogs()),
pair("_name", throwable.getLocalizedMessage()),
pair("_nonfatal", nonFatal)
);
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
@@ -74,37 +43,12 @@ public class CrashReport {
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" +
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
private static final Long BYTES_IN_MB = 1024L * 1024;
private static long getMemoryAvailable() {
Long total = Runtime.getRuntime().totalMemory();
Long availMem = Runtime.getRuntime().freeMemory();
return (total - availMem) / BYTES_IN_MB;
}
private static long getDiskAvailable() {
long total = 0, free = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
free += f.getUsableSpace();
}
return (total - free) / BYTES_IN_MB;
}
private static long getDiskTotal() {
long total = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
}
return total / BYTES_IN_MB;
}
}
@@ -30,22 +30,21 @@ import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
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.ui.FXUtils;
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.platform.SystemInfo;
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;
@@ -151,18 +150,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
Version fromVersion = getVersion(srcId);
if (Files.exists(dstDir)) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir, dstDir);
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
Path fromJson = dstDir.resolve(srcId + ".json");
Path fromJar = dstDir.resolve(srcId + ".jar");
if (Files.exists(dstDir)) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
Path fromJson = srcDir.resolve(srcId + ".json");
Path fromJar = srcDir.resolve(srcId + ".jar");
Path toJson = dstDir.resolve(dstId + ".json");
Path toJar = dstDir.resolve(dstId + ".jar");
if (Files.exists(fromJar)) {
Files.move(fromJar, toJar);
Files.copy(fromJar, toJar);
}
Files.move(fromJson, toJson);
Files.copy(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
@@ -176,12 +181,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
}
@@ -204,6 +203,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
/**
* Create new version setting if version id has no version setting.
*
* @param id the version id.
* @return new version setting, null if given version does not exist.
*/
@@ -218,7 +218,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
localVersionSettings.put(id, vs);
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
vs.addListener(a -> saveVersionSetting(id));
return vs;
}
@@ -226,7 +226,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
* Get the version setting for version id.
*
* @param id version id
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
@@ -251,7 +250,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
profile.getGlobal().setUsesGlobal(true);
return profile.getGlobal();
} else
@@ -261,24 +259,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
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);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
File file = new File(root, "icon." + extension);
if (file.exists()) {
return Optional.of(file);
}
}
return Optional.empty();
@@ -286,7 +271,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
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")) {
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
throw new IllegalArgumentException("Unsupported icon file: " + ext);
}
@@ -297,11 +282,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
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();
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
new File(root, "icon." + extension).delete();
}
}
public Image getVersionIconImage(String id) {
@@ -315,9 +298,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
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) {
try {
return FXUtils.loadImage(iconFile.get().toPath());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
}
@@ -353,6 +336,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
LOG.info("Saving version setting: " + id);
try {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
@@ -364,6 +349,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
/**
* Make version use self version settings instead of the global one.
*
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
@@ -373,7 +359,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs = createLocalVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
if (vs.isUsesGlobal()) {
vs.setUsesGlobal(false);
}
return vs;
}
@@ -383,7 +371,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -396,7 +384,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
@@ -426,9 +414,14 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
.setJavaAgents(javaAgents);
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
builder.setProxyType(config().getProxyType());
builder.setProxyHost(config().getProxyHost());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
@@ -521,16 +514,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024;
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16384L * 1024 * 1024);
16L * 1024 * 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;
@@ -67,8 +66,7 @@ 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 (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
@@ -64,7 +64,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
}
@Override
public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException {
public Modpack readManifest(ZipArchiveReader 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");
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
@@ -28,21 +27,23 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -55,19 +56,19 @@ import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
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;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
@@ -131,13 +132,14 @@ public final class LauncherHelper {
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
List<String> javaArguments = new ArrayList<>(0);
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), javaVersion, setting));
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments));
if (setting.isNotCheckGame())
return null;
return Task.allOf(
@@ -155,7 +157,7 @@ public final class LauncherHelper {
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(javaVersion, renderer);
Library lib = NativePatcher.getMesaLoader(java);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -179,12 +181,14 @@ public final class LauncherHelper {
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null);
})
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
LaunchOptions launchOptions = repository.getLaunchOptions(
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
return new HMCLGameLauncher(
repository,
version.get(),
@@ -214,7 +218,7 @@ public final class LauncherHelper {
it.fireEvent(new DialogCloseEvent());
}));
} else {
Platform.runLater(() -> {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
@@ -234,7 +238,7 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
Platform.runLater(() -> {
runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
@@ -251,9 +255,9 @@ public final class LauncherHelper {
} else if (ex instanceof PermissionException) {
message = i18n("launch.failed.executable_permission");
} else if (ex instanceof ProcessCreationException) {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
message = i18n("launch.failed.creating_process") + "\n" + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
message = i18n("launch.failed.decompressing_natives") + "\n" + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n";
if (ex.getCause() instanceof ResponseCodeException) {
@@ -331,178 +335,181 @@ public final class LauncherHelper {
executor.start();
}
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
private static Task<JavaRuntime> checkGameState(Profile profile, VersionSetting setting, Version version) {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
try {
return setting.getJava(gameVersion, version);
} catch (InterruptedException e) {
throw new CancellationException();
}
});
Task<JavaRuntime> task;
if (setting.isNotCheckJVM()) {
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
.withStage("launch.state.java");
}
task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault()));
} else if (setting.getJavaVersionType() == JavaVersionType.AUTO || setting.getJavaVersionType() == JavaVersionType.VERSION) {
task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> {
if (java != null) {
return Task.completed(java);
}
return Task.composeAsync(() -> {
return setting.getJavaVersion(gameVersion, version);
}).thenComposeAsync(Schedulers.javafx(), javaVersion -> {
// Reset invalid java version
if (javaVersion == null) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
// Reset invalid java version
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("No accepted java"));
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM);
if (setting.isJavaAutoSelected()) {
GameJavaVersion targetJavaVersion = null;
GameJavaVersion targetJavaVersion = null;
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
try {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
if (range.getMandatory().contains(VersionNumber.asVersion("21.0.3"))) {
targetJavaVersion = GameJavaVersion.JAVA_21;
} else if (range.getMandatory().contains(VersionNumber.asVersion("17.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_17;
} else if (range.getMandatory().contains(VersionNumber.asVersion("16.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else {
String java8Version;
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
Controllers.dialog(
i18n("launch.failed.java_version_too_low"),
i18n("message.error"),
MessageType.ERROR,
breakAction
);
return result;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
java8Version = "1.8.0_51";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
java8Version = "1.8.0_202";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
java8Version = "1.8.0_74";
} else {
java8Version = null;
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
} catch (NumberFormatException ignored) {
}
} else
targetJavaVersion = version.getJavaVersion();
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
.whenCompleteAsync((downloadedJava, exception) -> {
if (exception == null) {
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", exception);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
}, Schedulers.javafx());
} else {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
return result;
});
} else {
task = getJavaTask.thenComposeAsync(java -> {
Set<JavaVersionConstraint> violatedMandatoryConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
Set<JavaVersionConstraint> violatedSuggestedConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
if (java != null) {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violatedMandatoryConstraints.add(constraint);
} else {
violatedSuggestedConstraints.add(constraint);
}
}
if (java8Version != null && range.getMandatory().contains(VersionNumber.asVersion(java8Version)))
targetJavaVersion = GameJavaVersion.JAVA_8;
else
targetJavaVersion = null;
}
}
}
if (targetJavaVersion == null) {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version);
if (suggestedJava != null) {
FXUtils.runInFX(() -> {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJava);
}, breakAction);
});
return result;
} else if (java == null) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.invalid_java"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
} else {
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
.thenAcceptAsync(downloadedJavaVersion -> {
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
else
gameJavaVersion = null;
if (gameJavaVersion != null) {
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> {
if (throwable == null) {
setting.setJavaAutoSelected();
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", throwable);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
});
return null;
});
}
} else {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
breakAction.run();
}
}, Schedulers.javafx()));
return result;
}
setting.setJava(null);
setting.setDefaultJavaPath(null);
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
}
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
violatedMandatoryConstraints.remove(JavaVersionConstraint.VANILLA_LINUX_JAVA_8);
}
}
return Task.fromCompletableFuture(future);
} else {
return Task.completed(javaVersion);
}
}).thenComposeAsync(javaVersion -> {
return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version)));
}).thenComposeAsync(Schedulers.javafx(), javaVersions -> {
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.LAUNCH_WRAPPER)) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
JavaVersionConstraint violatedMandatoryConstraint = null;
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
violatedMandatoryConstraint = constraint;
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
if (violatedSuggestedConstraints == null)
violatedSuggestedConstraints = new ArrayList<>(1);
violatedSuggestedConstraints.add(constraint);
if (!violatedMandatoryConstraints.isEmpty()) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.unknown") + "\n" + violatedMandatoryConstraints,
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
}
}
}
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
List<String> suggestions = new ArrayList<>();
if (violatedMandatoryConstraint != null) {
if (suggestedJavaVersion != null) {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJavaVersion);
}, breakAction);
return Task.fromCompletableFuture(future);
} else {
switch (violatedMandatoryConstraint) {
case GAME_JSON:
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
.thenAcceptAsync(downloadedJavaVersion -> {
setting.setJavaVersion(downloadedJavaVersion);
future.complete(downloadedJavaVersion);
}, Schedulers.javafx())
.whenCompleteAsync((result, throwable) -> {
LOG.warning("Failed to download java", throwable);
breakAction.run();
}, Schedulers.javafx());
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_16:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_17:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_21:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 21), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
case VANILLA_LINUX_JAVA_8:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
} else {
break;
}
case LAUNCH_WRAPPER:
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && java.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
}
List<String> suggestions = new ArrayList<>(0);
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
// 32-bit JVM cannot make use of too much memory.
if (javaVersion.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (violatedSuggestedConstraints != null) {
for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) {
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
@@ -510,7 +517,7 @@ public final class LauncherHelper {
break;
case MODDED_JAVA_8:
// Minecraft>=1.7.10+Forge accepts Java 8
if (javaVersion.getParsedVersion() < 8)
if (java.getParsedVersion() < 8)
suggestions.add(i18n("launch.advice.newer_java"));
else
suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion));
@@ -526,6 +533,9 @@ public final class LauncherHelper {
case MODDED_JAVA_17:
suggestions.add(i18n("launch.advice.modded_java", 17, gameVersion));
break;
case MODDED_JAVA_21:
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
break;
case VANILLA_JAVA_8_51:
suggestions.add(i18n("launch.advice.java8_51_1_13"));
break;
@@ -534,117 +544,92 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
&& isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
default:
suggestions.add(violatedSuggestedConstraint.name());
}
}
}
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
VersionNumber forgeVersion = version.getLibraries().stream()
.filter(it -> it.is("net.minecraftforge", "forge"))
.findFirst()
.map(library -> VersionNumber.asVersion(library.getVersion()))
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(javaVersion);
// Cannot allocate too much memory exceeding free space.
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
}
} else {
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
.map(VersionNumber::asVersion)
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(java);
}
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
}
FXUtils.runInFX(() -> Controllers.confirm(
message,
i18n("message.warning"),
MessageType.WARNING,
() -> future.complete(java),
breakAction));
}
Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction);
}
return result;
});
}
return Task.fromCompletableFuture(future);
}).withStage("launch.state.java");
return task.withStage("launch.state.java");
}
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
link.setOnAction(e -> {
if (javaVersion.getMajorVersion() == JavaVersion.JAVA_8) {
FXUtils.openLink(ORACLEJDK_DOWNLOAD_LINK);
} else {
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}
future.completeExceptionally(new CancellationException());
});
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version",
gameVersion,
javaVersion.getMajorVersion()),
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.addAction(link)
.yesOrNo(() -> {
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
.thenAcceptAsync(future::complete)
.exceptionally(throwable -> {
Throwable resolvedException = resolveException(throwable);
LOG.warning("Failed to download java", throwable);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider();
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
future.complete(result);
} else {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
future.completeExceptionally(new CancellationException());
}
future.completeExceptionally(new CancellationException());
return null;
});
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
}, () -> future.completeExceptionally(new CancellationException())).build());
return future;
}
/**
* Directly start java downloading.
*
* @param javaVersion target Java version
* @param downloadProvider download provider
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
*/
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Controllers.taskDialog(JavaRepository.downloadJava(javaVersion, downloadProvider)
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
if (exception != null) {
future.completeExceptionally(exception);
} else {
future.complete(downloadedJava);
}
}), i18n("download.java"), TaskCancellationAction.NORMAL);
return future;
}
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
@@ -685,7 +670,7 @@ public final class LauncherHelper {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
@@ -696,9 +681,9 @@ public final class LauncherHelper {
case CLOSE:
throw new Error("Never get to here");
case HIDE:
Platform.runLater(() -> {
runLater(() -> {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
setImplicitExit(true);
// If we use Launcher.stop(), log window will be halt immediately.
Launcher.stopWithoutPlatform();
});
@@ -717,14 +702,14 @@ public final class LauncherHelper {
private final Version version;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private volatile boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final ArrayDeque<String> logs;
private final ArrayDeque</*Log4jLevel*/Object> levels;
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CircularArrayList<Log> logs;
private final CountDownLatch launchingLatch;
private final String forbiddenAccessToken;
private Thread submitLogThread;
private LinkedBlockingQueue<Log> logBuffer;
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
@@ -733,10 +718,7 @@ public final class LauncherHelper {
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
final int numLogs = config().getLogLines() + 1;
this.logs = new ArrayDeque<>(numLogs);
this.levels = new ArrayDeque<>(numLogs);
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
}
@Override
@@ -752,18 +734,66 @@ public final class LauncherHelper {
LOG.info("Process ClassPath: " + classpath);
}
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow(process);
logWindow.showNormal();
if (showLogs) {
CountDownLatch logWindowLatch = new CountDownLatch(1);
runLater(() -> {
logWindow = new LogWindow(process, logs);
logWindow.show();
logWindowLatch.countDown();
});
logBuffer = new LinkedBlockingQueue<>();
submitLogThread = Lang.thread(new Runnable() {
private final ArrayList<Log> currentLogs = new ArrayList<>();
private final Semaphore semaphore = new Semaphore(0);
private void submitLogs() {
if (currentLogs.size() == 1) {
Log log = currentLogs.get(0);
runLater(() -> logWindow.logLine(log));
} else {
runLater(() -> {
logWindow.logLines(currentLogs);
semaphore.release();
});
semaphore.acquireUninterruptibly();
}
currentLogs.clear();
}
@Override
public void run() {
while (true) {
try {
currentLogs.add(logBuffer.take());
//noinspection BusyWait
Thread.sleep(200); // Wait for more logs
} catch (InterruptedException e) {
break;
}
logBuffer.drainTo(currentLogs);
submitLogs();
}
do {
submitLogs();
} while (logBuffer.drainTo(currentLogs) > 0);
}
}, "Game Log Submitter", true);
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void finishLaunch() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -776,11 +806,11 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
Platform.runLater(launchingLatch::countDown);
runLater(launchingLatch::countDown);
break;
case HIDE:
launchingLatch.countDown();
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -796,44 +826,37 @@ public final class LauncherHelper {
@Override
public void onLog(String log, boolean isErrorStream) {
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
if (isErrorStream)
System.err.println(filteredLog);
System.err.println(log);
else
System.out.println(filteredLog);
System.out.println(log);
Log4jLevel level;
if (isErrorStream)
level = Log4jLevel.ERROR;
else
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
synchronized (this) {
logs.add(filteredLog);
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
if (logs.size() > config().getLogLines()) {
logs.removeFirst();
levels.removeFirst();
}
}
log = StringUtils.parseEscapeSequence(log);
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
if (showLogs) {
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
if (level == null)
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
}
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl) {
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
lwjgl = true;
finishLaunch();
synchronized (this) {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
}
}
}
}
@@ -841,7 +864,13 @@ public final class LauncherHelper {
@Override
public void onExit(int exitCode, ExitType exitType) {
if (showLogs) {
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
submitLogThread.interrupt();
try {
submitLogThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
launchingLatch.countDown();
@@ -850,14 +879,16 @@ public final class LauncherHelper {
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (!lwjgl) {
synchronized (this) {
if (!lwjgl)
finishLaunch();
}
}
if (exitType != ExitType.NORMAL) {
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
Lang.forEachZipped(logs, levels,
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}
checkExit();
@@ -865,9 +896,6 @@ public final class LauncherHelper {
}
private static final String ORACLEJDK_DOWNLOAD_LINK = "https://www.java.com/download";
private static final String OPENJDK_DOWNLOAD_LINK = "https://docs.microsoft.com/java/openjdk/download";
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
@@ -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,12 +17,13 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.io.IOUtils;
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.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
@@ -93,9 +94,8 @@ public final class LogExporter {
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());
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
}
@@ -18,8 +18,7 @@
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 kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
@@ -84,7 +83,7 @@ public final class ModpackHelper {
}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
// Order for trying detecting manifest is necessary here.
// Do not change to iterating providers.
for (ModpackProvider provider : new ModpackProvider[]{
@@ -141,8 +140,7 @@ 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");
}
@@ -87,7 +87,7 @@ public final class TexturesLoader {
}
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
private static final Path TEXTURES_DIR = Metadata.MINECRAFT_DIRECTORY.resolve("assets").resolve("skins");
private static final Path TEXTURES_DIR = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("skins");
private static Path getTexturePath(Texture texture) {
String url = texture.getUrl();
@@ -0,0 +1,232 @@
/*
* 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.MACOS) {
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());
}
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
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.MACOS)
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(), isManaged));
}
}
} catch (Throwable e) {
LOG.warning("Failed to parse " + file, e);
}
}
} catch (IOException ignored) {
}
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
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.MACOS)
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 kala.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,667 @@
/*
* 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.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.platform.windows.WinReg;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
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_GLOBAL_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.MACOS) {
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 MACOS:
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.MACOS)
&& (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 MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.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 MACOS:
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
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.MACOS) {
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(File.pathSeparator);
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(File.pathSeparator);
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.MACOS) {
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.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_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));
}
}
private static void searchJavaInMacJavaVirtualMachines(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.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
}
@@ -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,12 +17,12 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.reflect.TypeToken;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.*;
@@ -51,10 +51,13 @@ import java.util.*;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
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.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;
@@ -171,14 +174,11 @@ public final class Accounts {
config().getAccountStorages().setAll(portable);
}
@SuppressWarnings("unchecked")
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
Path globalAccountsFile = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll((List<Map<Object, Object>>)
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
}.getType()));
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
} catch (Throwable e) {
LOG.warning("Failed to load global accounts", e);
}
@@ -279,6 +279,30 @@ public final class Accounts {
selected = accounts.get(0);
}
if (!globalConfig().isEnableOfflineAccount())
for (Account account : accounts) {
if (account instanceof MicrosoftAccount) {
globalConfig().setEnableOfflineAccount(true);
break;
}
}
if (!globalConfig().isEnableOfflineAccount())
accounts.addListener(new ListChangeListener<Account>() {
@Override
public void onChanged(Change<? extends Account> change) {
while (change.next()) {
for (Account account : change.getAddedSubList()) {
if (account instanceof MicrosoftAccount) {
accounts.removeListener(this);
globalConfig().setEnableOfflineAccount(true);
return;
}
}
}
}
});
selectedAccount.set(selected);
InvalidationListener listener = o -> {
@@ -364,7 +388,7 @@ public final class Accounts {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(
Metadata.HMCL_DIRECTORY.resolve("authlib-injector.jar"),
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
@@ -373,7 +397,7 @@ public final class Accounts {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Paths.get("authlib-injector.jar"));
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
}
};
} else {
@@ -465,6 +489,8 @@ public final class Accounts {
return i18n("account.methods.microsoft.error.country_unavailable");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
return i18n("account.methods.microsoft.error.missing_xbox_account");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.BANNED) {
return i18n("account.methods.microsoft.error.banned");
} else {
return i18n("account.methods.microsoft.error.unknown", errorCode);
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.ToNumberPolicy;
import com.google.gson.annotations.SerializedName;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -47,7 +48,7 @@ import java.net.Proxy;
import java.util.Map;
import java.util.TreeMap;
public final class Config implements Cloneable, Observable {
public final class Config implements Observable {
public static final int CURRENT_UI_VERSION = 0;
@@ -60,6 +61,7 @@ public final class Config implements Cloneable, Observable {
.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()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.create();
@Nullable
@@ -164,7 +166,7 @@ public final class Config implements Cloneable, Observable {
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@@ -223,11 +225,6 @@ public final class Config implements Cloneable, Observable {
return CONFIG_GSON.toJson(this);
}
@Override
public Config clone() {
return fromJson(this.toJson());
}
// Getters & Setters & Properties
public String getSelectedProfile() {
return selectedProfile.get();
@@ -573,15 +570,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;
}
@@ -39,7 +39,7 @@ public final class ConfigHolder {
public static final String CONFIG_FILENAME = "hmcl.json";
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("config.json");
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("config.json");
private static Path configLocation;
private static Config configInstance;
@@ -93,19 +93,9 @@ public final class ConfigHolder {
LOG.setLogRetention(globalConfig().getLogRetention());
Settings.init();
if (newlyCreated) {
if (newlyCreated)
saveConfigSync();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + configLocation, e);
}
}
}
if (!Files.isWritable(configLocation)) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
@@ -121,12 +111,14 @@ public final class ConfigHolder {
}
private static Path locateConfig() {
Path exePath = Paths.get("").toAbsolutePath();
Path defaultConfigFile = Metadata.HMCL_CURRENT_DIRECTORY.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(defaultConfigFile))
return defaultConfigFile;
try {
Path jarPath = JarUtils.thisJarPath();
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
Path config = jarPath.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(config))
@@ -149,7 +141,7 @@ public final class ConfigHolder {
return dotConfig;
// create new
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
return defaultConfigFile;
}
private static Config loadConfig() throws IOException {
@@ -197,7 +189,7 @@ public final class ConfigHolder {
}
}
static void markConfigDirty() {
private static void markConfigDirty() {
configWriter.accept(configInstance.toJson());
}
@@ -241,11 +233,7 @@ public final class ConfigHolder {
}
}
static void markGlobalConfigDirty() {
private static void markGlobalConfigDirty() {
globalConfigWriter.accept(globalConfigInstance.toJson());
}
private static void saveGlobalConfigSync() throws IOException {
writeToConfig(globalConfigInstance.toJson());
}
}
@@ -0,0 +1,200 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class FontManager {
public static final String[] FONT_EXTENSIONS = {
"ttf", "otf", "woff"
};
public static final double DEFAULT_FONT_SIZE = 12.0f;
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
if (font != null)
return font;
font = tryLoadDefaultFont(Metadata.CURRENT_DIRECTORY);
if (font != null)
return font;
font = tryLoadDefaultFont(Metadata.HMCL_GLOBAL_DIRECTORY);
if (font != null)
return font;
Path thisJar = JarUtils.thisJarPath();
if (thisJar != null && thisJar.getParent() != null) {
font = tryLoadDefaultFont(thisJar.getParent());
if (font != null)
return font;
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
&& Locale.getDefault() != Locale.ROOT
&& !"en".equals(Locale.getDefault().getLanguage()))
return findByFcMatch();
else
return null;
});
private static final ObjectProperty<Font> fontProperty;
static {
String fontFamily = config().getLauncherFontFamily();
if (fontFamily == null)
fontFamily = System.getProperty("hmcl.font.override");
if (fontFamily == null)
fontFamily = System.getenv("HMCL_FONT");
Font font = fontFamily == null ? DEFAULT_FONT.get() : Font.font(fontFamily, DEFAULT_FONT_SIZE);
fontProperty = new SimpleObjectProperty<>(font);
LOG.info("Font: " + (font != null ? font.getName() : Font.getDefault().getName()));
fontProperty.addListener((obs, oldValue, newValue) -> {
if (newValue != null)
config().setLauncherFontFamily(newValue.getFamily());
else
config().setLauncherFontFamily(null);
});
}
private static Font tryLoadDefaultFont(Path dir) {
for (String extension : FONT_EXTENSIONS) {
Path path = dir.resolve("font." + extension);
if (Files.isRegularFile(path)) {
try {
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font != null) {
return font;
}
} catch (MalformedURLException ignored) {
}
LOG.warning("Failed to load font " + path);
}
}
return null;
}
public static Font findByFcMatch() {
Path fcMatch = SystemUtils.which("fc-match");
if (fcMatch == null)
return null;
try {
String result = SystemUtils.run(fcMatch.toString(),
":lang=" + Locale.getDefault().toLanguageTag(),
"--format", "%{family}\\n%{file}").trim();
String[] results = result.split("\\n");
if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) {
LOG.warning("Unexpected output from fc-match: " + result);
return null;
}
String family = results[0].trim();
String path = results[1];
Path file = Paths.get(path).toAbsolutePath().normalize();
if (!Files.isRegularFile(file)) {
LOG.warning("Font file does not exist: " + path);
return null;
}
if (JavaRuntime.CURRENT_VERSION >= 9) {
try {
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
MethodType.methodType(Font[].class, String.class, double.class));
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (NoSuchMethodException | IllegalAccessException ignored) {
}
}
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font == null)
LOG.warning("Failed to load font from " + path);
return font;
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
}
}
public static ObjectProperty<Font> fontProperty() {
return fontProperty;
}
public static Font getFont() {
return fontProperty.get();
}
public static void setFont(Font font) {
fontProperty.set(font);
}
public static void setFontFamily(String fontFamily) {
setFont(fontFamily != null ? Font.font(fontFamily, DEFAULT_FONT_SIZE) : null);
}
private FontManager() {
}
}
@@ -21,8 +21,12 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
@@ -31,7 +35,7 @@ import java.lang.reflect.Type;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public final class GlobalConfig implements Cloneable, Observable {
public final class GlobalConfig implements Observable {
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
@@ -51,6 +55,12 @@ public final class GlobalConfig implements Cloneable, Observable {
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
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 final transient ObservableHelper helper = new ObservableHelper(this);
@@ -73,11 +83,6 @@ public final class GlobalConfig implements Cloneable, Observable {
return Config.CONFIG_GSON.toJson(this);
}
@Override
public GlobalConfig clone() {
return fromJson(this.toJson());
}
public int getAgreementVersion() {
return agreementVersion.get();
}
@@ -114,11 +119,34 @@ public final class GlobalConfig implements Cloneable, Observable {
this.logRetention.set(logRetention);
}
public boolean isEnableOfflineAccount() {
return enableOfflineAccount.get();
}
public BooleanProperty enableOfflineAccountProperty() {
return enableOfflineAccount;
}
public void setEnableOfflineAccount(boolean value) {
enableOfflineAccount.set(value);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
public ObservableSet<String> getDisabledJava() {
return disabledJava;
}
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"platformPromptVersion",
"logRetention"
"logRetention",
"userJava",
"disabledJava",
"enableOfflineAccount"
));
@Override
@@ -131,6 +159,15 @@ public final class GlobalConfig implements Cloneable, Observable {
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
if (src.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
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()));
}
@@ -148,6 +185,21 @@ public final class GlobalConfig implements Cloneable, Observable {
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).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));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
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())) {
@@ -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
}
@@ -188,7 +188,7 @@ public final class Profile implements Observable {
global.addListener(listener);
gameDir.addListener(listener);
useRelativePath.addListener(listener);
global.get().addPropertyChangedListener(listener);
global.get().addListener(listener);
selectedVersion.addListener(listener);
}
@@ -17,100 +17,118 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.io.IOException;
import java.net.*;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private ProxyManager() {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static ProxySelector getProxySelector() {
if (config().hasProxy()) {
Proxy.Type proxyType = config().getProxyType();
String host = config().getProxyHost();
int port = config().getProxyPort();
if (proxyType == Proxy.Type.DIRECT || StringUtils.isBlank(host)) {
return NO_PROXY;
} else if (port < 0 || port > 0xFFFF) {
LOG.warning("Illegal proxy port: " + port);
return NO_PROXY;
} else {
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
}
} else {
return ProxyManager.SYSTEM_DEFAULT;
}
}
private static ObjectBinding<Proxy> proxyProperty;
private static Authenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
public static Proxy getProxy() {
return proxyProperty.get();
}
public static ObservableObjectValue<Proxy> proxyProperty() {
return proxyProperty;
if (username != null || password != null)
return new SimpleAuthenticator(username, password.toCharArray());
else
return null;
} else
return null;
}
static void init() {
proxyProperty = Bindings.createObjectBinding(
() -> {
String host = config().getProxyHost();
int port = config().getProxyPort();
if (!config().hasProxy() || StringUtils.isBlank(host) || config().getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
if (port < 0 || port > 0xFFFF) {
LOG.warning("Illegal proxy port: " + port);
return Proxy.NO_PROXY;
}
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
ProxySelector.setDefault(getProxySelector());
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector);
proxyProperty.addListener(any -> updateSystemProxy());
updateSystemProxy();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null && password != null) {
return new PasswordAuthentication(username, password.toCharArray());
}
}
return null;
}
});
Authenticator.setDefault(getAuthenticator());
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator);
config().proxyPassProperty().addListener(updateAuthenticator);
}
private static void updateSystemProxy() {
Proxy proxy = proxyProperty.get();
if (proxy.type() == Proxy.Type.DIRECT) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
} else {
InetSocketAddress address = (InetSocketAddress) proxy.address();
String host = address.getHostString();
String port = String.valueOf(address.getPort());
if (proxy.type() == Type.HTTP) {
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", port);
System.setProperty("https.proxyHost", host);
System.setProperty("https.proxyPort", port);
} else if (proxy.type() == Type.SOCKS) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.setProperty("socksProxyHost", host);
System.setProperty("socksProxyPort", port);
private static final class SimpleProxySelector extends ProxySelector {
private final List<Proxy> proxies;
SimpleProxySelector(Proxy proxy) {
this(Collections.singletonList(proxy));
}
SimpleProxySelector(List<Proxy> proxies) {
this.proxies = proxies;
}
@Override
public List<Proxy> select(URI uri) {
if (uri == null)
throw new IllegalArgumentException("URI can't be null.");
return proxies;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (uri == null || sa == null || ioe == null) {
throw new IllegalArgumentException("Arguments can't be null.");
}
}
@Override
public String toString() {
return "SimpleProxySelector" + proxies;
}
}
private static final class SimpleAuthenticator extends Authenticator {
private final String username;
private final char[] password;
private SimpleAuthenticator(String username, char[] password) {
this.username = username;
this.password = password;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
}
}
private ProxyManager() {
}
}
@@ -0,0 +1,168 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class StyleSheets {
private static final int FONT_STYLE_SHEET_INDEX = 0;
private static final int THEME_STYLE_SHEET_INDEX = 1;
private static final ObservableList<String> stylesheets;
static {
String[] array = new String[]{
getFontStyleSheet(),
getThemeStyleSheet(),
"/assets/css/root.css"
};
stylesheets = FXCollections.observableList(Arrays.asList(array));
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
}
private static String toStyleSheetUri(String styleSheet, String fallback) {
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
else
try {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
return fallback;
}
}
private static String getFontStyleSheet() {
final String defaultCss = "/assets/css/font.css";
final Font font = FontManager.getFont();
if (font == null || font == Font.getDefault())
return defaultCss;
String fontFamily = font.getFamily();
String style = font.getStyle();
String weight = null;
String posture = null;
if (style != null) {
style = style.toLowerCase(Locale.ROOT);
if (style.contains("thin"))
weight = "100";
else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light"))
weight = "200";
else if (style.contains("medium"))
weight = "500";
else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold"))
weight = "600";
else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold"))
weight = "800";
else if (style.contains("black") || style.contains("heavy"))
weight = "900";
else if (style.contains("light"))
weight = "lighter";
else if (style.contains("bold"))
weight = "bold";
posture = style.contains("italic") || style.contains("oblique") ? "italic" : null;
}
StringBuilder builder = new StringBuilder();
builder.append(".root {");
if (fontFamily == null)
// https://github.com/HMCL-dev/HMCL/pull/3423
builder.append("-fx-font-family: -fx-base-font-family;");
else
builder.append("-fx-font-family:\"").append(fontFamily).append("\";");
if (weight != null)
builder.append("-fx-font-weight:").append(weight).append(";");
if (posture != null)
builder.append("-fx-font-style:").append(posture).append(";");
builder.append('}');
return toStyleSheetUri(builder.toString(), fontFamily);
}
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);
}
private static String getThemeStyleSheet() {
final String blueCss = "/assets/css/blue.css";
Theme theme = config().getTheme();
if (theme == null || theme.getPaint().equals(Theme.BLUE.getPaint()))
return blueCss;
return toStyleSheetUri(".root {" +
"-fx-base-color:" + theme.getColor() + ';' +
"-fx-base-darker-color: derive(-fx-base-color, -10%);" +
"-fx-base-check-color: derive(-fx-base-color, 30%);" +
"-fx-rippler-color:" + rgba(theme.getPaint(), 0.3) + ';' +
"-fx-base-rippler-color: derive(" + rgba(theme.getPaint(), 0.3) + ", 100%);" +
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
'}', blueCss);
}
public static void init(Scene scene) {
Bindings.bindContent(scene.getStylesheets(), stylesheets);
}
private StyleSheets() {
}
}
@@ -23,30 +23,16 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
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.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
public final class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
public static final Color BLACK = Color.web("#292929");
public static final Color[] SUGGESTED_COLORS = new Color[]{
@@ -58,58 +44,6 @@ public class Theme {
Color.web("#B71C1C") // red
};
private static Charset cssCharset;
private static Charset getCssCharset() {
if (cssCharset != null)
return cssCharset;
Charset defaultCharset = Charset.defaultCharset();
if (defaultCharset != StandardCharsets.UTF_8) {
// https://bugs.openjdk.org/browse/JDK-8279328
// For JavaFX 17 or earlier, native encoding should be used
String jfxVersion = System.getProperty("javafx.version");
if (jfxVersion != null) {
Matcher matcher = Pattern.compile("^(?<version>[0-9]+)").matcher(jfxVersion);
if (matcher.find()) {
int v = Lang.parseInt(matcher.group(), -1);
if (v >= 18) {
cssCharset = StandardCharsets.UTF_8;
}
}
}
}
if (cssCharset == null)
cssCharset = defaultCharset;
return cssCharset;
}
@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;
@@ -133,6 +67,10 @@ public class Theme {
return color;
}
public Color getPaint() {
return paint;
}
public boolean isCustom() {
return name.startsWith("#");
}
@@ -145,53 +83,6 @@ public class Theme {
return isLight() ? Color.BLACK : Color.WHITE;
}
public String[] getStylesheets(String overrideFontFamily) {
String css = "/assets/css/blue.css";
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();
}
}
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
Color textFill = getForegroundColor();
String fontCss = "";
if (fontFamily != null) {
fontCss = "-fx-font-family: \"" + fontFamily + "\";";
if (fontStyle != null && !fontStyle.isEmpty())
fontCss += " -fx-font-style: \"" + fontStyle + "\";";
}
try {
File temp = File.createTempFile("hmcl", ".css");
String themeText = IOUtils.readFullyAsString(Theme.class.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%", fontCss);
FileUtils.writeText(temp, themeText, getCssCharset());
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"};
}
public static Theme custom(String color) {
if (!color.startsWith("#"))
throw new IllegalArgumentException();
@@ -235,7 +126,7 @@ public class Theme {
}
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(Locale.ROOT) : 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;
@@ -20,44 +20,47 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
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.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.SystemInfo;
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.Locale;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public final class VersionSetting implements Cloneable, Observable {
private boolean global = false;
private static final int SUGGESTED_MEMORY;
public boolean isGlobal() {
return global;
static {
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
SUGGESTED_MEMORY = totalMemoryMB >= 32768
? 8192
: Integer.max((int) (Math.round(totalMemoryMB / 4.0 / 128.0) * 128), 256);
}
public void setGlobal(boolean global) {
this.global = global;
private final transient ObservableHelper helper = new ObservableHelper(this);
public VersionSetting() {
PropertyUtils.attachListener(this, helper);
}
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
@@ -84,38 +87,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);
}
@@ -221,7 +229,7 @@ public final class VersionSetting implements Cloneable {
permSizeProperty.set(permSize);
}
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", SUGGESTED_MEMORY);
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
@@ -644,127 +652,86 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber 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(GameVersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom");
int majorVersion = -1;
try {
majorVersion = Integer.parseInt(javaVersion);
} catch (NumberFormatException ignored) {
}
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);
}
}).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,
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;
}
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
FXUtils.runInFX(() -> setJava("Auto"));
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
} catch (IOException | InvalidPathException ignored) {
}
for (JavaRuntime java : JavaManager.getAllJava()) {
if (java.getVersion().equals(javaVersion)) {
return java;
}
} else throw new Error();
} catch (InterruptedException e) {
throw new CancellationException();
}
return null;
}
});
default:
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
}
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
maxMemoryProperty.addListener(listener);
minMemoryProperty.addListener(listener);
autoMemory.addListener(listener);
preLaunchCommandProperty.addListener(listener);
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);
widthProperty.addListener(listener);
heightProperty.addListener(listener);
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 void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setAutoMemory(isAutoMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
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());
versionSetting.setWidth(getWidth());
versionSetting.setHeight(getHeight());
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;
VersionSetting cloned = new VersionSetting();
PropertyUtils.copyProperties(this, cloned);
return cloned;
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
@@ -777,7 +744,7 @@ public final class VersionSetting implements Cloneable {
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("maxMemory", src.getMaxMemory() <= 0 ? SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
obj.addProperty("permSize", src.getPermSize());
@@ -787,7 +754,6 @@ 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());
@@ -806,6 +772,24 @@ public final class VersionSetting implements Cloneable {
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);
@@ -827,8 +811,8 @@ public final class VersionSetting implements Cloneable {
return null;
JsonObject obj = (JsonObject) json;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), OperatingSystem.SUGGESTED_MEMORY);
if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), SUGGESTED_MEMORY);
if (maxMemoryN <= 0) maxMemoryN = SUGGESTED_MEMORY;
VersionSetting vs = new VersionSetting();
@@ -846,7 +830,6 @@ public final class VersionSetting implements Cloneable {
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(""));
@@ -865,6 +848,27 @@ public final class VersionSetting implements Cloneable {
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 {
@@ -892,5 +896,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;
}
}
}
}
}
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
@@ -34,13 +37,17 @@ import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
@@ -53,10 +60,10 @@ import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -65,6 +72,8 @@ 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 String JAVA_VERSION_TIP = "javaVersion";
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();
@@ -252,7 +261,7 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).start();
Lang.thread(JavaManager::initialize, "Search Java", true);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
@@ -260,18 +269,36 @@ public final class Controllers {
stage.setMinHeight(MIN_HEIGHT);
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
StyleSheets.init(scene);
FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(decorator.getDecorator().opacityProperty(), 0, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, FXUtils.EASE)
),
new KeyFrame(Duration.millis(600),
new KeyValue(decorator.getDecorator().opacityProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, FXUtils.EASE)
)
);
timeline.play();
}
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);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.macos_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 &&
@@ -284,6 +311,51 @@ public final class Controllers {
}
}
if (JavaRuntime.CURRENT_VERSION < 10) {
Integer shownTipVersion = null;
try {
shownTipVersion = (Integer) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion < 11) {
String downloadLink = null;
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
downloadLink = "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
downloadLink = String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
}
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, 11))
.build());
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
@@ -368,10 +440,7 @@ public final class Controllers {
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setExecutor(executor);
dialog(pane);
TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel);
executor.start();
return pane;
}
@@ -391,9 +460,6 @@ public final class Controllers {
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
break;
case "hmcl://hide-announcement":
Controllers.getRootPage().getMainPage().hideAnnouncementPane();
break;
}
} else {
FXUtils.openLink(href);
@@ -53,7 +53,7 @@ public class CrashWindow extends Stage {
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);
@@ -18,19 +18,24 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.WeakListener;
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.geometry.Insets;
import javafx.beans.value.*;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.event.EventType;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -40,9 +45,11 @@ 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.FileChooser;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
@@ -50,13 +57,11 @@ import javafx.util.StringConverter;
import org.glavo.png.PNGType;
import org.glavo.png.PNGWriter;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -67,18 +72,15 @@ 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.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -87,6 +89,9 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -98,8 +103,26 @@ 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";
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp"
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
@@ -215,20 +238,21 @@ public final class FXUtils {
});
}
public static <K, T> void setupCellValueFactory(JFXTreeTableColumn<K, T> column, Function<K, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
@SuppressWarnings("unchecked")
public static <T extends Event> void ignoreEvent(Node node, EventType<T> type, Predicate<? super T> filter) {
EventDispatcher oldDispatcher = node.getEventDispatcher();
node.setEventDispatcher((event, tail) -> {
EventType<?> t = event.getEventType();
while (t != null && t != type)
t = t.getSuperType();
if (t == type && filter.test((T) event)) {
return tail.dispatchEvent(event);
} else {
return oldDispatcher.dispatchEvent(event, tail);
}
});
}
public static Node wrapMargin(Node node, Insets insets) {
StackPane.setMargin(node, insets);
return new StackPane(node);
}
public static void setValidateWhileTextChanged(Node field, boolean validate) {
if (field instanceof JFXTextField) {
if (validate) {
@@ -300,8 +324,12 @@ public final class FXUtils {
ScrollUtils.addSmoothScrolling(scrollPane);
}
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) {
@@ -309,39 +337,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);
LOG.error("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);
@@ -377,7 +379,7 @@ public final class FXUtils {
String openCommand;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommand = "explorer.exe";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
openCommand = "/usr/bin/open";
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
openCommand = "/usr/bin/xdg-open";
@@ -414,8 +416,18 @@ public final class FXUtils {
String[] openCommands;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && SystemUtils.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;
@@ -472,12 +484,13 @@ public final class FXUtils {
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
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});
Path path = SystemUtils.which(browser);
if (path != null) {
try {
Runtime.getRuntime().exec(new String[]{path.toString(), link});
return;
} catch (Throwable ignored) {
}
} catch (Throwable ignored) {
}
LOG.warning("No known browser found");
}
@@ -485,7 +498,7 @@ public final class FXUtils {
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
@@ -496,57 +509,13 @@ public final class FXUtils {
});
}
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.warning("WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtils.initLookAndFeel();
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
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);
TextFieldBinding<T> binding = new TextFieldBinding<>(textField, property, converter);
binding.updateTextField();
textField.getProperties().put("FXUtils.bind.binding", binding);
textField.focusedProperty().addListener(binding.focusedListener);
textField.sceneProperty().addListener(binding.sceneListener);
property.addListener(binding.propertyListener);
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
@@ -558,53 +527,116 @@ public final class FXUtils {
}
public static void unbind(JFXTextField textField, Property<?> property) {
TextFieldBindingListener<?> listener = new TextFieldBindingListener<>(textField, property, null);
textField.focusedProperty().removeListener((ChangeListener<Boolean>) listener);
property.removeListener(listener);
TextFieldBinding<?> binding = (TextFieldBinding<?>) textField.getProperties().remove("FXUtils.bind.binding");
if (binding != null) {
textField.focusedProperty().removeListener(binding.focusedListener);
textField.sceneProperty().removeListener(binding.sceneListener);
property.removeListener(binding.propertyListener);
}
}
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 static final class TextFieldBinding<T> {
private final JFXTextField textField;
private final Property<T> property;
private final StringConverter<T> converter;
TextFieldBindingListener(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
this.textFieldRef = new WeakReference<>(textField);
this.propertyRef = new WeakReference<>(property);
public final ChangeListener<Boolean> focusedListener;
public final ChangeListener<Scene> sceneListener;
public final InvalidationListener propertyListener;
public TextFieldBinding(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
this.textField = textField;
this.property = property;
this.converter = converter;
this.hashCode = System.identityHashCode(textField) ^ System.identityHashCode(property);
focusedListener = (observable, oldFocused, newFocused) -> {
if (oldFocused && !newFocused) {
if (textField.validate()) {
updateProperty();
} else {
// Rollback to old value
updateTextField();
}
}
};
sceneListener = (observable, oldScene, newScene) -> {
if (oldScene != null && newScene == null) {
// Component is being removed from scene
if (textField.validate()) {
updateProperty();
}
}
};
propertyListener = observable -> {
updateTextField();
};
}
public void updateProperty() {
String newText = textField.getText();
@SuppressWarnings("unchecked")
T newValue = converter == null ? (T) newText : converter.fromString(newText);
if (!Objects.equals(newValue, property.getValue())) {
property.setValue(newValue);
}
}
public void updateTextField() {
T value = property.getValue();
textField.setText(converter == null ? (String) value : converter.toString(value));
}
}
private static final class EnumBidirectionalBinding<E extends Enum<E>> implements InvalidationListener, WeakListener {
private final WeakReference<JFXComboBox<E>> comboBoxRef;
private final WeakReference<Property<E>> propertyRef;
private final int hashCode;
private boolean updating = false;
private EnumBidirectionalBinding(JFXComboBox<E> comboBox, Property<E> property) {
this.comboBoxRef = new WeakReference<>(comboBox);
this.propertyRef = new WeakReference<>(property);
this.hashCode = System.identityHashCode(comboBox) ^ 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();
public void invalidated(Observable sourceProperty) {
if (!updating) {
final JFXComboBox<E> comboBox = comboBoxRef.get();
final Property<E> property = 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 (comboBox == null || property == null) {
if (comboBox != null) {
comboBox.getSelectionModel().selectedItemProperty().removeListener(this);
}
if (!Objects.equals(newValue, property.getValue()))
property.setValue(newValue);
if (property != null) {
property.removeListener(this);
}
} else {
// Rollback to old value
invalidated(null);
updating = true;
try {
if (property == sourceProperty) {
E newValue = property.getValue();
comboBox.getSelectionModel().select(newValue);
} else {
E newValue = comboBox.getSelectionModel().getSelectedItem();
property.setValue(newValue);
}
} finally {
updating = false;
}
}
}
}
@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));
}
public boolean wasGarbageCollected() {
return comboBoxRef.get() == null || propertyRef.get() == null;
}
@Override
@@ -613,52 +645,45 @@ public final class FXUtils {
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TextFieldBindingListener))
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof EnumBidirectionalBinding))
return false;
TextFieldBindingListener<?> other = (TextFieldBindingListener<?>) obj;
return this.hashCode == other.hashCode
&& this.textFieldRef.get() == other.textFieldRef.get()
&& this.propertyRef.get() == other.propertyRef.get();
EnumBidirectionalBinding<?> that = (EnumBidirectionalBinding<?>) o;
final JFXComboBox<E> comboBox = this.comboBoxRef.get();
final Property<E> property = this.propertyRef.get();
final JFXComboBox<?> thatComboBox = that.comboBoxRef.get();
final Property<?> thatProperty = that.propertyRef.get();
if (comboBox == null || property == null || thatComboBox == null || thatProperty == null)
return false;
return comboBox == thatComboBox && property == thatProperty;
}
}
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
toggleButton.selectedProperty().bindBidirectional(property);
}
public static void unbindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
toggleButton.selectedProperty().unbindBidirectional(property);
}
public static void bindBoolean(JFXCheckBox checkBox, Property<Boolean> property) {
checkBox.selectedProperty().bindBidirectional(property);
}
public static void unbindBoolean(JFXCheckBox checkBox, Property<Boolean> property) {
checkBox.selectedProperty().unbindBidirectional(property);
}
/**
* Bind combo box selection with given enum property bidirectionally.
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
*
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @see #unbindEnum(JFXComboBox, Property)
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
*/
public static <T extends Enum<T>> void bindEnum(JFXComboBox<T> comboBox, Property<T> property) {
unbindEnum(comboBox);
EnumBidirectionalBinding<T> binding = new EnumBidirectionalBinding<>(comboBox, property);
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().selectedItemProperty().removeListener(binding);
property.removeListener(binding);
comboBox.getSelectionModel().select(currentValue.ordinal());
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
comboBox.getSelectionModel().select(property.getValue());
comboBox.getSelectionModel().selectedItemProperty().addListener(binding);
property.addListener(binding);
}
/**
@@ -669,23 +694,123 @@ public final class FXUtils {
* @see #bindEnum(JFXComboBox, Property)
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
*/
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 <T extends Enum<T>> void unbindEnum(JFXComboBox<T> comboBox, Property<T> property) {
EnumBidirectionalBinding<T> binding = new EnumBidirectionalBinding<>(comboBox, property);
comboBox.getSelectionModel().selectedItemProperty().removeListener(binding);
property.removeListener(binding);
}
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 if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
icon = "/assets/img/icon-mac.png";
} else {
icon = "/assets/img/icon@4x.png";
}
stage.getIcons().add(newBuiltinImage(icon));
}
private static Image loadWebPImage(InputStream input) throws IOException {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
return SwingFXUtils.toFXImage(reader.read(0, reader.getDefaultReadParam()), null);
} finally {
reader.dispose();
}
}
public static Image loadImage(Path path) throws Exception {
try (InputStream input = Files.newInputStream(path)) {
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
return loadWebPImage(input);
else {
Image image = new Image(input);
if (image.isError())
throw image.getException();
return image;
}
}
}
public static Image loadImage(URL url) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url);
if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
}
try (InputStream input = connection.getInputStream()) {
String path = url.getPath();
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
return loadWebPImage(input);
else {
Image image = new Image(input);
if (image.isError())
throw image.getException();
return image;
}
}
}
/**
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
*
@@ -815,6 +940,17 @@ public final class FXUtils {
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);
}
@@ -903,6 +1039,8 @@ public final class FXUtils {
}
};
public static final Interpolator EASE = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
public static void onEscPressed(Node node, Runnable action) {
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
@@ -912,6 +1050,27 @@ public final class FXUtils {
});
}
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();
}
});
}
public static void copyOnDoubleClick(Labeled label) {
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
String text = label.getText();
if (text != null && !text.isEmpty()) {
copyText(label.getText());
e.consume();
}
}
});
}
public static void copyText(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text);
@@ -933,7 +1092,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);
@@ -941,8 +1100,8 @@ 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 -> {
Text text = new Text(element.getTextContent());
onClicked(text, () -> {
String link = href;
try {
link = new URI(href).toASCIIString();
@@ -950,7 +1109,10 @@ public final class FXUtils {
}
hyperlinkAction.accept(link);
});
texts.add(hyperlink);
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");
@@ -977,4 +1139,16 @@ 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);
}
public static FileChooser.ExtensionFilter getImageExtensionFilter() {
return new FileChooser.ExtensionFilter(i18n("extension.png"),
IMAGE_EXTENSIONS.stream().map(ext -> "*." + ext).toArray(String[]::new));
}
}
@@ -20,8 +20,6 @@ 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;
@@ -39,19 +37,14 @@ 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.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.*;
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 org.jackhuang.hmcl.util.platform.*;
import java.io.IOException;
import java.nio.file.Files;
@@ -65,8 +58,7 @@ 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.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
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;
@@ -77,8 +69,6 @@ public class GameCrashWindow extends Stage {
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 TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final TextFlow feedbackTextFlow = new TextFlow();
@@ -89,9 +79,9 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;
private final Collection<Pair<String, Log4jLevel>> logs;
private final List<Log> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<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;
@@ -102,7 +92,7 @@ public class GameCrashWindow extends Stage {
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
total_memory = OperatingSystem.TOTAL_MEMORY + " MB";
total_memory = MEGABYTES.formatBytes(SystemInfo.getTotalMemorySize());
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
? launchOptions.getJava().getVersion()
@@ -113,7 +103,7 @@ public class GameCrashWindow extends Stage {
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
setScene(new Scene(view, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
StyleSheets.init(getScene());
setTitle(i18n("game.crash.title"));
FXUtils.setIcon(this);
@@ -124,7 +114,7 @@ public class GameCrashWindow extends Stage {
private void analyzeCrashReport() {
loading.set(true);
Task.allOf(Task.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
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;
@@ -146,7 +136,7 @@ public class GameCrashWindow extends Stage {
String log;
try {
log = FileUtils.readText(latestLog);
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
} catch (IOException e) {
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
@@ -264,20 +254,18 @@ public class GameCrashWindow extends Stage {
private void showLogWindow() {
LogWindow logWindow = new LogWindow(managedProcess);
logWindow.logLine(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
if (managedProcess.getClasspath() != null)
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("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("\n")))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
@@ -358,12 +346,12 @@ 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(launcher, version, total_memory, memory, java, os, arch);
}
@@ -392,13 +380,13 @@ public class GameCrashWindow extends Stage {
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
runInFX(() -> FXUtils.installFastTooltip(gameDir, i18n("game.directory")));
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());
runInFX(() -> FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory")));
FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory"));
Label reasonTitle = new Label(i18n("game.crash.reason"));
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
@@ -406,7 +394,7 @@ public class GameCrashWindow extends Stage {
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
reasonPane.setFitToWidth(true);
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
@@ -422,14 +410,14 @@ public class GameCrashWindow extends Stage {
HBox toolBar = new HBox();
{
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnMouseClicked(e -> showLogWindow());
logButton.setOnAction(e -> showLogWindow());
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help")));
helpButton.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
toolBar.setPadding(new Insets(8));
@@ -0,0 +1,319 @@
/*
* 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 appendAutoLineBreak(String text) {
AutoLineBreak textNode = new AutoLineBreak(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())
appendAutoLineBreak("\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":
appendAutoLineBreak("\n");
break;
}
}
private static boolean isSpacing(String text) {
if (text == null)
return true;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch != ' ' && ch != '\t')
return false;
}
return true;
}
public void mergeLineBreaks() {
for (int i = 0; i < this.children.size(); i++) {
javafx.scene.Node child = this.children.get(i);
if (child instanceof AutoLineBreak) {
int lastAutoLineBreak = -1;
for (int j = i + 1; j < this.children.size(); j++) {
javafx.scene.Node otherChild = this.children.get(j);
if (otherChild instanceof AutoLineBreak) {
lastAutoLineBreak = j;
} else if (otherChild instanceof Text && isSpacing(((Text) otherChild).getText())) {
// do nothing
} else {
break;
}
}
if (lastAutoLineBreak > 0) {
this.children.subList(i + 1, lastAutoLineBreak + 1).clear();
if (((Text) child).getText().length() == 1) {
((Text) child).setText("\n\n");
}
}
}
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
private static final class AutoLineBreak extends Text {
public AutoLineBreak(String text) {
super(text);
}
}
}
@@ -23,7 +23,6 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
@@ -33,7 +32,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
@@ -60,8 +59,8 @@ public class InstallerItem extends Control {
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
private final ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty = new SimpleObjectProperty<>(this, "installAction");
private final ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty = new SimpleObjectProperty<>(this, "removeAction");
private final ObjectProperty<Runnable> onInstall = new SimpleObjectProperty<>(this, "onInstall");
private final ObjectProperty<Runnable> onRemove = new SimpleObjectProperty<>(this, "onRemove");
public interface State {
}
@@ -170,12 +169,28 @@ public class InstallerItem extends Control {
return resolvedStateProperty;
}
public ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty() {
return installActionProperty;
public ObjectProperty<Runnable> onInstallProperty() {
return onInstall;
}
public ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty() {
return removeActionProperty;
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
@@ -347,7 +362,9 @@ public class InstallerItem extends Control {
}
return i18n("install.installer.version", s.version);
} else if (state instanceof InstallableState) {
return i18n("install.installer.not_installed");
return control.style == Style.CARD
? i18n("install.installer.do_not_install")
: i18n("install.installer.not_installed");
} else if (state instanceof IncompatibleState) {
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
} else {
@@ -363,7 +380,7 @@ public class InstallerItem extends Control {
pane.getChildren().add(buttonsContainer);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1));
removeButton.getStyleClass().add("toggle-icon4");
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
@@ -371,19 +388,23 @@ public class InstallerItem extends Control {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.onMouseClickedProperty().bind(control.removeActionProperty);
removeButton.setOnAction(e -> {
Runnable onRemove = control.getOnRemove();
if (onRemove != null)
onRemove.run();
});
buttonsContainer.getChildren().add(removeButton);
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),
SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), -1) :
SVG.UPDATE.createIcon(Theme.blackFill(), -1),
control.resolvedStateProperty
));
installButton.getStyleClass().add("toggle-icon4");
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
if (control.installActionProperty.get() == null) {
if (control.getOnInstall() == null) {
return false;
}
@@ -396,18 +417,27 @@ public class InstallerItem extends Control {
}
return false;
}, control.resolvedStateProperty, control.installActionProperty));
}, control.resolvedStateProperty, control.onInstall));
installButton.managedProperty().bind(installButton.visibleProperty());
installButton.onMouseClickedProperty().bind(control.installActionProperty);
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.installActionProperty);
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);
container.setOnMouseClicked(null);
pane.setCursor(Cursor.DEFAULT);
}
});
@@ -69,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.createIcon(Theme.whiteFill(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
btnAdd.setGraphic(SVG.ADD.createIcon(Theme.whiteFill(), -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.createIcon(Theme.whiteFill(), -1, -1));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1));
btnRefresh.setOnAction(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
@@ -25,8 +25,8 @@ 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;
@@ -34,17 +34,19 @@ 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.setting.Theme;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
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.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
@@ -54,14 +56,11 @@ import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -69,128 +68,120 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
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;
}
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()));
StyleSheets.init(getScene());
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
for (SimpleBooleanProperty property : levelShownMap.values()) {
property.addListener(o -> shakeLogs());
}
this.gameProcess = gameProcess;
}
public void logLine(String filteredLine, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(filteredLine), level);
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) {
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;
private boolean selected = false;
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 final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
private final BooleanProperty showCrashReport = 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");
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000", "10000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
for (int i = 0; i < LEVELS.length; i++) {
buttonText[i] = new SimpleStringProperty();
showLevel[i] = new SimpleBooleanProperty(true);
}
if (!flag)
cboLines.getSelectionModel().select(2);
cboLines.getItems().setAll(500, 2000, 5000, 10000);
cboLines.setValue(Log.getLogLines());
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(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]);
}
}
@@ -207,7 +198,7 @@ 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.warning("Failed to export logs", e);
return;
@@ -223,38 +214,31 @@ public final class LogWindow extends Stage {
});
}
private void onExportDump(JFXButton button) {
private void onExportDump(SpinnerPane pane) {
assert SystemUtils.supportJVMAttachment();
pane.setLoading(true);
thread(() -> {
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
if (SystemUtils.supportJVMAttachment()) {
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
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.dependency_ok.button"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
}
});
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
private void onExportGameCrashInfo() {
if (exportGameCrashInfoCallback == null) return;
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
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
@@ -263,7 +247,7 @@ public final class LogWindow extends Stage {
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
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");
@@ -275,17 +259,7 @@ public final class LogWindow extends Stage {
private final Set<ListCell<Log>> selected = new HashSet<>();
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + backgroundColor + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(buttonText);
button.setSelected(true);
showLevel.bind(button.selectedProperty());
return button;
}
protected LogWindowSkin(LogWindowImpl control) {
LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
@@ -310,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);
}
@@ -346,12 +323,15 @@ public final class LogWindow extends Stage {
setGraphic(null);
setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
if (!event.isControlDown()) {
for (ListCell<Log> logListCell: selected) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().selected = false;
logListCell.getItem().setSelected(false);
}
}
}
@@ -362,8 +342,10 @@ public final class LogWindow extends Stage {
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().selected = true;
getItem().setSelected(true);
}
event.consume();
});
}
@@ -377,18 +359,18 @@ public final class LogWindow extends Stage {
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(SELECTED, !empty && item.selected);
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());
}
}
});
@@ -398,10 +380,9 @@ public final class LogWindow extends Stage {
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.selected) {
if (item.log != null) {
stringBuilder.append(item.log);
}
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
}
}
@@ -417,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);
@@ -432,24 +408,24 @@ public final class LogWindow extends Stage {
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
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.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
JFXButton exportDumpButton = new JFXButton();
SpinnerPane exportDumpPane = new SpinnerPane();
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
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, exportDumpButton, clearButton);
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
vbox.getChildren().add(bottom);
}
@@ -25,92 +25,90 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
/**
* All vector icons used in the launcher.
* <p>
* Unless otherwise stated,
* these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
* with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
* The view boxes of all icons are normalized to {@code 0 0 24 24}.
*/
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("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"),
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"),
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");
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("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"), // Not Material
ARCHIVE("M12 18 16 14 14.6 12.6 13 14.2V10H11V14.2L9.4 12.6 8 14 12 18ZM5 8V19H19V8H5ZM5 21Q4.175 21 3.5875 20.4125T3 19V6.525Q3 6.175 3.1125 5.85T3.45 5.25L4.7 3.725Q4.975 3.375 5.3875 3.1875T6.25 3H17.75Q18.2 3 18.6125 3.1875T19.3 3.725L20.55 5.25Q20.775 5.525 20.8875 5.85T21 6.525V19Q21 19.825 20.4125 20.4125T19 21H5ZM5.4 6H18.6L17.75 5H6.25L5.4 6ZM12 13.5Z"),
ARROW_BACK("M7.825 13 13.425 18.6 12 20 4 12 12 4 13.425 5.4 7.825 11H20V13H7.825Z"),
ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"),
ARROW_DROP_UP("M7 14 12 9 17 14H7Z"),
ARROW_FORWARD("M16.175 13H4V11H16.175L10.575 5.4 12 4 20 12 12 20 10.575 18.6 16.175 13Z"),
BETA_CIRCLE("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"), // Not Material
CANCEL("M8.4 17 12 13.4 15.6 17 17 15.6 13.4 12 17 8.4 15.6 7 12 10.6 8.4 7 7 8.4 10.6 12 7 15.6 8.4 17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
CHECK("M9.55 18 3.85 12.3 5.275 10.875 9.55 15.15 18.725 5.975 20.15 7.4 9.55 18Z"),
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
HELP("M11.95 18Q12.475 18 12.8375 17.6375T13.2 16.75Q13.2 16.225 12.8375 15.8625T11.95 15.5Q11.425 15.5 11.0625 15.8625T10.7 16.75Q10.7 17.275 11.0625 17.6375T11.95 18ZM11.05 14.15H12.9Q12.9 13.325 13.0875 12.85T14.15 11.55Q14.8 10.9 15.175 10.3125T15.55 8.9Q15.55 7.5 14.525 6.75T12.1 6Q10.675 6 9.7875 6.75T8.55 8.55L10.2 9.2Q10.325 8.75 10.7625 8.225T12.1 7.7Q12.9 7.7 13.3 8.1375T13.7 9.1Q13.7 9.6 13.4 10.0375T12.65 10.85Q11.55 11.825 11.3 12.325T11.05 14.15ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"),
HOST("M4 21Q3.175 21 2.5875 20.4125T2 19V5Q2 4.175 2.5875 3.5875T4 3H9Q9.825 3 10.4125 3.5875T11 5V19Q11 19.825 10.4125 20.4125T9 21H4ZM15 21Q14.175 21 13.5875 20.4125T13 19V5Q13 4.175 13.5875 3.5875T15 3H20Q20.825 3 21.4125 3.5875T22 5V19Q22 19.825 21.4125 20.4125T20 21H15ZM4 19H9V5H4V19ZM15 19H20V5H15V19ZM5 15H8V13H5V15ZM16 15H19V13H16V15ZM5 12H8V10H5V12ZM16 12H19V10H16V12ZM5 9H8V7H5V9ZM16 9H19V7H16V9ZM4 19H9 4ZM15 19H20 15Z"),
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE("M6 21V19H18V21H6Z"),
MOJANG("M13.9658 0C12.9552.828 12.7686 2.195 12.7007 3.418 12.7219 4.4201 14.0423 4.7174 14.6538 4.0082 15.0912 2.6579 14.3692 1.2739 13.9658 0ZM10.913 2.9297C10.7559 3.6983 10.6284 4.4669 10.4925 5.2355 8.9894 3.9913 7.1505 3.0825 5.142 3.2948 3.4944 3.4646.9429 2.6961.1404 4.6452-.1229 8.314.0722 12.0124.034 15.6896-.0891 16.9126.8957 18.1101 2.1781 17.9699 6.3989 18.0039 10.6283 18.0172 14.8491 17.9661 16.1145 18.102 16.8067 16.9554 16.9851 15.8768 13.2696 16.4374 9.1761 16.9296 5.7111 15.1292 2.5986 13.5836 2.246 8.3139 5.5581 6.798 9.3203 5.1759 13.8596 8.0383 14.9424 11.7877 15.6133 12.1105 16.2844 12.4247 16.9596 12.7432 16.6241 10.3483 16.6537 7.8005 15.5793 5.5882 15.0571 4.7474 14.0975 6.2714 13.3799 5.6217 12.416 4.8659 11.7495 3.8129 10.913 2.9297Z"), // Not Material
MORE_HORIZ("M6 14Q5.175 14 4.5875 13.4125T4 12Q4 11.175 4.5875 10.5875T6 10Q6.825 10 7.4125 10.5875T8 12Q8 12.825 7.4125 13.4125T6 14ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM18 14Q17.175 14 16.5875 13.4125T16 12Q16 11.175 16.5875 10.5875T18 10Q18.825 10 19.4125 10.5875T20 12Q20 12.825 19.4125 13.4125T18 14Z"),
MORE_VERT("M12 20Q11.175 20 10.5875 19.4125T10 18Q10 17.175 10.5875 16.5875T12 16Q12.825 16 13.4125 16.5875T14 18Q14 18.825 13.4125 19.4125T12 20ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM12 8Q11.175 8 10.5875 7.4125T10 6Q10 5.175 10.5875 4.5875T12 4Q12.825 4 13.4125 4.5875T14 6Q14 6.825 13.4125 7.4125T12 8Z"),
OPEN_IN_NEW("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H12V5H5V19H19V12H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM9.7 15.7 8.3 14.3 17.6 5H14V3H21V10H19V6.4L9.7 15.7Z"),
OUTPUT("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V7H19V5H5V19H19V17H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM17 17 15.6 15.6 18.175 13H9V11H18.175L15.6 8.4 17 7 22 12 17 17Z"),
PACKAGE("M10 9.75 12 8.75 14 9.75V5H10V9.75ZM7 17V15H12V17H7ZM5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V19Q21 19.825 20.4125 20.4125T19 21H5ZM5 5V19 5ZM5 19H19V5H16V13L12 11 8 13V5H5V19Z"),
PACKAGE2("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM11 21.725 4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725ZM16 8.525 17.925 7.425 12 4 10.05 5.125 16 8.525ZM12 10.85 13.95 9.725 8.025 6.3 6.075 7.425 12 10.85Z"),
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
RELEASE_CIRCLE("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"), // Not Material
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
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"), // Not Material
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons
STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"),
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
;
public static final double DEFAULT_SIZE = 24;
private final String path;
@@ -122,39 +120,38 @@ public enum SVG {
return path;
}
private static Node createIcon(SVGPath path, double width, double height) {
if (width < 0 || height < 0) {
private static Node createIcon(SVGPath path, double size) {
if (size < 0) {
StackPane pane = new StackPane(path);
pane.setAlignment(Pos.CENTER);
return pane;
}
Group svg = new Group(path);
double scale = Math.min(width / 24, height / 24);
double scale = size / 24;
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
}
public Node createIcon(ObservableValue<? extends Paint> fill, double width, double height) {
public Node createIcon(ObservableValue<? extends Paint> fill, double size) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().bind(fill);
return createIcon(p, width, height);
return createIcon(p, size);
}
public Node createIcon(Paint fill, double width, double height) {
public Node createIcon(Paint fill, double size) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().set(fill);
return createIcon(p, width, height);
return createIcon(p, size);
}
}
@@ -25,14 +25,11 @@ 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;
import org.jackhuang.hmcl.util.Holder;
/**
* Utility class for ScrollPanes.
@@ -142,23 +139,23 @@ final class ScrollUtils {
smoothScroll(scrollPane, speed, trackPadAdjustment);
}
private static 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};
private static final Duration DURATION = Duration.millis(3);
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);
final double[] derivatives = new double[FRICTIONS.length];
Timeline timeline = new Timeline();
AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
Holder<ScrollDirection> scrollDirectionHolder = new Holder<>();
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();
ScrollDirection scrollDirection = determineScrollDirection(event);
scrollDirectionHolder.value = scrollDirection;
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
derivatives[0] += scrollDirection.intDirection * currentSpeed;
if (timeline.getStatus() == Status.STOPPED) {
timeline.play();
}
@@ -180,28 +177,26 @@ final class ScrollUtils {
}
});
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> {
for (int i = 0; i < derivatives.length; i++) {
derivatives[i] *= frictions[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()) {
double size;
switch (scrollDirectionHolder.value) {
case LEFT:
case RIGHT:
value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
scrollPane.hvalueProperty().set(value);
size = scrollPane.getContent().getLayoutBounds().getWidth();
scrollPane.setHvalue(Math.min(Math.max(scrollPane.getHvalue() + dy / size, 0), 1));
break;
case UP:
case DOWN:
value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
scrollPane.vvalueProperty().set(value);
size = scrollPane.getContent().getLayoutBounds().getHeight();
scrollPane.setVvalue(Math.min(Math.max(scrollPane.getVvalue() + dy / size, 0), 1));
break;
}
@@ -89,18 +89,18 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
@@ -108,9 +108,9 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -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);
}
}
}
@@ -19,58 +19,78 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.concurrent.Worker;
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.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.Logger.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")));
}
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
try {
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(url);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.FAILED) {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
});
setBody(webView);
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.warning("WebView is missing or initialization failed", e);
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
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);
renderer.mergeLineBreaks();
return renderer.render();
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane(result);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setBody(scrollPane);
} else {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
}
}).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);
}
}
@@ -0,0 +1,76 @@
/*
* 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);
renderer.mergeLineBreaks();
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 org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
FXUtils.setIcon(this);
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;
}
}
@@ -32,17 +32,14 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
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.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
@@ -128,7 +125,7 @@ public class AccountListItem extends RadioButton {
.orElse(emptySet());
return uploadableTextures.contains(TextureType.SKIN);
}, profile);
} else if (account instanceof OfflineAccount || account instanceof MicrosoftAccount) {
} else if (account instanceof OfflineAccount || account.canUploadSkin()) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
@@ -144,11 +141,7 @@ public class AccountListItem extends RadioButton {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (account instanceof MicrosoftAccount) {
FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editskin");
return null;
}
if (!(account instanceof YggdrasilAccount)) {
if (!account.canUploadSkin()) {
return null;
}
@@ -174,7 +167,7 @@ public class AccountListItem extends RadioButton {
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 -> {
@@ -20,7 +20,9 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
@@ -42,22 +44,19 @@ 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);
BorderPane root = new BorderPane();
root.setCursor(Cursor.HAND);
FXUtils.onClicked(root, skinnable::fire);
JFXRadioButton chkSelected = new JFXRadioButton() {
@Override
public void fire() {
skinnable.fire();
}
};
JFXRadioButton chkSelected = new JFXRadioButton();
chkSelected.setMouseTransparent(true);
BorderPane.setAlignment(chkSelected, Pos.CENTER);
chkSelected.selectedProperty().bind(skinnable.selectedProperty());
root.setLeft(chkSelected);
@@ -94,7 +93,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnMove = new JFXButton();
SpinnerPane spinnerMove = new SpinnerPane();
spinnerMove.getStyleClass().add("small-spinner-pane");
btnMove.setOnMouseClicked(e -> {
btnMove.setOnAction(e -> {
Account account = skinnable.getAccount();
Accounts.getAccounts().remove(account);
if (account.isPortable()) {
@@ -117,11 +116,11 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
});
btnMove.getStyleClass().add("toggle-icon4");
if (skinnable.getAccount().isPortable()) {
btnMove.setGraphic(SVG.EARTH.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global")));
btnMove.setGraphic(SVG.PUBLIC.createIcon(Theme.blackFill(), -1));
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global"));
} else {
btnMove.setGraphic(SVG.EXPORT.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable")));
btnMove.setGraphic(SVG.OUTPUT.createIcon(Theme.blackFill(), -1));
FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable"));
}
spinnerMove.setContent(btnMove);
right.getChildren().add(spinnerMove);
@@ -129,7 +128,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
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 -> {
@@ -142,14 +141,14 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
.start();
});
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.blackFill(), -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();
@@ -159,10 +158,9 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
}
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.HANGER.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
btnUpload.setGraphic(SVG.CHECKROOM.createIcon(Theme.blackFill(), -1));
FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload"));
btnUpload.disableProperty().bind(Bindings.not(skinnable.canUploadSkin()));
spinnerUpload.setContent(btnUpload);
spinnerUpload.getStyleClass().add("small-spinner-pane");
right.getChildren().add(spinnerUpload);
@@ -170,18 +168,19 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnCopyUUID = new JFXButton();
SpinnerPane spinnerCopyUUID = new SpinnerPane();
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
btnCopyUUID.setOnMouseClicked(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnCopyUUID.setGraphic(SVG.COPY.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid")));
btnUpload.getStyleClass().add("toggle-icon4");
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
btnCopyUUID.setGraphic(SVG.CONTENT_COPY.createIcon(Theme.blackFill(), -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.createIcon(Theme.blackFill(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1));
FXUtils.installFastTooltip(btnRemove, i18n("button.delete"));
right.getChildren().add(btnRemove);
root.setRight(right);
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -42,15 +43,70 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import java.net.URI;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
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;
public class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
private static boolean isExemptedRegion() {
String zoneId = ZoneId.systemDefault().getId();
if (Arrays.asList(
"Asia/Shanghai",
// Although Asia/Beijing is not a legal name, Deepin uses it
"Asia/Beijing",
"Asia/Chongqing",
"Asia/Chungking",
"Asia/Harbin"
).contains(zoneId))
return true;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && NativeUtils.USE_JNA) {
Kernel32 kernel32 = Kernel32.INSTANCE;
// https://learn.microsoft.com/windows/win32/intl/table-of-geographical-locations
if (kernel32 != null && kernel32.GetUserGeoID(WinConstants.GEOCLASS_NATION) == 45) // China
return true;
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && "GMT+08:00".equals(zoneId))
// Some Linux distributions may use invalid time zone ids (e.g., Asia/Beijing)
// Java may not be able to resolve this name and use GMT+08:00 instead.
return true;
return false;
}
static {
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
if ("false".equals(property)
|| "auto".equals(property) && isExemptedRegion()
|| globalConfig().isEnableOfflineAccount())
RESTRICTED.set(false);
else
globalConfig().enableOfflineAccountProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) {
if (newValue) {
globalConfig().enableOfflineAccountProperty().removeListener(this);
RESTRICTED.set(false);
}
}
});
}
private final ObservableList<AccountListItem> items;
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
@@ -87,6 +143,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
private static class AccountListPageSkin extends DecoratorAnimatedPageSkin<AccountListPage> {
private final ObservableList<AdvancedListItem> authServerItems;
private ChangeListener<Boolean> holder;
public AccountListPageSkin(AccountListPage skinnable) {
super(skinnable);
@@ -95,39 +152,38 @@ 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")));
FXUtils.setLimitWidth(boxMethods, 200);
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG.ACCOUNT));
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
boxMethods.getChildren().add(offlineItem);
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setActionButtonVisible(false);
microsoftItem.setTitle(i18n("account.methods.microsoft"));
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setActionButtonVisible(false);
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setLeftGraphic(wrap(SVG.SERVER));
item.setLeftGraphic(wrap(SVG.DRESSER));
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.createIcon(Theme.blackFill(), 14, 14));
btnRemove.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 14));
item.setRightGraphic(btnRemove);
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
@@ -146,7 +202,29 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
return item;
});
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
boxMethods.getChildren().add(boxAuthServers);
ClassTitle title = new ClassTitle(i18n("account.create").toUpperCase(Locale.ROOT));
if (RESTRICTED.get()) {
VBox wrapper = new VBox(offlineItem, boxAuthServers);
wrapper.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(wrapper, i18n("account.login.restricted"));
offlineItem.setDisable(true);
boxAuthServers.setDisable(true);
boxMethods.getChildren().setAll(title, microsoftItem, wrapper);
holder = FXUtils.onWeakChange(RESTRICTED, value -> {
if (!value) {
holder = null;
offlineItem.setDisable(false);
boxAuthServers.setDisable(false);
boxMethods.getChildren().setAll(title, microsoftItem, offlineItem, boxAuthServers);
}
});
} else {
boxMethods.getChildren().setAll(title, microsoftItem, offlineItem, boxAuthServers);
}
}
AdvancedListItem addAuthServerItem = new AdvancedListItem();
@@ -155,7 +233,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.PLUS_CIRCLE_OUTLINE));
addAuthServerItem.setLeftGraphic(wrap(SVG.ADD_CIRCLE));
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
}
@@ -147,7 +147,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
confirmServerPane.setActions(prevButton, cancelButton, finishButton);
}
this.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
this.setContent(addServerPane, ContainerAnimations.NONE);
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
@@ -198,7 +198,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else {
LOG.warning("Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
@@ -208,7 +208,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
}
private void onAddPrev() {
this.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
this.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT);
}
private void onAddFinish() {
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.*;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.binding.BooleanBinding;
@@ -35,6 +38,9 @@ import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextInputControl;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
@@ -65,6 +71,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
@@ -105,12 +112,17 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
public CreateAccountPane(AccountFactory<?> factory) {
if (factory == null) {
showMethodSwitcher = true;
String preferred = config().getPreferredLoginType();
try {
factory = Accounts.getAccountFactory(preferred);
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_OFFLINE;
if (AccountListPage.RESTRICTED.get()) {
showMethodSwitcher = false;
factory = Accounts.FACTORY_MICROSOFT;
} else {
showMethodSwitcher = true;
String preferred = config().getPreferredLoginType();
try {
factory = Accounts.getAccountFactory(preferred);
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_OFFLINE;
}
}
} else {
showMethodSwitcher = false;
@@ -256,16 +268,33 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
};
if (factory instanceof OfflineAccountFactory && username != null && !USERNAME_CHECKER_PATTERN.matcher(username).matches()) {
Controllers.confirm(
JFXButton btnYes = new JFXButton(i18n("button.ok"));
btnYes.getStyleClass().add("dialog-error");
btnYes.setOnAction(e -> doCreate.run());
btnYes.setDisable(true);
int countdown = 10;
KeyFrame[] keyFrames = new KeyFrame[countdown + 1];
for (int i = 0; i < countdown; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(btnYes.textProperty(), i18n("button.ok.countdown", countdown - i)));
}
keyFrames[countdown] = new KeyFrame(Duration.seconds(countdown),
new KeyValue(btnYes.textProperty(), i18n("button.ok")),
new KeyValue(btnYes.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
Controllers.confirmAction(
i18n("account.methods.offline.name.invalid"), i18n("message.warning"),
MessageDialogPane.MessageType.WARNING,
doCreate,
btnYes,
() -> {
lblErrorMessage.setText(i18n("account.methods.offline.name.invalid"));
timeline.stop();
body.setDisable(false);
spinner.hideSpinner();
}
);
timeline.play();
} else {
doCreate.run();
}
@@ -296,7 +325,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
@@ -316,10 +345,12 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setExternalLink("https://www.minecraft.net/password/forgot");
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
createProfileLink.setExternalLink("https://www.minecraft.net/msaprofile/mygames/editprofile");
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
JFXHyperlink bannedQueryLink = new JFXHyperlink(i18n("account.methods.ban_query"));
bannedQueryLink.setExternalLink("https://enforcement.xbox.com/enforcement/showenforcementhistory");
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink, bannedQueryLink);
GridPane.setColumnSpan(box, 2);
if (!IntegrityChecker.isOfficial()) {
@@ -336,7 +367,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
JFXHyperlink officialWebsite = new JFXHyperlink(i18n("account.methods.microsoft.snapshot.website"));
officialWebsite.setExternalLink("https://hmcl.huangyuhui.net");
officialWebsite.setExternalLink(Metadata.PUBLISH_URL);
vbox.getChildren().setAll(hintPane, officialWebsite);
btnAccept.setDisable(true);
@@ -459,7 +490,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
linksContainer.setMinWidth(USE_PREF_SIZE);
JFXButton btnAddServer = new JFXButton();
btnAddServer.setGraphic(SVG.PLUS.createIcon(Theme.blackFill(), 20, 20));
btnAddServer.setGraphic(SVG.ADD.createIcon(Theme.blackFill(), 20));
btnAddServer.getStyleClass().add("toggle-icon4");
btnAddServer.setOnAction(e -> {
Controllers.dialog(new AddAuthlibInjectorServerPane());
@@ -509,7 +540,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory instanceof OfflineAccountFactory) {
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
runInFX(() -> FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters")));
FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
@@ -637,7 +668,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
StackPane.setAlignment(cancel, Pos.BOTTOM_RIGHT);
cancel.setOnAction(e -> latch.countDown());
listBox.startCategory(i18n("account.choose"));
listBox.startCategory(i18n("account.choose").toUpperCase(Locale.ROOT));
setCenter(listBox);
@@ -657,7 +688,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
accountItem.setOnMouseClicked(e -> {
FXUtils.onClicked(accountItem, () -> {
selectedProfile = profile;
latch.countDown();
});
@@ -58,7 +58,7 @@ public class OAuthAccountLoginDialog extends DialogPane {
);
}
});
hintPane.setOnMouseClicked(e -> {
FXUtils.onClicked(hintPane, () -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
@@ -28,9 +28,9 @@ import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.ui.skin.SkinCanvas;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniRunning;
import org.jackhuang.hmcl.ui.skin.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
@@ -101,6 +101,9 @@ public class OfflineAccountSkinPane extends StackPane {
VBox optionPane = new VBox(skinItem, skinOptionPane);
pane.setRight(optionPane);
skinSelector.maxWidthProperty().bind(skinOptionPane.maxWidthProperty().multiply(0.7));
capeSelector.maxWidthProperty().bind(skinOptionPane.maxWidthProperty().multiply(0.7));
layout.setBody(pane);
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
@@ -108,10 +111,10 @@ public class OfflineAccountSkinPane extends StackPane {
skinItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
new MultiFileItem.Option<>("Steve", Skin.Type.STEVE),
new MultiFileItem.Option<>("Alex", Skin.Type.ALEX),
new MultiFileItem.Option<>(i18n("account.skin.type.steve"), Skin.Type.STEVE),
new MultiFileItem.Option<>(i18n("account.skin.type.alex"), Skin.Type.ALEX),
new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE),
new MultiFileItem.Option<>("LittleSkin", Skin.Type.LITTLE_SKIN),
new MultiFileItem.Option<>(i18n("account.skin.type.little_skin"), Skin.Type.LITTLE_SKIN),
new MultiFileItem.Option<>(i18n("account.skin.type.csl_api"), Skin.Type.CUSTOM_SKIN_LOADER_API)
));
@@ -151,7 +154,7 @@ public class OfflineAccountSkinPane extends StackPane {
result.getCape() != null ? result.getCape().getImage() : null);
}
}).start();
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
GridPane gridPane = new GridPane();
@@ -201,6 +204,12 @@ public class OfflineAccountSkinPane extends StackPane {
}
private Skin getSkin() {
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue());
Skin.Type type = skinItem.getSelectedData();
if (type == Skin.Type.LOCAL_FILE) {
return new Skin(type, cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue());
} else {
String cslApi = type == Skin.Type.CUSTOM_SKIN_LOADER_API ? cslApiField.getText() : null;
return new Skin(type, cslApi, null, null, null);
}
}
}
@@ -1,7 +1,28 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.animation;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/**
* @author Glavo
*/
public final class AnimationUtils {
private AnimationUtils() {
@@ -15,9 +36,14 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean enabled = !ConfigHolder.config().isAnimationDisabled();
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
return enabled;
return ENABLED;
}
public static boolean playWindowAnimation() {
return PLAY_WINDOW_ANIMATION;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,165 +27,225 @@ import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
public enum ContainerAnimations {
NONE(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c -> Collections.emptyList()),
public enum ContainerAnimations implements AnimationProducer {
NONE {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Collections.emptyList();
}
},
/**
* A fade between the old and new view
*/
FADE(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
FADE {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)))),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)));
}
},
/**
* A fade between the old and new view
*/
FADE_IN(c -> {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
FADE_IN {
@Override
public void init(AnimationHandler c) {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)))),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)));
}
},
/**
* A fade between the old and new view
*/
FADE_OUT(c -> {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
FADE_OUT {
@Override
public void init(AnimationHandler c) {
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)))),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)));
}
},
/**
* A zoom effect
*/
ZOOM_IN(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
ZOOM_IN {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().scaleXProperty(), 4, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 4, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
},
/**
* A zoom effect
*/
ZOOM_OUT(c -> {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}, c ->
(Arrays.asList(new KeyFrame(Duration.ZERO,
ZOOM_OUT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setTranslateX(0);
c.getPreviousNode().setTranslateY(0);
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(1);
c.getCurrentNode().setTranslateX(0);
c.getCurrentNode().setTranslateY(0);
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getPreviousNode().scaleXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().scaleYProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
},
/**
* A swipe effect
*/
SWIPE_LEFT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_LEFT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)));
}
},
/**
* A swipe effect
*/
SWIPE_RIGHT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_RIGHT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
new KeyFrame(c.getDuration(),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)));
}
},
SWIPE_LEFT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_LEFT_FADE_SHORT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
@@ -194,19 +254,26 @@ public enum ContainerAnimations {
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)))),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
},
SWIPE_RIGHT_FADE_SHORT(c -> {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}, c ->
Arrays.asList(new KeyFrame(Duration.ZERO,
SWIPE_RIGHT_FADE_SHORT {
@Override
public void init(AnimationHandler c) {
c.getPreviousNode().setScaleX(1);
c.getPreviousNode().setScaleY(1);
c.getPreviousNode().setOpacity(0);
c.getPreviousNode().setTranslateX(0);
c.getCurrentNode().setScaleX(1);
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(1);
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
}
@Override
public List<KeyFrame> animate(AnimationHandler c) {
return Arrays.asList(new KeyFrame(Duration.ZERO,
new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
@@ -215,38 +282,12 @@ public enum ContainerAnimations {
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))));
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
}
};
private final AnimationProducer animationProducer;
private ContainerAnimations opposite;
ContainerAnimations(Consumer<AnimationHandler> init, Function<AnimationHandler, List<KeyFrame>> animationProducer) {
this.animationProducer = new AnimationProducer() {
@Override
public void init(AnimationHandler handler) {
init.accept(handler);
}
@Override
public List<KeyFrame> animate(AnimationHandler handler) {
return animationProducer.apply(handler);
}
@Override
public @Nullable AnimationProducer opposite() {
return opposite != null ? opposite.getAnimationProducer() : null;
}
};
}
public AnimationProducer getAnimationProducer() {
return animationProducer;
}
public ContainerAnimations getOpposite() {
return opposite;
}
static {
NONE.opposite = NONE;
FADE.opposite = FADE;
@@ -257,4 +298,15 @@ public enum ContainerAnimations {
ZOOM_IN.opposite = ZOOM_OUT;
ZOOM_OUT.opposite = ZOOM_IN;
}
@Override
public abstract void init(AnimationHandler handler);
@Override
public abstract List<KeyFrame> animate(AnimationHandler handler);
@Override
public @Nullable ContainerAnimations opposite() {
return opposite;
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.Node;
@@ -26,6 +25,8 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
public class TransitionPane extends StackPane implements AnimationHandler {
private static final Duration DEFAULT_DURATION = Duration.millis(200);
private Duration duration;
private Node previousNode, currentNode;
@@ -55,7 +56,7 @@ public class TransitionPane extends StackPane implements AnimationHandler {
}
public void setContent(Node newView, AnimationProducer transition) {
setContent(newView, transition, Duration.millis(160));
setContent(newView, transition, DEFAULT_DURATION);
}
public void setContent(Node newView, AnimationProducer transition, Duration duration) {
@@ -64,26 +65,25 @@ public class TransitionPane extends StackPane implements AnimationHandler {
updateContent(newView);
if (previousNode == EMPTY_PANE) {
setMouseTransparent(false);
getChildren().setAll(newView);
return;
}
if (AnimationUtils.isAnimationEnabled()) {
if (AnimationUtils.isAnimationEnabled() && transition != ContainerAnimations.NONE) {
setMouseTransparent(true);
transition.init(this);
// runLater or "init" will not work
Platform.runLater(() -> {
Timeline newAnimation = new Timeline();
newAnimation.getKeyFrames().addAll(transition.animate(this));
newAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
newAnimation.getKeyFrames().setAll(transition.animate(this));
newAnimation.setOnFinished(e -> {
setMouseTransparent(false);
getChildren().remove(previousNode);
}));
});
FXUtils.playAnimation(this, "transition_pane", newAnimation);
});
} else {
setMouseTransparent(false);
getChildren().remove(previousNode);
}
}
@@ -99,8 +99,6 @@ public class TransitionPane extends StackPane implements AnimationHandler {
if (previousNode == newView)
previousNode = EMPTY_PANE;
setMouseTransparent(true);
currentNode = newView;
getChildren().setAll(previousNode, currentNode);
@@ -24,6 +24,8 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import java.util.function.Consumer;
@@ -54,11 +56,36 @@ public class AdvancedListBox extends ScrollPane {
return this;
}
public AdvancedListBox addNavigationDrawerItem(Consumer<AdvancedListItem> fn) {
private AdvancedListItem createNavigationDrawerItem(String title, SVG leftGraphic) {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
item.setActionButtonVisible(false);
fn.accept(item);
item.setTitle(title);
if (leftGraphic != null) {
item.setLeftGraphic(VersionPage.wrap(leftGraphic));
}
return item;
}
public AdvancedListBox addNavigationDrawerItem(String title, SVG leftGraphic, Runnable onAction) {
return addNavigationDrawerItem(title, leftGraphic, onAction, null);
}
public AdvancedListBox addNavigationDrawerItem(String title, SVG leftGraphic, Runnable onAction, Consumer<AdvancedListItem> initializer) {
AdvancedListItem item = createNavigationDrawerItem(title, leftGraphic);
if (onAction != null) {
item.setOnAction(e -> onAction.run());
}
if (initializer != null) {
initializer.accept(item);
}
return add(item);
}
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title, SVG leftGraphic) {
AdvancedListItem item = createNavigationDrawerItem(title, leftGraphic);
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
item.setOnAction(e -> tabHeader.select(tab));
return add(item);
}
@@ -25,7 +25,6 @@ import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Pair;
@@ -42,7 +41,7 @@ public class AdvancedListItem extends Control {
public AdvancedListItem() {
getStyleClass().add("advanced-list-item");
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
FXUtils.onClicked(this, () -> fireEvent(new ActionEvent()));
}
public Node getLeftGraphic() {
@@ -41,6 +41,7 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
RipplerContainer container = new RipplerContainer(root);
HBox left = new HBox();
left.setAlignment(Pos.CENTER_LEFT);
left.setMouseTransparent(true);
TwoLineListItem item = new TwoLineListItem();
@@ -1,37 +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.ui.construct;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
public class AnnouncementCard extends VBox {
public AnnouncementCard(String title, String content) {
TextFlow tf = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
Label label = new Label(title);
label.getStyleClass().add("title");
getChildren().setAll(label, tf);
setSpacing(14);
getStyleClass().addAll("card", "announcement");
}
}
@@ -25,9 +25,6 @@ import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -85,7 +82,7 @@ final class ComponentListCell extends StackPane {
VBox groupNode = new VBox();
Node expandIcon = SVG.EXPAND.createIcon(Theme.blackFill(), 20, 20);
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(Theme.blackFill(), 20);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
@@ -139,7 +136,7 @@ final class ComponentListCell extends StackPane {
container.getChildren().setAll(content);
groupNode.getChildren().add(container);
EventHandler<Event> onExpand = e -> {
Runnable onExpand = () -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
expandAnimation.stop();
}
@@ -152,8 +149,8 @@ final class ComponentListCell extends StackPane {
}
Platform.runLater(() -> {
double newAnimatedHeight = (list.prefHeight(-1) + 8 + 10) * (expanded ? 1 : -1);
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(-1);
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + 8 + 10) * (expanded ? 1 : -1);
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(list.getWidth());
double contentHeight = expanded ? newAnimatedHeight : 0;
if (expanded) {
@@ -182,8 +179,8 @@ final class ComponentListCell extends StackPane {
});
};
headerRippler.setOnMouseClicked(onExpand);
expandButton.setOnAction((EventHandler<ActionEvent>) (Object) onExpand);
FXUtils.onClicked(headerRippler, onExpand);
expandButton.setOnAction(e -> onExpand.run());
expandedProperty().addListener((a, b, newValue) -> expandIcon.setRotate(newValue ? 180 : 0));
@@ -58,9 +58,9 @@ public class FileItem extends BorderPane {
setLeft(left);
JFXButton right = new JFXButton();
right.setGraphic(SVG.PENCIL.createIcon(Theme.blackFill(), 15, 15));
right.setGraphic(SVG.EDIT.createIcon(Theme.blackFill(), 16));
right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore());
right.setOnAction(e -> onExplore());
FXUtils.installFastTooltip(right, i18n("button.edit"));
setRight(right);
@@ -81,7 +81,7 @@ public class FileSelector extends HBox {
FXUtils.bindString(customField, valueProperty());
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), 15, 15));
selectButton.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), 15));
selectButton.setOnAction(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
@@ -21,6 +21,7 @@ import static javafx.collections.FXCollections.emptyObservableList;
import static javafx.collections.FXCollections.observableList;
import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import com.jfoenix.controls.JFXComboBox;
@@ -29,11 +30,13 @@ import com.jfoenix.controls.JFXListCell;
import javafx.beans.binding.Bindings;
import javafx.scene.text.Font;
public class FontComboBox extends JFXComboBox<String> {
public final class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false;
public FontComboBox() {
setMinWidth(260);
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new JFXListCell<String>() {
@@ -51,7 +54,7 @@ public class FontComboBox extends JFXComboBox<String> {
itemsProperty().bind(BindingMapping.of(valueProperty())
.map(value -> value == null ? emptyObservableList() : singletonObservableList(value)));
setOnMouseClicked(e -> {
FXUtils.onClicked(this, () -> {
if (loaded)
return;
itemsProperty().unbind();
@@ -49,25 +49,25 @@ public class HintPane extends VBox {
SVG svg;
switch (type) {
case INFO:
svg = SVG.INFORMATION_OUTLINE;
svg = SVG.INFO;
break;
case ERROR:
svg = SVG.CLOSE_CIRCLE_OUTLINE;
svg = SVG.ERROR;
break;
case SUCCESS:
svg = SVG.CHECK_CIRCLE_OUTLINE;
svg = SVG.CHECK_CIRCLE;
break;
case WARNING:
svg = SVG.ALERT_OUTLINE;
svg = SVG.WARNING;
break;
case QUESTION:
svg = SVG.HELP_CIRCLE_OUTLINE;
svg = SVG.HELP;
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
HBox hbox = new HBox(svg.createIcon(Theme.blackFill(), 16, 16), new Text(type.getDisplayName()));
HBox hbox = new HBox(svg.createIcon(Theme.blackFill(), 16), new Text(type.getDisplayName()));
hbox.setAlignment(Pos.CENTER_LEFT);
flow.getChildren().setAll(label);
getChildren().setAll(hbox, flow);
@@ -25,14 +25,14 @@ import org.jackhuang.hmcl.ui.SVG;
public class IconedMenuItem extends IconedItem {
public IconedMenuItem(SVG icon, String text, Runnable action, JFXPopup popup) {
super(icon != null ? FXUtils.limitingSize(icon.createIcon(Theme.blackFill(), 14, 14), 14, 14) : null, text);
super(icon != null ? FXUtils.limitingSize(icon.createIcon(Theme.blackFill(), 14), 14, 14) : null, text);
getStyleClass().setAll("iconed-menu-item");
if (popup == null) {
setOnMouseClicked(e -> action.run());
FXUtils.onClicked(this, action);
} else {
setOnMouseClicked(e -> {
FXUtils.onClicked(this, () -> {
action.run();
popup.hide();
});
@@ -17,7 +17,6 @@ import javafx.scene.layout.Priority;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.StringUtils;
public class IconedTwoLineListItem extends HBox {
@@ -29,13 +28,8 @@ public class IconedTwoLineListItem extends HBox {
private final ImageView imageView = new ImageView();
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
private final Lazy<JFXButton> externalLinkButton = new Lazy<>(() -> {
JFXButton button = new JFXButton();
button.getStyleClass().add("toggle-icon4");
button.setGraphic(SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), -1, -1));
button.setOnAction(e -> FXUtils.openLink(externalLink.get()));
return button;
});
private JFXButton externalLinkButton;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener observer;
@@ -52,7 +46,7 @@ public class IconedTwoLineListItem extends HBox {
getChildren().clear();
if (image.get() != null) getChildren().add(imageView);
getChildren().add(twoLineListItem);
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(externalLinkButton.get());
if (StringUtils.isNotBlank(externalLink.get())) getChildren().add(getExternalLinkButton());
}, image, externalLink);
}
@@ -111,4 +105,14 @@ public class IconedTwoLineListItem extends HBox {
public ImageView getImageView() {
return imageView;
}
public JFXButton getExternalLinkButton() {
if (externalLinkButton == null) {
externalLinkButton = new JFXButton();
externalLinkButton.getStyleClass().add("toggle-icon4");
externalLinkButton.setGraphic(SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), -1));
externalLinkButton.setOnAction(e -> FXUtils.openLink(externalLink.get()));
}
return externalLinkButton;
}
}
@@ -23,12 +23,12 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -44,8 +44,8 @@ public final class ImagePickerItem extends BorderPane {
private final ImageView imageView;
private final StringProperty title = new SimpleStringProperty(this, "title");
private final ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
private final ObjectProperty<EventHandler<? super MouseEvent>> onDeleteButtonClicked = new SimpleObjectProperty<>(this, "onDeleteButtonClicked");
private final ObjectProperty<EventHandler<ActionEvent>> onSelectButtonClicked = new SimpleObjectProperty<>(this, "onSelectButtonClicked");
private final ObjectProperty<EventHandler<ActionEvent>> onDeleteButtonClicked = new SimpleObjectProperty<>(this, "onDeleteButtonClicked");
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
public ImagePickerItem() {
@@ -54,13 +54,13 @@ public final class ImagePickerItem extends BorderPane {
imageView.setPreserveRatio(true);
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.PENCIL.createIcon(Theme.blackFill(), 20, 20));
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
selectButton.setGraphic(SVG.EDIT.createIcon(Theme.blackFill(), 20));
selectButton.onActionProperty().bind(onSelectButtonClicked);
selectButton.getStyleClass().add("toggle-icon4");
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 20, 20));
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 20));
deleteButton.onActionProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
@@ -93,27 +93,27 @@ public final class ImagePickerItem extends BorderPane {
this.title.set(title);
}
public EventHandler<? super MouseEvent> getOnSelectButtonClicked() {
public EventHandler<ActionEvent> getOnSelectButtonClicked() {
return onSelectButtonClicked.get();
}
public ObjectProperty<EventHandler<? super MouseEvent>> onSelectButtonClickedProperty() {
public ObjectProperty<EventHandler<ActionEvent>> onSelectButtonClickedProperty() {
return onSelectButtonClicked;
}
public void setOnSelectButtonClicked(EventHandler<? super MouseEvent> onSelectButtonClicked) {
public void setOnSelectButtonClicked(EventHandler<ActionEvent> onSelectButtonClicked) {
this.onSelectButtonClicked.set(onSelectButtonClicked);
}
public EventHandler<? super MouseEvent> getOnDeleteButtonClicked() {
public EventHandler<ActionEvent> getOnDeleteButtonClicked() {
return onDeleteButtonClicked.get();
}
public ObjectProperty<EventHandler<? super MouseEvent>> onDeleteButtonClickedProperty() {
public ObjectProperty<EventHandler<ActionEvent>> onDeleteButtonClickedProperty() {
return onDeleteButtonClicked;
}
public void setOnDeleteButtonClicked(EventHandler<? super MouseEvent> onDeleteButtonClicked) {
public void setOnDeleteButtonClicked(EventHandler<ActionEvent> onDeleteButtonClicked) {
this.onDeleteButtonClicked.set(onDeleteButtonClicked);
}
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.FutureCallback;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InputDialogPane extends JFXDialogLayout {
@@ -68,6 +69,8 @@ public class InputDialogPane extends JFXDialogLayout {
lblCreationWarning.setText(msg);
});
});
onEscPressed(this, cancelButton::fire);
}
public CompletableFuture<String> getCompletableFuture() {
@@ -20,24 +20,28 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Stack;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
public class JFXDialogPane extends StackPane {
private final ArrayList<Node> stack = new ArrayList<>();
public int size() {
return stack.size();
}
public Optional<Node> peek() {
if (stack.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(stack.peek());
return Optional.of(stack.get(stack.size() - 1));
}
}
public void push(Node node) {
stack.push(node);
stack.add(node);
getChildren().setAll(node);
LOG.info(this + " " + stack);
@@ -48,7 +52,7 @@ public class StackContainerPane extends StackPane {
if (stack.isEmpty())
getChildren().setAll();
else
getChildren().setAll(stack.peek());
getChildren().setAll(stack.get(stack.size() - 1));
LOG.info(this + " " + stack + ", removed: " + flag + ", object: " + node);
}
@@ -22,18 +22,12 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
public class JFXHyperlink extends Hyperlink {
public JFXHyperlink() {
super();
setGraphic(SVG.LAUNCH_OUTLINE.createIcon(Theme.blackFill(), 16, 16));
}
public final class JFXHyperlink extends Hyperlink {
public JFXHyperlink(String text) {
super(text);
setGraphic(SVG.LAUNCH_OUTLINE.createIcon(Theme.blackFill(), 16, 16));
setGraphic(SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), 16));
}
public void setExternalLink(String externalLink) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.DoubleBinding;
import javafx.css.PseudoClass;
import javafx.scene.control.ListCell;
import javafx.scene.layout.Region;
@@ -45,9 +46,11 @@ public abstract class MDListCell<T> extends ListCell<T> {
Region clippedContainer = (Region) listView.lookup(".clipped-container");
setPrefWidth(0);
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
minWidthProperty().bind(clippedContainer.widthProperty());
DoubleBinding converted = clippedContainer.widthProperty().subtract(1);
maxWidthProperty().bind(converted);
prefWidthProperty().bind(converted);
minWidthProperty().bind(converted);
}
}
@@ -81,8 +81,8 @@ public class MenuUpDownButton extends Control {
label.setStyle("-fx-text-fill: black;");
label.textProperty().bind(control.text);
Node up = SVG.MENU_UP.createIcon(Theme.blackFill(), 16, 16);
Node down = SVG.MENU_DOWN.createIcon(Theme.blackFill(), 16, 16);
Node up = SVG.ARROW_DROP_UP.createIcon(Theme.blackFill(), 16);
Node down = SVG.ARROW_DROP_DOWN.createIcon(Theme.blackFill(), 16);
JFXButton button = new JFXButton();
button.setGraphic(content);
@@ -74,24 +74,24 @@ public final class MessageDialogPane extends HBox {
SVG svg;
switch (type) {
case INFO:
svg = SVG.INFO_CIRCLE;
svg = SVG.INFO;
break;
case ERROR:
svg = SVG.CLOSE_CIRCLE;
svg = SVG.ERROR;
break;
case SUCCESS:
svg = SVG.CHECK_CIRCLE;
break;
case WARNING:
svg = SVG.ALERT;
svg = SVG.WARNING;
break;
case QUESTION:
svg = SVG.HELP_CIRCLE;
svg = SVG.HELP;
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
graphic.setGraphic(svg.createIcon(Theme.blackFill(), 40, 40));
graphic.setGraphic(svg.createIcon(Theme.blackFill(), 40));
VBox vbox = new VBox();
HBox.setHgrow(vbox, Priority.ALWAYS);
@@ -160,12 +160,29 @@ public final class MessageDialogPane extends HBox {
this.dialog = new MessageDialogPane(text, title, type);
}
public Builder addHyperLink(String text, String externalLink) {
JFXHyperlink link = new JFXHyperlink(text);
link.setExternalLink(externalLink);
dialog.actions.getChildren().add(link);
return this;
}
public Builder addAction(Node actionNode) {
dialog.addButton(actionNode);
actionNode.getStyleClass().add("dialog-accept");
return this;
}
public Builder addAction(String text, @Nullable Runnable action) {
JFXButton btnAction = new JFXButton(text);
btnAction.getStyleClass().add("dialog-accept");
if (action != null) {
btnAction.setOnAction(e -> action.run());
}
dialog.addButton(btnAction);
return this;
}
public Builder ok(@Nullable Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -54,10 +53,9 @@ public final class MultiFileItem<T> extends VBox {
setSpacing(8);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
selectedData.set(newValue != null ? (T) newValue.getUserData() : null);
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
selectedData.set((T) newValue.getUserData());
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
@@ -114,6 +112,7 @@ public final class MultiFileItem<T> extends VBox {
public static class Option<T> {
protected final String title;
protected String subtitle;
protected String tooltip;
protected final T data;
protected final BooleanProperty selected = new SimpleBooleanProperty();
protected final JFXRadioButton left = new JFXRadioButton();
@@ -140,6 +139,11 @@ public final class MultiFileItem<T> extends VBox {
return this;
}
public Option<T> setTooltip(String tooltip) {
this.tooltip = tooltip;
return this;
}
public boolean isSelected() {
return left.isSelected();
}
@@ -161,6 +165,8 @@ public final class MultiFileItem<T> extends VBox {
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
left.setToggleGroup(group);
left.setUserData(data);
if (StringUtils.isNotBlank(tooltip))
FXUtils.installFastTooltip(left, tooltip);
pane.setLeft(left);
if (StringUtils.isNotBlank(subtitle)) {
@@ -183,6 +189,10 @@ public final class MultiFileItem<T> extends VBox {
super(title, data);
}
public JFXTextField getCustomField() {
return customField;
}
public String getValue() {
return customField.getText();
}
@@ -264,8 +274,9 @@ public final class MultiFileItem<T> extends VBox {
return this;
}
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return selector.getExtensionFilters();
public FileOption<T> addExtensionFilter(FileChooser.ExtensionFilter filter) {
selector.getExtensionFilters().add(filter);
return this;
}
@Override
@@ -126,7 +126,7 @@ public class Navigator extends TransitionPane {
if (obj instanceof AnimationProducer) {
setContent(node, (AnimationProducer) obj);
} else {
setContent(node, ContainerAnimations.NONE.getAnimationProducer());
setContent(node, ContainerAnimations.NONE);
}
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.NAVIGATED);
@@ -61,9 +61,7 @@ public class OptionToggleButton extends StackPane {
toggleButton.setSize(8);
FXUtils.setLimitHeight(toggleButton, 30);
container.setOnMouseClicked(e -> {
toggleButton.setSelected(!toggleButton.isSelected());
});
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
if (StringUtils.isNotBlank(subtitle)) {
@@ -39,6 +39,7 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.util.Lang;
import java.util.List;
@@ -46,6 +47,7 @@ import java.util.List;
@DefaultProperty("container")
public class RipplerContainer extends StackPane {
private static final String DEFAULT_STYLE_CLASS = "rippler-container";
private static final Duration DURATION = Duration.millis(200);
private final ObjectProperty<Node> container = new SimpleObjectProperty<>(this, "container", null);
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,this, "ripplerFill", null);
@@ -103,11 +105,10 @@ public class RipplerContainer extends StackPane {
rectangle.heightProperty().bind(heightProperty());
}));
setOnMouseEntered(e -> {
new Transition() {
if (AnimationUtils.isAnimationEnabled()) {
setOnMouseEntered(e -> new Transition() {
{
setCycleCount(1);
setCycleDuration(Duration.millis(200));
setCycleDuration(DURATION);
setInterpolator(Interpolator.EASE_IN);
}
@@ -115,14 +116,11 @@ public class RipplerContainer extends StackPane {
protected void interpolate(double frac) {
interpolateBackground(frac);
}
}.play();
});
}.play());
setOnMouseExited(e -> {
new Transition() {
setOnMouseExited(e -> new Transition() {
{
setCycleCount(1);
setCycleDuration(Duration.millis(200));
setCycleDuration(DURATION);
setInterpolator(Interpolator.EASE_OUT);
}
@@ -130,8 +128,11 @@ public class RipplerContainer extends StackPane {
protected void interpolate(double frac) {
interpolateBackground(1 - frac);
}
}.play();
});
}.play());
} else {
setOnMouseEntered(e -> interpolateBackground(1));
setOnMouseExited(e -> interpolateBackground(0));
}
}
private void interpolateBackground(double frac) {
@@ -1,62 +0,0 @@
/*
* 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.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ServerAddressValidator extends ValidatorBase {
private final boolean nullable;
public ServerAddressValidator() {
this(false);
}
public ServerAddressValidator(@NamedArg("nullable") boolean nullable) {
this(i18n("input.url"), nullable);
}
public ServerAddressValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private static final Pattern PATTERN = Pattern.compile("[-a-zA-Z0-9@:%._+~#=]{1,256}(:\\d+)?");
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else
hasErrors.set(!PATTERN.matcher(textField.getText()).matches());
}
}
@@ -32,7 +32,6 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@DefaultProperty("content")
public class SpinnerPane extends Control {
@@ -101,18 +100,19 @@ public class SpinnerPane extends Control {
return onFailedActionProperty().get();
}
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
private final ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
@Override
protected Skin createDefaultSkin() {
protected SkinBase<SpinnerPane> createDefaultSkin() {
return new Skin(this);
}
private static class Skin extends SkinBase<SpinnerPane> {
private static final class Skin extends SkinBase<SpinnerPane> {
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final StackPane topPane = new StackPane();
@@ -122,20 +122,18 @@ public class SpinnerPane extends Control {
@SuppressWarnings("FieldCanBeLocal") // prevent from gc.
private final InvalidationListener observer;
protected Skin(SpinnerPane control) {
Skin(SpinnerPane control) {
super(control);
topPane.getChildren().setAll(spinner);
topPane.getStyleClass().add("notice-pane");
failedPane.getStyleClass().add("notice-pane");
failedPane.getChildren().setAll(failedReasonLabel);
failedPane.onMouseClickedProperty().bind(
BindingMapping.of(control.onFailedAction)
.map(actionHandler -> (e -> {
if (actionHandler != null) {
actionHandler.handle(new Event(FAILED_ACTION));
}
})));
FXUtils.onClicked(failedPane, () -> {
EventHandler<Event> action = control.getOnFailedAction();
if (action != null)
action.handle(new Event(FAILED_ACTION));
});
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
if (newValue == null) {
@@ -148,34 +146,16 @@ public class SpinnerPane extends Control {
observer = FXUtils.observeWeak(() -> {
if (getSkinnable().getFailedReason() != null) {
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(failedPane, ContainerAnimations.FADE);
failedReasonLabel.setText(getSkinnable().getFailedReason());
} else if (getSkinnable().isLoading()) {
root.setContent(topPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(topPane, ContainerAnimations.FADE);
} else {
root.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
root.setContent(contentPane, ContainerAnimations.FADE);
}
}, getSkinnable().loadingProperty(), getSkinnable().failedReasonProperty());
}
}
public interface State {}
public static class LoadedState implements State {}
public static class LoadingState implements State {}
public static class FailedState implements State {
private final String reason;
public FailedState(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
public static final EventType<Event> FAILED_ACTION = new EventType<>(Event.ANY, "FAILED_ACTION");
}
@@ -30,7 +30,6 @@ import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.transform.Rotate;
@@ -594,11 +593,9 @@ public class TabHeader extends Control implements TabControl, PageAware {
FXUtils.onChangeAndOperate(tab.selectedProperty(), selected -> inner.pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, selected));
this.setOnMouseClicked(event -> {
if (event.getButton() == MouseButton.PRIMARY) {
this.setOpacity(1);
getSkinnable().getSelectionModel().select(tab);
}
FXUtils.onClicked(this, () -> {
this.setOpacity(1);
getSkinnable().getSelectionModel().select(tab);
});
}
}
@@ -98,17 +98,15 @@ public class TaskExecutorDialogPane extends BorderPane {
double speed = speedEvent.getSpeed();
if (speed > 1024) {
speed /= 1024;
unit = "KB/s";
unit = "KiB/s";
}
if (speed > 1024) {
speed /= 1024;
unit = "MB/s";
unit = "MiB/s";
}
double finalSpeed = speed;
String finalUnit = unit;
Platform.runLater(() -> {
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
});
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
};
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.java.JavaInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
@@ -146,21 +147,23 @@ public final class TaskListPane extends StackPane {
} else if (task instanceof ModpackUpdateTask) {
task.setName(i18n("modpack.update"));
} else if (task instanceof CurseInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
task.setName(i18n("modpack.installing.given", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
task.setName(i18n("modpack.installing.given", i18n("modpack.type.multimc")));
} else if (task instanceof ModrinthInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.modrinth")));
task.setName(i18n("modpack.installing.given", i18n("modpack.type.modrinth")));
} else if (task instanceof ServerModpackLocalInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.server")));
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
} else if (task instanceof JavaDownloadTask) {
} else if (task instanceof MojangJavaDownloadTask) {
task.setName(i18n("download.java"));
} else if (task instanceof JavaInstallTask) {
task.setName(i18n("java.installing"));
}
Platform.runLater(() -> {
@@ -267,21 +270,21 @@ public final class TaskListPane extends StackPane {
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
setPadding(new Insets(0, 0, 8, 4));
setCenter(title);
setLeft(FXUtils.limitingSize(SVG.DOTS_HORIZONTAL.createIcon(Theme.blackFill(), 14, 14), 14, 14));
setLeft(FXUtils.limitingSize(SVG.MORE_HORIZ.createIcon(Theme.blackFill(), 14), 14, 14));
}
public void begin() {
if (started) return;
started = true;
setLeft(FXUtils.limitingSize(SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), 14, 14), 14, 14));
setLeft(FXUtils.limitingSize(SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), 14), 14, 14));
}
public void fail() {
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14, 14), 14, 14));
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14), 14, 14));
}
public void succeed() {
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14, 14), 14, 14));
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14), 14, 14));
}
public void count() {
@@ -18,6 +18,9 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXSnackbar;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -35,6 +38,9 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.wizard.Navigation;
public class Decorator extends Control {
@@ -61,12 +67,40 @@ public class Decorator extends Control {
private final ReadOnlyBooleanWrapper allowMove = new ReadOnlyBooleanWrapper();
private final ReadOnlyBooleanWrapper dragging = new ReadOnlyBooleanWrapper();
private boolean playRestoreMinimizeAnimation = false;
public Decorator(Stage primaryStage) {
this.primaryStage = primaryStage;
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
primaryStage.initStyle(StageStyle.UNDECORATED);
if (AnimationUtils.playWindowAnimation()) {
FXUtils.onChange(primaryStage.iconifiedProperty(), iconified -> {
if (playRestoreMinimizeAnimation && !iconified) {
playRestoreMinimizeAnimation = false;
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(this.opacityProperty(), 0, FXUtils.EASE),
new KeyValue(this.translateYProperty(), 200, FXUtils.EASE),
new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE),
new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE),
new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(this.opacityProperty(), 1, FXUtils.EASE),
new KeyValue(this.translateYProperty(), 0, FXUtils.EASE),
new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE),
new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE),
new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE)
)
);
timeline.play();
}
});
}
}
public Stage getPrimaryStage() {
@@ -239,7 +273,29 @@ public class Decorator extends Control {
}
public void minimize() {
primaryStage.setIconified(true);
if (AnimationUtils.playWindowAnimation()) {
playRestoreMinimizeAnimation = true;
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(this.opacityProperty(), 1, FXUtils.EASE),
new KeyValue(this.translateYProperty(), 0, FXUtils.EASE),
new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE),
new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE),
new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(this.opacityProperty(), 0, FXUtils.EASE),
new KeyValue(this.translateYProperty(), 200, FXUtils.EASE),
new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE),
new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE),
new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE)
)
);
timeline.setOnFinished(event -> primaryStage.setIconified(true));
timeline.play();
} else {
primaryStage.setIconified(true);
}
}
public void close() {
@@ -31,7 +31,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DecoratorAnimationProducer implements AnimationProducer {
public final class DecoratorAnimationProducer implements AnimationProducer {
@Override
public void init(AnimationHandler handler) {
}
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXSnackbar;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
@@ -30,30 +33,32 @@ import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -78,11 +83,32 @@ public class DecoratorController {
private final Navigator navigator;
private JFXDialog dialog;
private StackContainerPane dialogPane;
private JFXDialogPane dialogPane;
public DecoratorController(Stage stage, Node mainPage) {
decorator = new Decorator(stage);
decorator.setOnCloseButtonAction(Launcher::stopApplication);
decorator.setOnCloseButtonAction(() -> {
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(decorator.opacityProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.scaleXProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.scaleYProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.scaleZProperty(), 0.3, FXUtils.EASE)
),
new KeyFrame(Duration.millis(200),
new KeyValue(decorator.opacityProperty(), 0, FXUtils.EASE),
new KeyValue(decorator.scaleXProperty(), 0.8, FXUtils.EASE),
new KeyValue(decorator.scaleYProperty(), 0.8, FXUtils.EASE),
new KeyValue(decorator.scaleZProperty(), 0.8, FXUtils.EASE)
)
);
timeline.setOnFinished(event -> Launcher.stopApplication());
timeline.play();
} else {
Launcher.stopApplication();
}
});
decorator.titleTransparentProperty().bind(config().titleTransparentProperty());
navigator = new Navigator();
@@ -144,6 +170,18 @@ public class DecoratorController {
// press ESC to go back
onEscPressed(navigator, this::back);
try {
// For JavaFX 12+
MouseButton button = MouseButton.valueOf("BACK");
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == button) {
back();
e.consume();
}
});
} catch (IllegalArgumentException ignored) {
}
}
public Decorator getDecorator() {
@@ -166,23 +204,17 @@ public class DecoratorController {
case CUSTOM:
String backgroundImage = config().getBackgroundImage();
if (backgroundImage != null)
image = tryLoadImage(Paths.get(backgroundImage)).orElse(null);
try {
image = tryLoadImage(Paths.get(backgroundImage));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
break;
case NETWORK:
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) {
try {
URLConnection connection = NetworkUtils.createConnection(new URL(backgroundImageUrl));
if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
}
try (InputStream input = connection.getInputStream()) {
image = new Image(input);
if (image.isError()) {
throw image.getException();
}
}
image = FXUtils.loadImage(new URL(backgroundImageUrl));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
@@ -204,73 +236,67 @@ public class DecoratorController {
* Load background image from bg/, background.png, background.jpg, background.gif
*/
private Image loadDefaultBackgroundImage() {
Optional<Image> image = randomImageIn(Paths.get("bg"));
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.png"));
Image image = randomImageIn(Metadata.HMCL_CURRENT_DIRECTORY.resolve("background"));
if (image != null)
return image;
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
image = tryLoadImage(Metadata.HMCL_CURRENT_DIRECTORY.resolve("background." + extension));
if (image != null)
return image;
}
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.jpg"));
image = randomImageIn(Metadata.CURRENT_DIRECTORY.resolve("bg"));
if (image != null)
return image;
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
image = tryLoadImage(Metadata.CURRENT_DIRECTORY.resolve("background." + extension));
if (image != null)
return image;
}
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.gif"));
}
return image.orElseGet(() -> newBuiltinImage("/assets/img/background.jpg"));
return newBuiltinImage("/assets/img/background.jpg");
}
private Optional<Image> randomImageIn(Path imageDir) {
private @Nullable Image randomImageIn(Path imageDir) {
if (!Files.isDirectory(imageDir)) {
return Optional.empty();
return null;
}
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(it -> FXUtils.IMAGE_EXTENSIONS.contains(getExtension(it).toLowerCase(Locale.ROOT)))
.filter(Files::isReadable)
.filter(it -> {
String ext = getExtension(it).toLowerCase(Locale.ROOT);
return ext.equals("png") || ext.equals("jpg") || ext.equals("gif");
})
.collect(toList());
} catch (IOException e) {
LOG.warning("Failed to list files in ./bg", e);
return Optional.empty();
return null;
}
Random rnd = new Random();
while (candidates.size() > 0) {
while (!candidates.isEmpty()) {
int selected = rnd.nextInt(candidates.size());
Optional<Image> loaded = tryLoadImage(candidates.get(selected));
if (loaded.isPresent()) {
Image loaded = tryLoadImage(candidates.get(selected));
if (loaded != null)
return loaded;
} else {
else
candidates.remove(selected);
}
}
return Optional.empty();
return null;
}
private Optional<Image> tryLoadImage(Path path) {
private @Nullable Image tryLoadImage(Path path) {
if (!Files.isReadable(path))
return Optional.empty();
return null;
return tryLoadImage(path.toAbsolutePath().toUri().toString());
}
private Optional<Image> tryLoadImage(String url) {
Image img;
try {
img = new Image(url);
} catch (IllegalArgumentException e) {
return FXUtils.loadImage(path);
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
return Optional.empty();
return null;
}
if (img.getException() != null) {
LOG.warning("Couldn't load background image", img.getException());
return Optional.empty();
}
return Optional.of(img);
}
// ==== Navigation ====
@@ -369,7 +395,7 @@ public class DecoratorController {
return;
}
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialogPane = new JFXDialogPane();
dialog.setContent(dialogPane);
decorator.capableDraggingWindow(dialog);
@@ -386,21 +412,22 @@ public class DecoratorController {
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
node.addEventHandler(DialogCloseEvent.CLOSE, handler);
if (node instanceof DialogAware) {
DialogAware dialogAware = (DialogAware) node;
if (dialog.isVisible()) {
dialogAware.onDialogShown();
} else {
dialog.visibleProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (newValue) {
dialogAware.onDialogShown();
observable.removeListener(this);
}
if (dialog.isVisible()) {
dialog.requestFocus();
if (node instanceof DialogAware)
((DialogAware) node).onDialogShown();
} else {
dialog.visibleProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (newValue) {
dialog.requestFocus();
if (node instanceof DialogAware)
((DialogAware) node).onDialogShown();
observable.removeListener(this);
}
});
}
}
});
}
}
@@ -412,18 +439,21 @@ public class DecoratorController {
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
if (dialog != null) {
dialogPane.pop(node);
JFXDialogPane pane = dialogPane;
if (node instanceof DialogAware) {
((DialogAware) node).onDialogClosed();
}
if (dialogPane.getChildren().isEmpty()) {
if (pane.size() == 1 && pane.peek().orElse(null) == node) {
dialog.setOnDialogClosed(e -> pane.pop(node));
dialog.close();
dialog = null;
dialogPane = null;
navigator.setDisable(false);
} else {
pane.pop(node);
}
if (node instanceof DialogAware) {
((DialogAware) node).onDialogClosed();
}
}
}
@@ -443,7 +473,7 @@ public class DecoratorController {
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE);
}
// ==== Authlib Injector DnD ====
@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.svg.SVGGlyph;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -29,14 +27,15 @@ import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -44,20 +43,14 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Lang;
public class DecoratorSkin extends SkinBase<Decorator> {
private static final PseudoClass TRANSPARENT = PseudoClass.getPseudoClass("transparent");
private static final SVGGlyph minus = Lang.apply(new SVGGlyph(0, "MINUS", "M804.571 420.571v109.714q0 22.857-16 38.857t-38.857 16h-694.857q-22.857 0-38.857-16t-16-38.857v-109.714q0-22.857 16-38.857t38.857-16h694.857q22.857 0 38.857 16t16 38.857z", Color.WHITE),
glyph -> { glyph.setSize(12, 2); glyph.setTranslateY(4); });
private final StackPane root, parent;
private final StackPane titleContainer;
private final Stage primaryStage;
private final TransitionPane navBarPane;
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean titleBarTransparent = true;
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
/**
* Constructor for all SkinBase instances.
@@ -69,14 +62,13 @@ public class DecoratorSkin extends SkinBase<Decorator> {
primaryStage = control.getPrimaryStage();
minus.fillProperty().bind(Theme.foregroundFillBinding());
Decorator skinnable = getSkinnable();
root = new StackPane();
root.getStyleClass().add("window");
StackPane shadowContainer = new StackPane();
shadowContainer.getStyleClass().add("body");
shadowContainer.setEffect(new DropShadow(BlurType.ONE_PASS_BOX, Color.rgb(0, 0, 0, 0.4), 10, 0.3, 0.0, 0.0));
parent = new StackPane();
Rectangle clip = new Rectangle();
@@ -174,11 +166,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (s.isAnimate()) {
AnimationProducer animation;
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) {
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT.getAnimationProducer();
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT;
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) {
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT.getAnimationProducer();
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT;
} else {
animation = ContainerAnimations.FADE.getAnimationProducer();
animation = ContainerAnimations.FADE;
}
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
navBarPane.setContent(node, animation);
@@ -197,19 +189,20 @@ public class DecoratorSkin extends SkinBase<Decorator> {
buttonsContainer.setMaxHeight(40);
{
JFXButton btnHelp = new JFXButton();
btnHelp.setGraphic(SVG.HELP_CIRCLE_OUTLINE.createIcon(Theme.foregroundFillBinding(), -1, -1));
btnHelp.setFocusTraversable(false);
btnHelp.setGraphic(SVG.HELP.createIcon(Theme.foregroundFillBinding(), -1));
btnHelp.getStyleClass().add("jfx-decorator-button");
btnHelp.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
btnHelp.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
JFXButton btnMin = new JFXButton();
StackPane pane = new StackPane(minus);
pane.setAlignment(Pos.CENTER);
btnMin.setGraphic(pane);
btnMin.setFocusTraversable(false);
btnMin.setGraphic(SVG.MINIMIZE.createIcon(Theme.foregroundFillBinding(), -1));
btnMin.getStyleClass().add("jfx-decorator-button");
btnMin.setOnAction(e -> skinnable.minimize());
JFXButton btnClose = new JFXButton();
btnClose.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1, -1));
btnClose.setFocusTraversable(false);
btnClose.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1));
btnClose.getStyleClass().add("jfx-decorator-button");
btnClose.setOnAction(e -> skinnable.close());
@@ -236,7 +229,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canBack) {
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.BACK.createIcon(Theme.foregroundFillBinding(), -1, -1));
backNavButton.setFocusTraversable(false);
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Theme.foregroundFillBinding(), -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().set(Theme.whiteFill());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
@@ -247,14 +241,15 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canClose) {
JFXButton closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.setFocusTraversable(false);
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1));
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().set(Theme.whiteFill());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.HOME.createIcon(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.setGraphic(SVG.HOME.createIcon(Theme.foregroundFillBinding(), -1));
else
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1));
navLeft.getChildren().add(closeNavButton);
}
@@ -270,7 +265,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
titleLabel.getStyleClass().add("jfx-decorator-title");
if (titleNode == null) {
titleLabel.maxWidthProperty().bind(Bindings.createDoubleBinding(
() -> skinnable.getWidth() - 100 - navLeft.getWidth(),
() -> skinnable.getWidth() - 150 - navLeft.getWidth(),
skinnable.widthProperty(), navLeft.widthProperty()));
} else {
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
@@ -293,7 +288,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
HBox navRight = new HBox();
navRight.setAlignment(Pos.CENTER_RIGHT);
JFXButton refreshNavButton = new JFXButton();
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Theme.foregroundFillBinding(), -1, -1));
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Theme.foregroundFillBinding(), -1));
refreshNavButton.getStyleClass().add("jfx-decorator-button");
refreshNavButton.ripplerFillProperty().set(Theme.whiteFill());
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
@@ -310,13 +305,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
return navBar;
}
private void updateInitMouseValues(MouseEvent mouseEvent) {
initX = mouseEvent.getScreenX();
initY = mouseEvent.getScreenY();
xOffset = mouseEvent.getSceneX();
yOffset = mouseEvent.getSceneY();
}
private boolean isRightEdge(double x, double y, Bounds boundsInParent) {
return x < root.getWidth() && x >= root.getWidth() - root.snappedLeftInset();
}
@@ -333,146 +321,139 @@ public class DecoratorSkin extends SkinBase<Decorator> {
return x >= 0 && x <= root.snappedLeftInset();
}
private boolean setStageWidth(double width) {
if (width >= primaryStage.getMinWidth() && width >= titleContainer.getMinWidth()) {
primaryStage.setWidth(width);
initX = newX;
return true;
} else {
if (width >= primaryStage.getMinWidth() && width <= titleContainer.getMinWidth())
primaryStage.setWidth(titleContainer.getMinWidth());
private void resizeStage(double newWidth, double newHeight) {
if (newWidth < 0)
newWidth = primaryStage.getWidth();
if (newWidth < primaryStage.getMinWidth())
newWidth = primaryStage.getMinWidth();
if (newWidth < titleContainer.getMinWidth())
newWidth = titleContainer.getMinWidth();
return false;
}
if (newHeight < 0)
newHeight = primaryStage.getHeight();
if (newHeight < primaryStage.getMinHeight())
newHeight = primaryStage.getMinHeight();
if (newHeight < titleContainer.getMinHeight())
newHeight = titleContainer.getMinHeight();
// Width and height must be set simultaneously to avoid JDK-8344372 (https://github.com/openjdk/jfx/pull/1654)
primaryStage.setWidth(newWidth);
primaryStage.setHeight(newHeight);
}
private boolean setStageHeight(double height) {
if (height >= primaryStage.getMinHeight() && height >= titleContainer.getHeight()) {
primaryStage.setHeight(height);
initY = newY;
return true;
} else {
if (height >= primaryStage.getMinHeight() && height <= titleContainer.getHeight())
primaryStage.setHeight(titleContainer.getHeight());
return false;
}
}
// ====
protected void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isFullScreen()) {
updateInitMouseValues(mouseEvent);
if (primaryStage.isResizable()) {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = root.getBoundsInParent();
double diagonalSize = root.snappedLeftInset() + 10;
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else {
root.setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else {
root.setCursor(Cursor.N_RESIZE);
}
} else if (this.isBottomEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.S_RESIZE);
}
private void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isFullScreen() && primaryStage.isResizable()) {
double x = mouseEvent.getX(), y = mouseEvent.getY();
Bounds boundsInParent = root.getBoundsInParent();
double diagonalSize = root.snappedLeftInset() + 10;
if (this.isRightEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.DEFAULT);
root.setCursor(Cursor.E_RESIZE);
}
} else if (this.isLeftEdge(x, y, boundsInParent)) {
if (y < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (y > root.getHeight() - diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else {
root.setCursor(Cursor.W_RESIZE);
}
} else if (this.isTopEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.NW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.NE_RESIZE);
} else {
root.setCursor(Cursor.N_RESIZE);
}
} else if (this.isBottomEdge(x, y, boundsInParent)) {
if (x < diagonalSize) {
root.setCursor(Cursor.SW_RESIZE);
} else if (x > root.getWidth() - diagonalSize) {
root.setCursor(Cursor.SE_RESIZE);
} else {
root.setCursor(Cursor.S_RESIZE);
}
} else {
root.setCursor(Cursor.DEFAULT);
}
} else {
root.setCursor(Cursor.DEFAULT);
}
}
protected void onMouseReleased(MouseEvent mouseEvent) {
private void onMouseReleased(MouseEvent mouseEvent) {
getSkinnable().setDragging(false);
}
protected void onMouseDragged(MouseEvent mouseEvent) {
getSkinnable().setDragging(true);
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress()) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltaX = this.newX - this.initX;
double deltaY = this.newY - this.initY;
Cursor cursor = root.getCursor();
if (Cursor.E_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.NE_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
private void onMouseDragged(MouseEvent mouseEvent) {
if (!getSkinnable().isDragging()) {
getSkinnable().setDragging(true);
mouseInitX = mouseEvent.getScreenX();
mouseInitY = mouseEvent.getScreenY();
stageInitX = primaryStage.getX();
stageInitY = primaryStage.getY();
stageInitWidth = primaryStage.getWidth();
stageInitHeight = primaryStage.getHeight();
}
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
mouseEvent.consume();
} else if (Cursor.SE_RESIZE == cursor) {
this.setStageWidth(this.primaryStage.getWidth() + deltaX);
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.S_RESIZE == cursor) {
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.W_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
if (primaryStage.isFullScreen() || !mouseEvent.isPrimaryButtonDown() || mouseEvent.isStillSincePress())
return;
mouseEvent.consume();
} else if (Cursor.SW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
double dx = mouseEvent.getScreenX() - mouseInitX;
double dy = mouseEvent.getScreenY() - mouseInitY;
this.setStageHeight(this.primaryStage.getHeight() + deltaY);
mouseEvent.consume();
} else if (Cursor.NW_RESIZE == cursor) {
if (this.setStageWidth(this.primaryStage.getWidth() - deltaX)) {
this.primaryStage.setX(this.primaryStage.getX() + deltaX);
}
Cursor cursor = root.getCursor();
if (getSkinnable().isAllowMove()) {
if (cursor == Cursor.DEFAULT) {
primaryStage.setX(stageInitX + dx);
primaryStage.setY(stageInitY + dy);
mouseEvent.consume();
}
}
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
if (getSkinnable().isResizable()) {
if (cursor == Cursor.E_RESIZE) {
resizeStage(stageInitWidth + dx, -1);
mouseEvent.consume();
mouseEvent.consume();
} else if (Cursor.N_RESIZE == cursor) {
if (this.setStageHeight(this.primaryStage.getHeight() - deltaY)) {
this.primaryStage.setY(this.primaryStage.getY() + deltaY);
}
} else if (cursor == Cursor.S_RESIZE) {
resizeStage(-1, stageInitHeight + dy);
mouseEvent.consume();
mouseEvent.consume();
} else if (getSkinnable().isAllowMove()) {
this.primaryStage.setX(mouseEvent.getScreenX() - this.xOffset);
this.primaryStage.setY(mouseEvent.getScreenY() - this.yOffset);
mouseEvent.consume();
}
} else if (cursor == Cursor.W_RESIZE) {
resizeStage(stageInitWidth - dx, -1);
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
mouseEvent.consume();
} else if (cursor == Cursor.N_RESIZE) {
resizeStage(-1, stageInitHeight - dy);
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
mouseEvent.consume();
} else if (cursor == Cursor.SE_RESIZE) {
resizeStage(stageInitWidth + dx, stageInitHeight + dy);
mouseEvent.consume();
} else if (cursor == Cursor.SW_RESIZE) {
resizeStage(stageInitWidth - dx, stageInitHeight + dy);
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
mouseEvent.consume();
} else if (cursor == Cursor.NW_RESIZE) {
resizeStage(stageInitWidth - dx, stageInitHeight - dy);
primaryStage.setX(stageInitX + stageInitWidth - primaryStage.getWidth());
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
mouseEvent.consume();
} else if (cursor == Cursor.NE_RESIZE) {
resizeStage(stageInitWidth + dx, stageInitHeight - dy);
primaryStage.setY(stageInitY + stageInitHeight - primaryStage.getHeight());
mouseEvent.consume();
}
}
}
@@ -36,7 +36,7 @@ public abstract class DecoratorTabPage extends DecoratorTransitionPage implement
if (newValue.getNode() != null) {
onNavigating(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING));
navigate(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
navigate(newValue.getNode(), ContainerAnimations.FADE);
onNavigated(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED));
}
@@ -74,7 +74,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
@Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
displayer.navigateTo(page, nav);
navigate(page, nav.getAnimation().getAnimationProducer());
navigate(page, nav.getAnimation());
String prefix = category == null ? "" : category + " - ";
@@ -0,0 +1,152 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.ui.Controllers;
import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class AbstractInstallersPage extends Control implements WizardPage {
protected final WizardController controller;
protected InstallerItem.InstallerItemGroup group;
protected JFXTextField txtName = new JFXTextField();
protected BooleanProperty installable = new SimpleBooleanProperty();
public AbstractInstallersPage(WizardController controller, String gameVersion, DownloadProvider downloadProvider) {
this.controller = controller;
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
library.setOnInstall(() -> {
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
}
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
});
library.setOnRemove(() -> {
controller.getSettings().remove(libraryId);
reload();
});
}
}
protected InstallerItem.Style getInstallerItemStyle() {
return InstallerItem.Style.CARD;
}
@Override
public abstract String getTitle();
protected abstract void reload();
@Override
public void onNavigate(Map<String, Object> settings) {
reload();
}
@Override
public abstract void cleanup(Map<String, Object> settings);
protected abstract void onInstall();
@Override
protected Skin<?> createDefaultSkin() {
return new InstallersPageSkin(this);
}
protected static class InstallersPageSkin extends SkinBase<AbstractInstallersPage> {
/**
* Constructor for all SkinBase instances.
*
* @param control The control for which this Skin should attach to.
*/
protected InstallersPageSkin(AbstractInstallersPage control) {
super(control);
BorderPane root = new BorderPane();
root.setPadding(new Insets(16));
{
HBox versionNamePane = new HBox(8);
versionNamePane.getStyleClass().add("card-non-transparent");
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
versionNamePane.setAlignment(Pos.CENTER_LEFT);
control.txtName.setMaxWidth(300);
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
root.setTop(versionNamePane);
}
{
InstallerItem[] libraries = control.group.getLibraries();
FlowPane libraryPane = new FlowPane(libraries);
libraryPane.setVgap(16);
libraryPane.setHgap(16);
if (libraries.length <= 8) {
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
root.setCenter(libraryPane);
} else {
ScrollPane scrollPane = new ScrollPane(libraryPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
root.setCenter(scrollPane);
}
}
{
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
installButton.disableProperty().bind(control.installable.not());
installButton.setPrefWidth(100);
installButton.setPrefHeight(40);
installButton.setOnAction(e -> control.onInstall());
BorderPane.setAlignment(installButton, Pos.CENTER_RIGHT);
root.setBottom(installButton);
}
getChildren().setAll(root);
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.download;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.download.DownloadProvider;
@@ -36,30 +35,25 @@ import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class AdditionalInstallersPage extends InstallersPage {
class AdditionalInstallersPage extends AbstractInstallersPage {
protected final BooleanProperty compatible = new SimpleBooleanProperty();
protected final GameRepository repository;
protected final String gameVersion;
protected final Version version;
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) {
super(controller, repository, gameVersion, downloadProvider);
super(controller, gameVersion, downloadProvider);
this.gameVersion = gameVersion;
this.version = version;
this.repository = repository;
txtName.getValidators().clear();
txtName.setText(version.getId());
txtName.setEditable(false);
installable.bind(Bindings.createBooleanBinding(
() -> compatible.get() && txtName.validate(),
txtName.textProperty(), compatible));
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
library.removeActionProperty().set(e -> {
library.setOnRemove(() -> {
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
reload();
});
Loaded 100 of 399 files, more files were not shown because too many files have changed in this diff. Show more