Compare commits

...
112 Commits
Author SHA1 Message Date
Glavo af36a0ac2a Bump Gradle to 8.0.1 (#2158) 2023-03-01 23:32:24 +08:00
Glavo 7356335106 添加环境变量设置 (#2157)
* Remove unused methods

* 添加环境变量设置

* update

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

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

* update

* update

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

* update style

* update

* update

* update

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

* Hide feedbackTextFlow when it is not visible

* fix checkstyle

* update I18N

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

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

* update

* update

* update

* update

* update

* update

* update

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

* 更新 RemoteModRepository.java

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

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

* 还是默认“热度”吧

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

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

* 更新 RemoteModRepository.java

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

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

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

* update

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

* Remove MutableObject

* VersionRange

* Replace Range with VersionRange

* fix checkstyle

* Update

* update

* update

* fix checkstyle

* fix checkstyle

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

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

* Fix

---------

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

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

* Edit

* Fix

* add

* 删除 CrashReportAnalyzerTest.java

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

* Fix

* Fix

* Revert "删除 CrashReportAnalyzerTest.java"

This reverts commit 4fe873cfcb.

* 换回 LibericaJDK

* Fix

尴尬(

* 更新 GameCrashWindow.java

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

* 更新 I18N_zh_CN.properties

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

* Fix

* Update CrashReportAnalyzer.java

* Fix

- 删除重复检查项

* 添加日志检查

https://paste.ee/p/vv7DD

* fix

* add

怎么tm还来一个

* 更新 CrashReportAnalyzerTest.java

* 调整

* fix

* 更新 I18N_zh_CN.properties

* 更新 I18N_zh_CN.properties

* 添加重复安装 Optifine检查

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

* 添加重复安装 Forge 的检查

test

更新 CrashReportAnalyzer.java

fix

* 完善 OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE

完善 OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE

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

* fix

* 调整 FABRIC_WARNINGS

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

* fix test

* update

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

* 更新

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

* update

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

* Optimize layout

---------

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

* Add GitHub SHA to manifest

* Add unofficial to build channel

* Optimize IntegrityChecker

* update hint

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

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

* newRaisedButton

* update

* update

* update

* update

* update

* toLowerCase and toUpperCase

* update

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

* Add a new JavaVersionConstraint rule

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

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

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

* Check JavaFX Version

* javadoc

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

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

* Add tests

* update

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

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

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

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

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

* update

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

* Cleanup StringUtils

* Cleanup CommandBuilder

* Cleanup CommandBuilder

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

* update

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

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

* update

* update

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

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

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

* Update bug-report.yml

* Update config.yml

* Update bug-report.yml

* Update bug-report.yml

* Update bug-report.yml

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

* Update logo

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

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

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

又写错别字了。。

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

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

* fix

* Update I18N_zh_CN.properties

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

* close #1377: Add conversion button

* fix YggdrasilAccount::getIdentifier()

* Check whether the account exists before moving

* Add server url to selected account

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

* fix

* fix

* Cleanup Code

* Use blue.bss

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

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

* 完善

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

* Remove authlib-injector-server-add.fxml

* Remove profile.fxml

* Remove task-dialog.fxml

* Flatten AddAuthlibInjectorServerPane

* Flatten MessageDialogPane

* Remove unused fxml files

* Remove modpack.fxml

* Remove modpack-source.fxml

* Remove selection.fxml

* Remove type.fxml

* Remove versions.fxml

* Remove input-dialog.fxml

* Remove fxml

* Cleanup code

* Cleanup code

* Cleanup Code

* Cleanup Code
2022-12-10 22:36:14 +08:00
Glavo 2bcf9ee39e Optimize automatic memory allocation (#1914) 2022-12-10 22:32:09 +08:00
huanghongxun 7a90035278 feat(about): add link to qq channel. 2022-12-07 15:52:55 +08:00
245 changed files with 7785 additions and 4433 deletions

No files matched your search

+13 -8
View File
@@ -1,6 +1,8 @@
name: Bug Feedback
description:
反馈一个 Bug。为了提高反馈效率,请优先在 KOOK 或者或 Discord 频道中反馈。若确认是 HMCL 问题,请发布 issue。
反馈一个错误。
为了提高反馈效率,请优先在 KOOK、赞助频道或者 Discord 频道中反馈。若确认是 HMCL 问题,请发布 issue。
labels: bug
body:
- type: markdown
attributes:
@@ -8,8 +10,9 @@ body:
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [KOOK 频道](https://kook.top/Kx7n3t)
或 [Discord 频道](https://discord.gg/jVvC7HfM6U)中反馈信息,**而不在 GitHub Issues 中反馈问题**。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在
[KOOK 频道](https://kook.top/Kx7n3t) 或 [Discord 频道](https://discord.gg/jVvC7HfM6U)中反馈信息,
**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://hmcl.huangyuhui.net/api/redirect/sponsor) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
- type: input
@@ -28,6 +31,13 @@ body:
placeholder: e.g. Windows 11
validations:
required: true
- type: textarea
id: bug-report
attributes:
label: 问题描述 Problem Description
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。 Please describe the problem in as much detail as possible, especially how you encountered this bug.
validations:
required: true
- type: input
id: java-version
attributes:
@@ -61,9 +71,4 @@ body:
attributes:
label: 启动器日志文件 Launcher Log File
description: 遇到问题后,请在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。 After encountering a problem, please click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
- type: textarea
id: bug-report
attributes:
label: 问题描述 Problem Description
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。 Please describe the problem in as much detail as possible, especially how you encountered this bug.
-14
View File
@@ -1,14 +0,0 @@
language: java
jdk:
- oraclejdk8
branches:
only:
- master
before_install:
- chmod +x gradlew
script: "bash ./gradlew clean build --stacktrace"
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- "$HOME/.gradle/caches/"
+20 -94
View File
@@ -1,48 +1,31 @@
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipFile
import java.util.zip.GZIPOutputStream
import java.io.ByteArrayOutputStream
import java.io.ByteArrayInputStream
import java.net.URI
import org.glavo.pack200.Pack200
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
buildscript {
repositories {
gradlePluginPortal()
maven(url = "https://jitpack.io")
}
dependencies {
classpath("org.tukaani:xz:1.8")
classpath("org.glavo:pack200:0.3.0")
}
}
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun" && System.getenv("GITHUB_BASE_REF").isNullOrEmpty())
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7)
val prefix = if (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun") "dev" else "unofficial"
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
val versionType = System.getenv("VERSION_TYPE") ?: "nightly"
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") ?: ""
@@ -92,21 +75,6 @@ fun attachSignature(jar: File) {
}
}
val packer = Pack200.newPacker().apply {
properties()["pack.effort"] = "9"
}
val unpacker = Pack200.newUnpacker()
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
// .class files remain the same.
fun repack(file: File) {
val packed = ByteArrayOutputStream()
JarFile(file).use { packer.pack(it, packed) }
JarOutputStream(file.outputStream()).use { unpacker.unpack(ByteArrayInputStream(packed.toByteArray()), it) }
}
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
@@ -142,7 +110,7 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2021 huangyuhui.",
"Created-By" to "Copyright(c) 2013-2023 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
@@ -154,29 +122,25 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"javafx.graphics/javafx.css",
"javafx.base/com.sun.javafx.runtime",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"javafx.controls/com.sun.javafx.scene.control",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.graphics/com.sun.javafx.stage"
).joinToString(" "),
"Add-Exports" to listOf(
"java.base/jdk.internal.loader",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.base/com.sun.javafx.event"
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin"
).joinToString(" ")
)
System.getenv("GITHUB_SHA")?.also {
attributes("GitHub-SHA" to it)
}
}
doLast {
repack(jarPath) // see repack()
attachSignature(jarPath)
createChecksum(jarPath)
}
@@ -223,46 +187,8 @@ tasks.processResources {
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
}
val packFile = File(jarPath.parentFile, jarPath.nameWithoutExtension + ".pack")
val makePack = tasks.create("makePack") {
dependsOn(tasks.jar)
doLast {
packFile.outputStream().use { out ->
JarFile(jarPath).use { jarFile -> packer.pack(jarFile, out) }
}
createChecksum(packFile)
}
}
val makePackXz = tasks.create("makePackXz") {
dependsOn(makePack)
val packXz = File(packFile.parentFile, packFile.name + ".xz")
doLast {
// Our CI server does not have enough memory space to compress file at highest level.
XZOutputStream(packXz.outputStream(), LZMA2Options(5))
.use { it.write(packFile.readBytes()) }
createChecksum(packXz)
}
}
val makePackGz = tasks.create("makePackGz") {
dependsOn(makePack)
val packGz = File(packFile.parentFile, packFile.name + ".gz")
doLast {
GZIPOutputStream(packGz.outputStream()).use { it.write(packFile.readBytes()) }
createChecksum(packGz)
}
}
val makeExecutables = tasks.create("makeExecutables") {
dependsOn(makePack)
dependsOn(tasks.jar)
doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
@@ -270,7 +196,7 @@ val makeExecutables = tasks.create("makeExecutables") {
}
tasks.build {
dependsOn(makePackXz, makePackGz, makeExecutables)
dependsOn(makeExecutables)
}
tasks.create<JavaExec>("run") {
File renamed without changes.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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

After

Width:  |  Height:  |  Size: 2.0 KiB

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

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

@@ -16,8 +16,8 @@ import org.jetbrains.annotations.Nullable;
public class SkinCanvas extends Group {
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/skin/alex.png"));
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/skin/steve.png"));
public static final Image ALEX = new Image("/assets/img/skin/alex.png");
public static final Image STEVE = new Image("/assets/img/skin/steve.png");
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
@@ -4,8 +4,6 @@ import javafx.scene.shape.Mesh;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import org.apache.commons.lang3.ArrayUtils;
public class SkinCube extends MeshView {
public static class Model extends TriangleMesh {
@@ -77,13 +75,23 @@ public class SkinCube extends MeshView {
};
int[] copy = faces.clone();
ArrayUtils.reverse(copy);
for (int i = 0, mid = copy.length >> 1, j = copy.length - 1; i < mid; i++, j--) {
int tmp = copy[i];
copy[i] = copy[j];
copy[j] = tmp;
}
for (int i = 0; i < copy.length; i += 2) {
int tmp = copy[i];
copy[i] = copy[i + 1];
copy[i + 1] = tmp;
}
return ArrayUtils.addAll(faces, copy);
int[] result = new int[faces.length + copy.length];
System.arraycopy(faces, 0, result, 0, faces.length);
System.arraycopy(copy, 0, result, faces.length, copy.length);
return result;
}
}
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.CookieHandler;
@@ -64,6 +65,14 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
try {
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to get prism pipeline", e);
}
try {
try {
ConfigHolder.init();
@@ -71,45 +80,12 @@ public final class Launcher extends Application {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", e);
try {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
String owner = Files.getOwner(ConfigHolder.configLocation()).getName();
String userName = System.getProperty("user.name");
if (!Files.isWritable(ConfigHolder.configLocation())
&& !userName.equals("root")
&& !userName.equals(owner)) {
ArrayList<String> files = new ArrayList<>();
{
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_DIRECTORY))
files.add(Metadata.HMCL_DIRECTORY.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
}
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
ButtonType res = new Alert(Alert.AlertType.ERROR, i18n("fatal.config_loading_failure.unix", owner, command), copyAndExit, ButtonType.CLOSE)
.showAndWait()
.orElse(null);
if (res == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
System.exit(1);
}
}
} catch (IOException ioe) {
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
}
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize().toString()));
checkConfigInTempDir();
checkConfigOwner();
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
}
checkConfigInTempDir();
if (ConfigHolder.isOwnerChanged()) {
ButtonType res = new Alert(Alert.AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO)
.showAndWait()
@@ -118,7 +94,7 @@ public final class Launcher extends Application {
return;
}
if (Metadata.HMCL_DIRECTORY.toAbsolutePath().toString().indexOf('=') >= 0) {
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
@@ -138,6 +114,92 @@ public final class Launcher extends Application {
}
}
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
String tmpdir = System.getProperty("java.io.tmpdir");
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
return true;
String[] tempFolderNames = {"Temp", "Cache", "Caches"};
for (String name : tempFolderNames) {
if (configPath.contains(File.separator + name + File.separator))
return true;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
return configPath.contains("\\Temporary Internet Files\\")
|| configPath.contains("\\INetCache\\")
|| configPath.contains("\\$Recycle.Bin\\")
|| configPath.contains("\\recycler\\");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
return configPath.startsWith("/tmp/")
|| configPath.startsWith("/var/tmp/")
|| configPath.startsWith("/var/cache/")
|| configPath.startsWith("/dev/shm/")
|| configPath.contains("/Trash/");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
return configPath.startsWith("/var/folders/")
|| configPath.startsWith("/private/var/folders/")
|| configPath.startsWith("/tmp/")
|| configPath.startsWith("/private/tmp/")
|| configPath.startsWith("/var/tmp/")
|| configPath.startsWith("/private/var/tmp/")
|| configPath.contains("/.Trash/");
} else {
return false;
}
}
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()) {
ButtonType res = new Alert(Alert.AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO)
.showAndWait()
.orElse(null);
if (res == ButtonType.NO) {
System.exit(0);
}
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
String owner;
try {
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
} catch (IOException ioe) {
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
return;
}
if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_DIRECTORY))
files.add(Metadata.HMCL_DIRECTORY.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
ButtonType res = new Alert(Alert.AlertType.ERROR, i18n("fatal.config_loading_failure.unix", owner, command), copyAndExit, ButtonType.CLOSE)
.showAndWait()
.orElse(null);
if (res == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
System.exit(1);
}
@Override
public void stop() throws Exception {
super.stop();
@@ -164,8 +226,11 @@ public final class Launcher extends Application {
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("HMCL Jar Path: " + JarUtils.thisJar().map(it -> it.toAbsolutePath().toString()).orElse("Not Found"));
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
.filter(bean -> bean.getName().equals("Metaspace"))
.findAny()
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
.orElse("Unknown"));
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -56,10 +56,8 @@ public final class Main {
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
checkDirectoryPath();
@@ -17,10 +17,12 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Stores metadata about this application.
@@ -36,16 +38,35 @@ public final class Metadata {
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://github.com/huanghongxun/HMCL/issues";
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
public static final String PUBLISH_URL = "https://www.mcbbs.net/thread-142335-1-1.html";
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.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 MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static final Path HMCL_DIRECTORY;
static {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
} else {
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
}
} else {
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
}
public static boolean isStable() {
return "stable".equals(BUILD_CHANNEL);
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
@@ -47,11 +48,13 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -172,17 +175,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // HMCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
@@ -336,7 +329,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
@@ -344,6 +337,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setEnvironmentVariables(
Lang.mapOf(StringUtils.tokenize(vs.getEnvironmentVariables())
.stream()
.map(it -> {
int idx = it.indexOf('=');
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
})
.collect(Collectors.toList())
)
)
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
@@ -355,7 +358,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setUseSoftwareRenderer(vs.isUseSoftwareRenderer())
.setRenderer(vs.getRenderer())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
@@ -429,7 +432,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
return false;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase()))
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return OperatingSystem.isNameValid(id);
@@ -454,7 +457,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 256 * 1024 * 1024; // Reserve 256MB memory for off-heap memory and HMCL itself
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
@@ -463,7 +466,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
32736L * 1024 * 1024); // Limit the maximum suggested memory to ensure that compressed oops are available
16384L * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
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;
@@ -56,6 +57,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -148,8 +150,9 @@ public final class LauncherHelper {
}
}),
Task.composeAsync(() -> {
if (setting.isUseSoftwareRenderer() && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getSoftwareRendererLoader(javaVersion);
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(javaVersion, renderer);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -158,11 +161,13 @@ public final class LauncherHelper {
return null;
}
String agent = file.getAbsolutePath() + "=" + renderer.name().toLowerCase(Locale.ROOT);
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(file.getAbsolutePath()));
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(file.getAbsolutePath());
javaAgents.add(agent);
return null;
}
} else {
@@ -405,26 +410,26 @@ public final class LauncherHelper {
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
JavaVersionConstraint violatedMandatoryConstraint = null;
JavaVersionConstraint violatedSuggestedConstraint = null;
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, javaVersion)) {
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) {
violatedSuggestedConstraint = constraint;
if (violatedSuggestedConstraints == null)
violatedSuggestedConstraints = new ArrayList<>(1);
violatedSuggestedConstraints.add(constraint);
}
}
}
}
boolean suggested = false;
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(javaVersion);
Runnable breakAction = () -> {
future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
};
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
if (violatedMandatoryConstraint != null) {
if (suggestedJavaVersion != null) {
@@ -447,14 +452,14 @@ public final class LauncherHelper {
}, 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);
}, breakAction);
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);
}, breakAction);
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_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
@@ -473,83 +478,99 @@ public final class LauncherHelper {
}
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64
&& javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.warning"), MessageType.ERROR, continueAction);
suggested = true;
List<String> suggestions = new ArrayList<>(0);
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) {
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.
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), continueAction, breakAction);
suggested = true;
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (!suggested && violatedSuggestedConstraint != null) {
suggested = true;
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case MODDED_JAVA_8:
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case MODDED_JAVA_16:
Controllers.dialog(i18n("launch.advice.forge37_0_60"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case VANILLA_JAVA_8_51:
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case MODLAUNCHER_8:
Controllers.dialog(i18n("launch.advice.modlauncher8"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.warning"), MessageType.WARNING, continueAction);
} else {
continueAction.run();
}
break;
if (violatedSuggestedConstraints != null) {
for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) {
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
suggestions.add(i18n("launch.advice.java.modded_java_7"));
break;
case MODDED_JAVA_8:
// Minecraft>=1.7.10+Forge accepts Java 8
if (javaVersion.getParsedVersion() < 8)
suggestions.add(i18n("launch.advice.newer_java"));
else
suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion));
break;
case MODDED_JAVA_16:
// Minecraft<=1.17.1+Forge[37.0.0,37.0.60) not compatible with Java 17
String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
.map(LibraryAnalyzer.LibraryType.FORGE::patchVersion)
.orElse(null);
if (forgePatchVersion != null && VersionNumber.VERSION_COMPARATOR.compare(forgePatchVersion, "37.0.60") < 0)
suggestions.add(i18n("launch.advice.forge37_0_60"));
else
suggestions.add(i18n("launch.advice.modded_java", 16, gameVersion));
break;
case MODDED_JAVA_17:
suggestions.add(i18n("launch.advice.modded_java", 17, gameVersion));
break;
case VANILLA_JAVA_8_51:
suggestions.add(i18n("launch.advice.java8_51_1_13"));
break;
case MODLAUNCHER_8:
suggestions.add(i18n("launch.advice.modlauncher8"));
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
}
}
}
// Cannot allocate too much memory exceeding free space.
if (!suggested && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, breakAction);
suggested = true;
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.
if (!suggested) {
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, breakAction);
suggested = true;
}
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(VersionNumber.asVersion("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.
if (!suggested) {
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it ->
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, breakAction);
suggested = true;
}
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(VersionNumber.asVersion("1.14.4")) == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (!suggested && !future.isDone()) {
future.complete(javaVersion);
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(javaVersion);
}
} else {
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")));
}
Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction);
}
return Task.fromCompletableFuture(future);
@@ -578,9 +599,7 @@ public final class LauncherHelper {
.addAction(link)
.yesOrNo(() -> {
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
.thenAcceptAsync(downloadedJava -> {
future.complete(downloadedJava);
})
.thenAcceptAsync(future::complete)
.exceptionally(throwable -> {
Throwable resolvedException = resolveException(throwable);
LOG.log(Level.WARNING, "Failed to download java", throwable);
@@ -590,9 +609,7 @@ public final class LauncherHelper {
future.completeExceptionally(new CancellationException());
return null;
});
}, () -> {
future.completeExceptionally(new CancellationException());
}).build());
}, () -> future.completeExceptionally(new CancellationException())).build());
return future;
}
@@ -803,9 +820,12 @@ public final class LauncherHelper {
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl && (!detectWindow || filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal"))) {
lwjgl = true;
finishLaunch();
if (!lwjgl) {
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
lwjgl = true;
finishLaunch();
}
}
}
@@ -37,7 +37,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
@Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
String newSearchFilter;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
if (StringUtils.containsChinese(searchFilter)) {
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
List<String> searchFilters = new ArrayList<>();
@@ -19,17 +19,25 @@ package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class LogExporter {
private LogExporter() {
@@ -57,16 +65,37 @@ public final class LogExporter {
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
Path logsDir = runDirectory.resolve("logs");
if (Files.exists(logsDir.resolve("debug.log"))) {
zipper.putFile(logsDir.resolve("debug.log"), "debug.log");
}
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
if (Files.exists(logsDir.resolve("latest.log"))) {
zipper.putFile(logsDir.resolve("latest.log"), "latest.log");
}
if (Files.exists(logsDir.resolve("fml-client-latest.log"))) {
zipper.putFile(logsDir.resolve("fml-client-latest.log"), "fml-client-latest.log");
}
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
zipper.putTextFile(Logging.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(runDirectory, "hs_err_*.log")) {
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
for (Path file : stream) {
if (Files.isRegularFile(file)) {
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).creationTime();
if (time.toMillis() >= processStartTime) {
String crashLog = Logging.filterForbiddenToken(FileUtils.readText(file));
zipper.putTextFile(crashLog, file.getFileName().toString());
}
}
}
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to find vm crash log", e);
}
for (String id : versions) {
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
if (Files.exists(versionJson)) {
@@ -23,6 +23,7 @@ import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
@@ -30,13 +31,10 @@ import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
@@ -217,23 +215,25 @@ public final class TexturesLoader {
// ==== Avatar ====
public static void drawAvatar(Canvas canvas, Image skin) {
canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
GraphicsContext g = canvas.getGraphicsContext2D();
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
int size = (int) canvas.getWidth();
int scale = (int) skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
GraphicsContext g = canvas.getGraphicsContext2D();
try {
g.setImageSmoothing(false);
drawAvatarFX(g, skin, size, scale, faceOffset);
drawAvatar(g, skin, size, scale, faceOffset);
} catch (NoSuchMethodError ignored) {
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing, fallback to Java 2D
drawAvatarJ2D(g, skin, size, scale, faceOffset);
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
// In order to prevent the blurring caused by bilinear interpolation,
// we use the self-implemented nearest neighbor interpolation to scale
drawAvatarSlow(g, skin, size, scale, faceOffset);
}
}
private static void drawAvatarFX(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
g.drawImage(skin,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
@@ -242,27 +242,33 @@ public final class TexturesLoader {
0, 0, size, size);
}
private static void drawAvatarJ2D(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
BufferedImage bi = FXUtils.fromFXImage(skin);
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
PixelReader reader = skin.getPixelReader();
PixelWriter writer = g.getPixelWriter();
drawImage(writer, reader,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
drawImage(writer, reader,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = avatar.createGraphics();
/*
* Scale and draw image using nearest-neighbor interpolation
*/
private static void drawImage(PixelWriter writer, PixelReader reader,
int sx, int sy, int sw, int sh,
int dx, int dy, int dw, int dh) {
double xScale = ((double) sw) / dw;
double yScale = ((double) sh) / dh;
g2d.drawImage(bi,
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
g2d.drawImage(bi,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
for (int xOffset = 0; xOffset < dw; xOffset++) {
for (int yOffset = 0; yOffset < dh; yOffset++) {
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
g2d.dispose();
PixelWriter pw = g.getPixelWriter();
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
pw.setArgb(x, y, avatar.getRGB(x, y));
// Draw only non-transparent pixels
if ((color >>> 24) != 0)
writer.setArgb(dx + xOffset, dy + yOffset, color);
}
}
}
@@ -17,11 +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.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.*;
@@ -36,14 +37,17 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
@@ -81,7 +85,6 @@ public final class Accounts {
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====
@@ -130,59 +133,20 @@ public final class Accounts {
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
private static final String GLOBAL_PREFIX = "$GLOBAL:";
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
updateAccountStorages();
}
};
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
@@ -192,7 +156,67 @@ public final class Accounts {
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
ArrayList<Map<Object, Object>> global = new ArrayList<>();
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
for (Account account : accounts) {
Map<Object, Object> storage = getAccountStorage(account);
if (account.isPortable())
portable.add(storage);
else
global.add(storage);
}
if (!global.equals(globalAccountStorages))
globalAccountStorages.setAll(global);
if (!portable.equals(config().getAccountStorages()))
config().getAccountStorages().setAll(portable);
}
@SuppressWarnings("unchecked")
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_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()));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load global accounts", e);
}
}
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
LOG.info("Saving global accounts");
synchronized (globalAccountsFile) {
try {
synchronized (globalAccountsFile) {
FileUtils.saveSafely(globalAccountsFile, json);
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save global accounts", e);
}
}
});
globalAccountStorages.addListener(onInvalidating(() ->
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
}
private static Account parseAccount(Map<Object, Object> storage) {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return null;
}
try {
return factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return null;
}
}
/**
@@ -202,53 +226,112 @@ public final class Accounts {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
if (!config().isAddedLittleSkin()) {
AuthlibInjectorServer littleSkin = new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/");
if (Boolean.TRUE.equals(storage.get("selected"))) {
selectedAccount.set(account);
if (config().getAuthlibInjectorServers().stream().noneMatch(it -> littleSkin.getUrl().equals(it.getUrl()))) {
config().getAuthlibInjectorServers().add(0, littleSkin);
}
});
config().setAddedLittleSkin(true);
}
loadGlobalAccountStorages();
// load accounts
Account selected = null;
for (Map<Object, Object> storage : config().getAccountStorages()) {
Account account = parseAccount(storage);
if (account != null) {
account.setPortable(true);
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selected = account;
}
}
}
for (Map<Object, Object> storage : globalAccountStorages) {
Account account = parseAccount(storage);
if (account != null) {
accounts.add(account);
}
}
String selectedAccountIdentifier = config().getSelectedAccount();
if (selected == null && selectedAccountIdentifier != null) {
boolean portable = true;
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
portable = false;
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
}
for (Account account : accounts) {
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
if (portable == account.isPortable()) {
selected = account;
break;
} else if (selected == null) {
selected = account;
}
}
}
}
if (selected == null && !accounts.isEmpty()) {
selected = accounts.get(0);
}
selectedAccount.set(selected);
InvalidationListener listener = o -> {
// this method first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account account = selectedAccount.get();
if (accounts.isEmpty()) {
if (account == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
selectedAccount.set(null);
}
} else {
if (accounts.contains(account)) {
// valid
} else {
// the previously selected account is gone
selectedAccount.set(accounts.get(0));
}
}
};
selectedAccount.addListener(listener);
selectedAccount.addListener(onInvalidating(() -> {
Account account = selectedAccount.get();
if (account != null)
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
else
config().setSelectedAccount(null);
}));
accounts.addListener(listener);
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
Account selected = selectedAccount.get();
if (selected != null) {
Account finalSelected = selected;
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
finalSelected.logIn();
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
}
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
triggerAuthlibInjectorUpdateCheck();
}
Schedulers.io().execute(() -> {
try {
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
}
});
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
@@ -267,10 +350,6 @@ public final class Accounts {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
@@ -40,7 +40,6 @@ import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -52,7 +51,7 @@ public final class Config implements Cloneable, Observable {
public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder()
public static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
@@ -120,7 +119,7 @@ public final class Config implements Cloneable, Observable {
private DoubleProperty height = new SimpleDoubleProperty();
@SerializedName("theme")
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>();
@SerializedName("localization")
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@@ -143,11 +142,14 @@ public final class Config implements Cloneable, Observable {
@SerializedName("configurations")
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty();
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("fontFamily")
private StringProperty fontFamily = new SimpleStringProperty(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace");
private StringProperty fontFamily = new SimpleStringProperty();
@SerializedName("fontSize")
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@@ -164,6 +166,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@@ -186,6 +191,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("preferredLoginType")
private StringProperty preferredLoginType = new SimpleStringProperty();
@SerializedName("animationDisabled")
private BooleanProperty animationDisabled = new SimpleBooleanProperty();
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
@@ -480,6 +488,18 @@ public final class Config implements Cloneable, Observable {
return configurations;
}
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
@@ -536,6 +556,18 @@ public final class Config implements Cloneable, Observable {
return authlibInjectorServers;
}
public boolean isAddedLittleSkin() {
return addedLittleSkin.get();
}
public BooleanProperty addedLittleSkinProperty() {
return addedLittleSkin;
}
public void setAddedLittleSkin(boolean addedLittleSkin) {
this.addedLittleSkin.set(addedLittleSkin);
}
public int getConfigVersion() {
return configVersion.get();
}
@@ -572,6 +604,18 @@ public final class Config implements Cloneable, Observable {
return preferredLoginType;
}
public boolean isAnimationDisabled() {
return animationDisabled.get();
}
public BooleanProperty animationDisabledProperty() {
return animationDisabled;
}
public void setAnimationDisabled(boolean animationDisabled) {
this.animationDisabled.set(animationDisabled);
}
public boolean isTitleTransparent() {
return titleTransparent.get();
}
@@ -22,8 +22,8 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
@@ -121,9 +121,8 @@ public final class ConfigHolder {
private static Path locateConfig() {
Path exePath = Paths.get("").toAbsolutePath();
try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath();
if (Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
Path jarPath = JarUtils.thisJar().orElse(null);
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
@@ -208,28 +207,6 @@ public final class ConfigHolder {
// Global Config
private static GlobalConfig loadGlobalConfig() throws IOException {
// Migrate from old directory
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Files.notExists(GLOBAL_CONFIG_PATH)) {
Path oldHome;
String xdgCache = System.getenv("XDG_CACHE_HOME");
if (StringUtils.isNotBlank(xdgCache)) {
oldHome = Paths.get(xdgCache, "hmcl");
} else {
oldHome = Paths.get(System.getProperty("user.home", "."), ".cache", "hmcl");
}
if (Files.exists(oldHome)) {
Path oldConfigPath = oldHome.resolve("config.json");
if (Files.isRegularFile(oldConfigPath)) {
try {
Files.copy(oldConfigPath, GLOBAL_CONFIG_PATH);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to migrate global config", e);
}
}
}
}
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
@@ -108,22 +108,5 @@ final class ConfigUpgrader {
}
});
}
tryCast(rawJson.get("selectedAccount"), String.class)
.ifPresent(selected -> {
deserialized.getAccountStorages().stream()
.filter(storage -> {
Object type = storage.get("type");
if ("offline".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("username"));
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
} else {
return false;
}
})
.findFirst()
.ifPresent(storage -> storage.put("selected", true));
});
}
}
@@ -21,42 +21,23 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Type;
import java.net.Proxy;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public class GlobalConfig implements Cloneable, Observable {
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
.create();
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) {
return null;
}
@@ -93,7 +74,7 @@ public class GlobalConfig implements Cloneable, Observable {
}
public String toJson() {
return CONFIG_GSON.toJson(this);
return Config.CONFIG_GSON.toJson(this);
}
@Override
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -54,6 +55,7 @@ public final class Settings {
Accounts.init();
Profiles.init();
AuthlibInjectorServers.init();
AnimationUtils.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
@@ -23,17 +23,21 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@@ -50,6 +54,39 @@ 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;
}
public static Theme getTheme() {
Theme theme = config().getTheme();
return theme == null ? BLUE : theme;
}
private final Color paint;
private final String color;
private final String name;
@@ -73,7 +110,7 @@ public class Theme {
}
public boolean isLight() {
return Color.web(color).grayscale().getRed() >= 0.5;
return paint.grayscale().getRed() >= 0.5;
}
public Color getForegroundColor() {
@@ -81,33 +118,32 @@ public class Theme {
}
public String[] getStylesheets(String overrideFontFamily) {
Color textFill = getForegroundColor();
String css = "/assets/css/blue.css";
String fontFamily = System.getProperty("hmcl.font.override", overrideFontFamily);
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%base-red%", Integer.toString((int)Math.ceil(paint.getRed() * 256)))
.replace("%base-green%", Integer.toString((int)Math.ceil(paint.getGreen() * 256)))
.replace("%base-blue%", Integer.toString((int)Math.ceil(paint.getBlue() * 256)))
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse("")));
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = "/assets/css/blue.css";
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
Color textFill = getForegroundColor();
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%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse(""));
FileUtils.writeText(temp, themeText, getCssCharset());
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
}
}
return new String[]{
css,
"/assets/css/root.css"
};
return new String[]{css, "/assets/css/root.css"};
}
public static Theme custom(String color) {
@@ -119,50 +155,69 @@ public class Theme {
public static Optional<Theme> getTheme(String name) {
if (name == null)
return Optional.empty();
else if (name.equalsIgnoreCase("blue"))
return Optional.of(custom("#5C6BC0"));
else if (name.equalsIgnoreCase("darker_blue"))
return Optional.of(custom("#283593"));
else if (name.equalsIgnoreCase("green"))
return Optional.of(custom("#43A047"));
else if (name.equalsIgnoreCase("orange"))
return Optional.of(custom("#E67E22"));
else if (name.equalsIgnoreCase("purple"))
return Optional.of(custom("#9C27B0"));
else if (name.equalsIgnoreCase("red"))
return Optional.of(custom("#F44336"));
if (name.startsWith("#"))
else if (name.startsWith("#"))
try {
Color.web(name);
return Optional.of(custom(name));
} catch (IllegalArgumentException ignore) {
}
else {
String color = null;
switch (name.toLowerCase(Locale.ROOT)) {
case "blue":
return Optional.of(BLUE);
case "darker_blue":
color = "#283593";
break;
case "green":
color = "#43A047";
break;
case "orange":
color = "#E67E22";
break;
case "purple":
color = "#9C27B0";
break;
case "red":
color = "#F44336";
}
if (color != null)
return Optional.of(new Theme(name, color));
}
return Optional.empty();
}
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase(Locale.ROOT) : null;
}
private static final ObjectBinding<Color> BLACK_FILL = Bindings.createObjectBinding(() -> BLACK);
private static final ObjectBinding<Color> WHITE_FILL = Bindings.createObjectBinding(() -> Color.WHITE);
private static ObjectBinding<Color> FOREGROUND_FILL;
public static ObjectBinding<Color> foregroundFillBinding() {
return BindingMapping.of(config().themeProperty())
.map(Theme::getForegroundColor);
if (FOREGROUND_FILL == null)
FOREGROUND_FILL = Bindings.createObjectBinding(
() -> Theme.getTheme().getForegroundColor(),
config().themeProperty()
);
return FOREGROUND_FILL;
}
public static ObjectBinding<Color> blackFillBinding() {
return Bindings.createObjectBinding(() -> BLACK);
return BLACK_FILL;
}
public static ObjectBinding<Color> whiteFillBinding() {
return Bindings.createObjectBinding(() -> Color.WHITE);
return WHITE_FILL;
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
@Override
public void write(JsonWriter out, Theme value) throws IOException {
out.value(value.getName().toLowerCase());
out.value(value.getName().toLowerCase(Locale.ROOT));
}
@Override
@@ -38,6 +38,7 @@ 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.stream.Collectors;
@@ -340,6 +341,20 @@ public final class VersionSetting implements Cloneable {
minecraftArgsProperty.set(minecraftArgs);
}
private final StringProperty environmentVariablesProperty = new SimpleStringProperty(this, "environmentVariables", "");
public StringProperty environmentVariablesProperty() {
return environmentVariablesProperty;
}
public String getEnvironmentVariables() {
return environmentVariablesProperty.get();
}
public void setEnvironmentVariables(String env) {
environmentVariablesProperty.set(env);
}
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
public BooleanProperty noJVMArgsProperty() {
@@ -482,7 +497,6 @@ public final class VersionSetting implements Cloneable {
widthProperty.set(width);
}
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public IntegerProperty heightProperty() {
@@ -553,18 +567,18 @@ public final class VersionSetting implements Cloneable {
processPriorityProperty.set(processPriority);
}
private final BooleanProperty useSoftwareRenderer = new SimpleBooleanProperty(this, "softwareRenderer", false);
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
public boolean isUseSoftwareRenderer() {
return useSoftwareRenderer.get();
public Renderer getRenderer() {
return rendererProperty.get();
}
public BooleanProperty useSoftwareRendererProperty() {
return useSoftwareRenderer;
public ObjectProperty<Renderer> rendererProperty() {
return rendererProperty;
}
public void setUseSoftwareRenderer(boolean useSoftwareRenderer) {
this.useSoftwareRenderer.set(useSoftwareRenderer);
public void setRenderer(Renderer renderer) {
this.rendererProperty.set(renderer);
}
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
@@ -692,6 +706,7 @@ public final class VersionSetting implements Cloneable {
postExitCommand.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
environmentVariablesProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
@@ -704,7 +719,7 @@ public final class VersionSetting implements Cloneable {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
processPriorityProperty.addListener(listener);
useSoftwareRenderer.addListener(listener);
rendererProperty.addListener(listener);
useNativeGLFW.addListener(listener);
useNativeOpenAL.addListener(listener);
launcherVisibilityProperty.addListener(listener);
@@ -730,6 +745,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setPostExitCommand(getPostExitCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setEnvironmentVariables(getEnvironmentVariables());
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
@@ -742,7 +758,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setProcessPriority(getProcessPriority());
versionSetting.setUseSoftwareRenderer(isUseSoftwareRenderer());
versionSetting.setRenderer(getRenderer());
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
@@ -760,6 +776,7 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("usesGlobal", src.isUsesGlobal());
obj.addProperty("javaArgs", src.getJavaArgs());
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
obj.addProperty("environmentVariables", src.getEnvironmentVariables());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
@@ -781,7 +798,6 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
obj.addProperty("useSoftwareRenderer", src.isUseSoftwareRenderer());
obj.addProperty("useNativeGLFW", src.isUseNativeGLFW());
obj.addProperty("useNativeOpenAL", src.isUseNativeOpenAL());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
@@ -790,6 +806,10 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
return obj;
}
@@ -815,6 +835,7 @@ public final class VersionSetting implements Cloneable {
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
vs.setEnvironmentVariables(Optional.ofNullable(obj.get("environmentVariables")).map(JsonElement::getAsString).orElse(""));
vs.setMaxMemory(maxMemoryN);
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
@@ -836,7 +857,6 @@ public final class VersionSetting implements Cloneable {
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setUseSoftwareRenderer(Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
@@ -844,6 +864,18 @@ 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));
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
}));
return vs;
}
@@ -19,7 +19,10 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Node;
import javafx.scene.Scene;
@@ -35,6 +38,7 @@ import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountListPage;
@@ -60,12 +64,12 @@ import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
private static InvalidationListener stageSizeChangeListener;
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
@@ -88,7 +92,7 @@ public final class Controllers {
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
accountListPage.accountsProperty().bindContent(Accounts.getAccounts());
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
return accountListPage;
});
@@ -141,14 +145,13 @@ public final class Controllers {
}
public static void onApplicationStop() {
stageSizeChangeListener = null;
if (stageHeight != null) {
config().setHeight(stageHeight.get());
stageHeight.unbind();
stageHeight = null;
}
if (stageWidth != null) {
config().setWidth(stageWidth.get());
stageWidth.unbind();
stageWidth = null;
}
}
@@ -158,10 +161,28 @@ public final class Controllers {
Controllers.stage = stage;
stage.setHeight(config().getHeight());
stageHeight.bind(stage.heightProperty());
stage.setWidth(config().getWidth());
stageWidth.bind(stage.widthProperty());
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
DoubleProperty targetProperty = "width".equals(sourceProperty.getName()) ? stageWidth : stageHeight;
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()) {
targetProperty.set(sourceProperty.get());
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
double initHeight = config().getHeight();
double initWidth = config().getWidth();
stage.setHeight(initHeight);
stage.setWidth(initWidth);
stageHeight.set(initHeight);
stageWidth.set(initWidth);
stage.heightProperty().addListener(weakListener);
stage.widthProperty().addListener(weakListener);
stage.setOnCloseRequest(e -> Launcher.stopApplication());
@@ -181,7 +202,7 @@ public final class Controllers {
stage.setMinWidth(800 + 2 + 16); // bg width + border width*2 + shadow width*2
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.FULL_TITLE);
@@ -206,7 +227,7 @@ public final class Controllers {
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
agreementPane.setBody(new Label(i18n("launcher.agreement.hint")));
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
agreementLink.setOnAction(e -> FXUtils.openLink(Metadata.EULA_URL));
agreementLink.setExternalLink(Metadata.EULA_URL);
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
yesButton.getStyleClass().add("dialog-accept");
yesButton.setOnAction(e -> {
@@ -215,9 +236,7 @@ public final class Controllers {
});
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
noButton.getStyleClass().add("dialog-cancel");
noButton.setOnAction(e -> {
System.exit(1);
});
noButton.setOnAction(e -> System.exit(1));
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}
@@ -28,7 +28,6 @@ import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.beans.value.WritableValue;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -47,12 +46,12 @@ import javafx.scene.text.TextFlow;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.task.Schedulers;
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.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
@@ -70,7 +69,6 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
@@ -96,6 +94,8 @@ public final class FXUtils {
private FXUtils() {
}
public static String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
public static void runInFX(Runnable runnable) {
if (Platform.isFxApplicationThread()) {
runnable.run();
@@ -276,18 +276,8 @@ public final class FXUtils {
}
public static void smoothScrolling(ScrollPane scrollPane) {
JFXScrollPane.smoothScrolling(scrollPane);
}
public static void loadFXML(Node node, String absolutePath) {
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
loader.setRoot(node);
loader.setController(node);
try {
loader.load();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (AnimationUtils.isAnimationEnabled())
ScrollUtils.addSmoothScrolling(scrollPane);
}
public static void installFastTooltip(Node node, Tooltip tooltip) {
@@ -635,16 +625,17 @@ public final class FXUtils {
* @param comboBox the combo box being bound with {@code property}.
* @param property the property being bound with {@code combo box}.
* @see #unbindEnum(JFXComboBox)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
public static <T extends Enum<T>> void bindEnum(JFXComboBox<T> comboBox, Property<T> property) {
unbindEnum(comboBox);
@SuppressWarnings("rawtypes")
ChangeListener<Number> listener = (a, b, newValue) ->
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
comboBox.getSelectionModel().select(property.getValue().ordinal());
T currentValue = property.getValue();
@SuppressWarnings("unchecked")
T[] enumConstants = (T[]) currentValue.getClass().getEnumConstants();
ChangeListener<Number> listener = (a, b, newValue) -> property.setValue(enumConstants[newValue.intValue()]);
comboBox.getSelectionModel().select(currentValue.ordinal());
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
}
@@ -655,15 +646,13 @@ public final class FXUtils {
*
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
* @see #bindEnum(JFXComboBox, Property)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
*/
@SuppressWarnings("unchecked")
@Deprecated
public static void unbindEnum(JFXComboBox<?> comboBox) {
@SuppressWarnings("rawtypes")
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
public static void unbindEnum(JFXComboBox<? extends Enum<?>> comboBox) {
@SuppressWarnings("unchecked")
ChangeListener<Number> listener = (ChangeListener<Number>) comboBox.getProperties().remove("FXUtils.bindEnum.listener");
if (listener != null)
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
/**
@@ -682,6 +671,13 @@ public final class FXUtils {
}
}
public static JFXButton newRaisedButton(String text) {
JFXButton button = new JFXButton(text);
button.getStyleClass().add("jfx-button-raised");
button.setButtonType(JFXButton.ButtonType.RAISED);
return button;
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -726,16 +722,16 @@ public final class FXUtils {
}
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
MutableObject<Object> lastCell = new MutableObject<>();
Holder<Object> lastCell = new Holder<>();
return view -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
if (this == lastCell.value && !isVisible())
return;
lastCell.setValue(this);
lastCell.value = this;
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
@@ -786,38 +782,6 @@ public final class FXUtils {
});
}
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
public static WritableImage toFXImage(BufferedImage image) {
final int iw = image.getWidth();
final int ih = image.getHeight();
WritableImage wr = new WritableImage(iw, ih);
PixelWriter pw = wr.getPixelWriter();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
}
}
return wr;
}
public static BufferedImage fromFXImage(Image image) {
final int iw = (int) image.getWidth();
final int ih = (int) image.getHeight();
PixelReader pr = image.getPixelReader();
BufferedImage bufferedImage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
bufferedImage.setRGB(x, y, pr.getArgb(x, y));
}
}
return bufferedImage;
}
public static void copyText(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text);
@@ -28,6 +28,7 @@ import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -38,6 +39,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
@@ -107,7 +109,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(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("game.crash.title"));
getIcons().add(newImage("/assets/img/icon.png"));
@@ -346,21 +348,28 @@ public class GameCrashWindow extends Stage {
Label reasonTitle = new Label(i18n("game.crash.reason"));
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
reasonPane.setFitToWidth(true);
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonTextFlow, feedbackTextFlow));
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
if (visible) {
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane, feedbackTextFlow));
} else {
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane));
}
});
}
HBox toolBar = new HBox();
{
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setButtonType(JFXButton.ButtonType.RAISED);
exportGameCrashInfoButton.getStyleClass().add("jfx-button-raised");
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
JFXButton logButton = new JFXButton(i18n("logwindow.title"));
logButton.setButtonType(JFXButton.ButtonType.RAISED);
logButton.getStyleClass().add("jfx-button-raised");
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnMouseClicked(e -> showLogWindow());
toolBar.setPadding(new Insets(8));
@@ -207,7 +207,7 @@ public class InstallerItem extends Control {
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
if (control.imageUrl != null) {
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
ImageView view = new ImageView(new Image(control.imageUrl));
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
node.getStyleClass().add("installer-item-image");
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -56,7 +55,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
}
VBox vBox = new VBox();
@@ -26,6 +26,7 @@ import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -34,8 +35,11 @@ import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -87,7 +91,7 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
getIcons().add(newImage("/assets/img/icon.png"));
@@ -132,7 +136,6 @@ public final class LogWindow extends Stage {
while (logs.size() > config().getLogLines()) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
// TODO: fix O(n)
impl.listView.getItems().remove(0);
}
}
@@ -160,21 +163,23 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000");
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;
}
if (!flag)
cboLines.getSelectionModel().select(2);
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
});
if (!flag)
cboLines.getSelectionModel().select(0);
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) {
@@ -286,8 +291,10 @@ public final class LogWindow extends Stage {
if (!listView.getItems().isEmpty() && control.autoScroll.get())
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
MutableObject<Object> lastCell = new MutableObject<>();
listView.setStyle("-fx-font-family: " + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
+ "; -fx-font-size: " + config().getFontSize() + "px;");
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
getStyleClass().add("log-window-list-cell");
@@ -306,9 +313,9 @@ public final class LogWindow extends Stage {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
if (this == lastCell.value && !isVisible())
return;
lastCell.setValue(this);
lastCell.value = this;
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
@@ -54,6 +54,8 @@ public final class SVG {
return svg;
}
public static final String ARROW_RIGHT = "M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z";
// default fill: white, width: 24, height 24
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
@@ -273,7 +275,7 @@ public final class SVG {
}
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width,
return createSVGPath(ARROW_RIGHT, fill, width,
height);
}
@@ -0,0 +1,215 @@
// Copy from https://github.com/palexdev/MaterialFX/blob/c8038ce2090f5cddf923a19d79cc601db86a4d17/materialfx/src/main/java/io/github/palexdev/materialfx/utils/ScrollUtils.java
/*
* Copyright (C) 2022 Parisi Alessandro
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
*
* MaterialFX is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MaterialFX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.util.Duration;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
/**
* Utility class for ScrollPanes.
*/
final class ScrollUtils {
public enum ScrollDirection {
UP(-1), RIGHT(-1), DOWN(1), LEFT(1);
final int intDirection;
ScrollDirection(int intDirection) {
this.intDirection = intDirection;
}
public int intDirection() {
return intDirection;
}
}
private ScrollUtils() {
}
/**
* Determines if the given ScrollEvent comes from a trackpad.
* <p></p>
* Although this method works in most cases, it is not very accurate.
* Since in JavaFX there's no way to tell if a ScrollEvent comes from a trackpad or a mouse
* we use this trick: I noticed that a mouse scroll has a delta of 32 (don't know if it changes depending on the device or OS)
* and trackpad scrolls have a way smaller delta. So depending on the scroll direction we check if the delta is lesser than 10
* (trackpad event) or greater(mouse event).
*
* @see ScrollEvent#getDeltaX()
* @see ScrollEvent#getDeltaY()
*/
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
switch (scrollDirection) {
case UP:
case DOWN:
return Math.abs(event.getDeltaY()) < 10;
case LEFT:
case RIGHT:
return Math.abs(event.getDeltaX()) < 10;
default:
return false;
}
}
/**
* Determines the scroll direction of the given ScrollEvent.
* <p></p>
* Although this method works fine, it is not very accurate.
* In JavaFX there's no concept of scroll direction, if you try to scroll with a trackpad
* you'll notice that you can scroll in both directions at the same time, both deltaX and deltaY won't be 0.
* <p></p>
* For this method to work we assume that this behavior is not possible.
* <p></p>
* If deltaY is 0 we return LEFT or RIGHT depending on deltaX (respectively if lesser or greater than 0).
* <p>
* Else we return DOWN or UP depending on deltaY (respectively if lesser or greater than 0).
*
* @see ScrollEvent#getDeltaX()
* @see ScrollEvent#getDeltaY()
*/
public static ScrollDirection determineScrollDirection(ScrollEvent event) {
double deltaX = event.getDeltaX();
double deltaY = event.getDeltaY();
if (deltaY == 0.0) {
return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
} else {
return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
}
}
//================================================================================
// ScrollPanes
//================================================================================
/**
* Adds a smooth scrolling effect to the given scroll pane,
* calls {@link #addSmoothScrolling(ScrollPane, double)} with a
* default speed value of 1.
*/
public static void addSmoothScrolling(ScrollPane scrollPane) {
addSmoothScrolling(scrollPane, 1);
}
/**
* Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
* Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
* with a default trackPadAdjustment of 7.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
addSmoothScrolling(scrollPane, speed, 7);
}
/**
* Adds a smooth scrolling effect to the given scroll pane with the given
* scroll speed and the given trackPadAdjustment.
* <p></p>
* The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
* This is kind of a workaround and it's not perfect, but at least it's way better than before.
* The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
smoothScroll(scrollPane, speed, trackPadAdjustment);
}
private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
final double[] derivatives = new double[frictions.length];
AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
Timeline timeline = new Timeline();
AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
final EventHandler<ScrollEvent> scrollHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
scrollDirection.set(determineScrollDirection(event));
if (isTrackPad(event, scrollDirection.get())) {
atomicSpeed.set(speed / trackPadAdjustment);
} else {
atomicSpeed.set(speed);
}
derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
if (timeline.getStatus() == Status.STOPPED) {
timeline.play();
}
event.consume();
}
};
if (scrollPane.getContent().getParent() != null) {
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
}
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
}
if (newValue != null) {
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
}
});
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
for (int i = 0; i < derivatives.length; i++) {
derivatives[i] *= frictions[i];
}
for (int i = 1; i < derivatives.length; i++) {
derivatives[i] += derivatives[i - 1];
}
double dy = derivatives[derivatives.length - 1];
Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
double size = sizeFunction.apply(scrollPane.getContent().getLayoutBounds());
double value;
switch (scrollDirection.get()) {
case LEFT:
case RIGHT:
value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
scrollPane.hvalueProperty().set(value);
break;
case UP:
case DOWN:
value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
scrollPane.vvalueProperty().set(value);
break;
}
if (Math.abs(dy) < 0.001) {
timeline.stop();
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -69,7 +68,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
root.getContent().add(scrollPane);
}
@@ -26,6 +26,7 @@ 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;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
@@ -42,7 +43,7 @@ public class WebStage extends Stage {
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
getIcons().add(newImage("/assets/img/icon.png"));
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
@@ -25,6 +25,7 @@ import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthenticationException;
@@ -47,9 +48,8 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
@@ -73,13 +73,14 @@ public class AccountListItem extends RadioButton {
setUserData(account);
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
String portableSuffix = account.isPortable() ? ", " + i18n("account.portable") : "";
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
subtitle.bind(Bindings.concat(
loginTypeName, ", ", i18n("account.injector.server"), ": ",
Bindings.createStringBinding(server::getName, server)));
Bindings.createStringBinding(server::getName, server), portableSuffix));
} else {
subtitle.set(loginTypeName);
subtitle.set(loginTypeName + portableSuffix);
}
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
@@ -166,14 +167,14 @@ public class AccountListItem extends RadioButton {
return refreshAsync()
.thenRunAsync(() -> {
BufferedImage skinImg;
try {
skinImg = ImageIO.read(selectedFile);
Image skinImg;
try (FileInputStream input = new FileInputStream(selectedFile)) {
skinImg = new Image(input);
} catch (IOException e) {
throw new InvalidSkinException("Failed to read skin image", e);
}
if (skinImg == null) {
throw new InvalidSkinException("Failed to read skin image");
if (skinImg.isError()) {
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
@@ -28,6 +28,7 @@ import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.TexturesLoader;
@@ -90,6 +91,41 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnMove = new JFXButton();
SpinnerPane spinnerMove = new SpinnerPane();
spinnerMove.getStyleClass().add("small-spinner-pane");
btnMove.setOnMouseClicked(e -> {
Account account = skinnable.getAccount();
Accounts.getAccounts().remove(account);
if (account.isPortable()) {
account.setPortable(false);
if (!Accounts.getAccounts().contains(account))
Accounts.getAccounts().add(account);
} else {
account.setPortable(true);
if (!Accounts.getAccounts().contains(account)) {
int idx = 0;
for (int i = Accounts.getAccounts().size() - 1; i >= 0; i--) {
if (Accounts.getAccounts().get(i).isPortable()) {
idx = i + 1;
break;
}
}
Accounts.getAccounts().add(idx, account);
}
}
});
btnMove.getStyleClass().add("toggle-icon4");
if (skinnable.getAccount().isPortable()) {
btnMove.setGraphic(SVG.earth(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global")));
} else {
btnMove.setGraphic(SVG.export(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable")));
}
spinnerMove.setContent(btnMove);
right.getChildren().add(spinnerMove);
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
@@ -124,14 +123,6 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
AdvancedListItem littleSkinItem = new AdvancedListItem();
littleSkinItem.getStyleClass().add("navigation-drawer-item");
littleSkinItem.setActionButtonVisible(false);
littleSkinItem.setTitle(i18n("account.methods.little_skin"));
littleSkinItem.setLeftGraphic(wrap(SVG::server));
littleSkinItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_LITTLE_SKIN)));
boxMethods.getChildren().add(littleSkinItem);
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
@@ -195,7 +186,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
Bindings.bindContent(list.getChildren(), skinnable.items);
scrollPane.setContent(list);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
setCenter(scrollPane);
}
@@ -20,10 +20,8 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -32,29 +30,28 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
public final class AddAuthlibInjectorServerPane extends TransitionPane implements DialogAware {
@FXML private TransitionPane root;
@FXML private Label lblServerUrl;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@FXML private Label lblServerWarning;
@FXML private JFXTextField txtServerUrl;
@FXML private JFXDialogLayout addServerPane;
@FXML private JFXDialogLayout confirmServerPane;
@FXML private SpinnerPane nextPane;
@FXML private JFXButton btnAddNext;
private final Label lblServerUrl;
private final Label lblServerName;
private final Label lblCreationWarning;
private final Label lblServerWarning;
private final JFXTextField txtServerUrl;
private final JFXDialogLayout addServerPane;
private final JFXDialogLayout confirmServerPane;
private final SpinnerPane nextPane;
private final JFXButton btnAddNext;
private AuthlibInjectorServer serverBeingAdded;
@@ -65,8 +62,92 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
}
public AddAuthlibInjectorServerPane() {
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
addServerPane = new JFXDialogLayout();
addServerPane.setHeading(new Label(i18n("account.injector.add")));
{
txtServerUrl = new JFXTextField();
txtServerUrl.setPromptText(i18n("account.injector.server_url"));
txtServerUrl.setOnAction(e -> onAddNext());
lblCreationWarning = new Label();
lblCreationWarning.setWrapText(true);
HBox actions = new HBox();
{
JFXButton cancel = new JFXButton(i18n("button.cancel"));
cancel.getStyleClass().add("dialog-accept");
cancel.setOnAction(e -> onAddCancel());
nextPane = new SpinnerPane();
nextPane.getStyleClass().add("small-spinner-pane");
btnAddNext = new JFXButton(i18n("wizard.next"));
btnAddNext.getStyleClass().add("dialog-accept");
btnAddNext.setOnAction(e -> onAddNext());
nextPane.setContent(btnAddNext);
actions.getChildren().setAll(cancel, nextPane);
}
addServerPane.setBody(txtServerUrl);
addServerPane.setActions(lblCreationWarning, actions);
}
confirmServerPane = new JFXDialogLayout();
confirmServerPane.setHeading(new Label(i18n("account.injector.add")));
{
GridPane body = new GridPane();
body.setStyle("-fx-padding: 15 0 0 0;");
body.setVgap(15);
body.setHgap(15);
{
body.getColumnConstraints().setAll(
Lang.apply(new ColumnConstraints(), c -> c.setMaxWidth(100)),
new ColumnConstraints()
);
lblServerUrl = new Label();
GridPane.setColumnIndex(lblServerUrl, 1);
GridPane.setRowIndex(lblServerUrl, 0);
lblServerName = new Label();
GridPane.setColumnIndex(lblServerName, 1);
GridPane.setRowIndex(lblServerName, 1);
lblServerWarning = new Label(i18n("account.injector.http"));
lblServerWarning.setStyle("-fx-text-fill: red;");
GridPane.setColumnIndex(lblServerWarning, 0);
GridPane.setRowIndex(lblServerWarning, 2);
GridPane.setColumnSpan(lblServerWarning, 2);
body.getChildren().setAll(
Lang.apply(new Label(i18n("account.injector.server_url")), l -> {
GridPane.setColumnIndex(l, 0);
GridPane.setRowIndex(l, 0);
}),
Lang.apply(new Label(i18n("account.injector.server_name")), l -> {
GridPane.setColumnIndex(l, 0);
GridPane.setRowIndex(l, 1);
}),
lblServerUrl, lblServerName, lblServerWarning
);
}
JFXButton prevButton = new JFXButton(i18n("wizard.prev"));
prevButton.getStyleClass().add("dialog-cancel");
prevButton.setOnAction(e -> onAddPrev());
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> onAddCancel());
JFXButton finishButton = new JFXButton(i18n("wizard.finish"));
finishButton.getStyleClass().add("dialog-accept");
finishButton.setOnAction(e -> onAddFinish());
confirmServerPane.setBody(body);
confirmServerPane.setActions(prevButton, cancelButton, finishButton);
}
this.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
@@ -88,12 +169,10 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
}
}
@FXML
private void onAddCancel() {
fireEvent(new DialogCloseEvent());
}
@FXML
private void onAddNext() {
if (btnAddNext.isDisabled())
return;
@@ -117,7 +196,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
root.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
@@ -126,12 +205,10 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
}
@FXML
private void onAddPrev() {
root.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
this.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
}
@FXML
private void onAddFinish() {
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
config().getAuthlibInjectorServers().add(serverBeingAdded);
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
@@ -91,10 +90,10 @@ public class ClassicAccountLoginDialog extends StackPane {
body.getChildren().add(linkPane);
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
migrationLink.setExternalLink(YggdrasilService.PROFILE_URL);
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
migrationHowLink.setExternalLink(YggdrasilService.MIGRATION_FAQ_URL);
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
}
@@ -58,6 +58,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -289,28 +290,37 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
FlowPane box = new FlowPane();
box.setHgap(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a"));
birthLink.setExternalLink("https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a");
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A"));
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/password/forgot"));
forgotpasswordLink.setExternalLink("https://www.minecraft.net/password/forgot");
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
createProfileLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editprofile"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
createProfileLink.setExternalLink("https://www.minecraft.net/msaprofile/mygames/editprofile");
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(hintPane, box);
if (!IntegrityChecker.isOfficial()) {
HintPane unofficialHint = new HintPane(MessageDialogPane.MessageType.WARNING);
unofficialHint.setText(i18n("unofficial.hint"));
vbox.getChildren().add(unofficialHint);
}
vbox.getChildren().addAll(hintPane, box);
btnAccept.setDisable(false);
} else {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setText(i18n("account.methods.microsoft.snapshot"));
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
vbox.getChildren().setAll(hintPane);
JFXHyperlink officialWebsite = new JFXHyperlink(i18n("account.methods.microsoft.snapshot.website"));
officialWebsite.setExternalLink("https://hmcl.huangyuhui.net");
vbox.getChildren().setAll(hintPane, officialWebsite);
btnAccept.setDisable(true);
}
@@ -389,6 +399,15 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
int rowIndex = 0;
if (!IntegrityChecker.isOfficial() && !(factory instanceof OfflineAccountFactory)) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setSegment(i18n("unofficial.hint"));
GridPane.setColumnSpan(hintPane, 2);
add(hintPane, 0, rowIndex);
rowIndex++;
}
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
this.server = ((BoundAuthlibInjectorAccountFactory) factory).getServer();
@@ -493,13 +512,13 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
GridPane.setColumnSpan(box, 2);
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink("https://aka.ms/MinecraftMigration"));
migrationLink.setExternalLink("https://aka.ms/MinecraftMigration");
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
migrationHowLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/articles/4411173197709"));
migrationHowLink.setExternalLink("https://help.minecraft.net/hc/articles/4411173197709");
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
box.getChildren().setAll(migrationLink, migrationHowLink, purchaseLink);
add(box, 0, rowIndex);
@@ -511,7 +530,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
HBox linkPane = new HBox(purchaseLink);
GridPane.setColumnSpan(linkPane, 2);
add(linkPane, 0, rowIndex);
@@ -48,9 +48,15 @@ public class OAuthAccountLoginDialog extends DialogPane {
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.login.refresh.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
);
} else {
hintPane.setSegment(i18n("account.login.refresh.microsoft.hint"));
hintPane.setSegment(
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
+ i18n("account.methods.microsoft.hint")
);
}
});
hintPane.setOnMouseClicked(e -> {
@@ -141,9 +141,9 @@ public class OfflineAccountSkinPane extends StackPane {
return;
}
canvas.updateSkin(
result.getSkin() != null ? new Image(result.getSkin().getInputStream()) : getDefaultTexture(),
result.getSkin() != null ? result.getSkin().getImage() : getDefaultTexture(),
result.getModel() == TextureModel.ALEX,
result.getCape() != null ? new Image(result.getCape().getInputStream()) : null);
result.getCape() != null ? result.getCape().getImage() : null);
}
}).start();
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
@@ -0,0 +1,23 @@
package org.jackhuang.hmcl.ui.animation;
import org.jackhuang.hmcl.setting.ConfigHolder;
public final class AnimationUtils {
private AnimationUtils() {
}
/**
* Trigger initialization of this class.
* Should be called from {@link org.jackhuang.hmcl.setting.Settings#init()}.
*/
@SuppressWarnings("JavadocReference")
public static void init() {
}
private static final boolean enabled = !ConfigHolder.config().isAnimationDisabled();
public static boolean isAnimationEnabled() {
return enabled;
}
}
@@ -69,18 +69,23 @@ public class TransitionPane extends StackPane implements AnimationHandler {
return;
}
transition.init(this);
if (AnimationUtils.isAnimationEnabled()) {
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 -> {
setMouseTransparent(false);
getChildren().remove(previousNode);
}));
FXUtils.playAnimation(this, "transition_pane", newAnimation);
});
// 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 -> {
setMouseTransparent(false);
getChildren().remove(previousNode);
}));
FXUtils.playAnimation(this, "transition_pane", newAnimation);
});
} else {
setMouseTransparent(false);
getChildren().remove(previousNode);
}
}
private void updateContent(Node newView) {
@@ -38,7 +38,8 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.function.Consumer;
import java.util.List;
import java.util.function.Supplier;
@DefaultProperty("content")
public class ComponentList extends Control {
@@ -47,13 +48,17 @@ public class ComponentList extends Control {
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
private boolean hasSubtitle = false;
public final ObservableList<Node> content = FXCollections.observableArrayList();
private boolean expanded = false;
private Consumer<ComponentList> lazyInitializer;
private Supplier<List<? extends Node>> lazyInitializer;
public ComponentList() {
getStyleClass().add("options-list");
}
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
this();
this.lazyInitializer = lazyInitializer;
}
public String getTitle() {
return title.get();
}
@@ -102,17 +107,12 @@ public class ComponentList extends Control {
return content;
}
public void setLazyInitializer(Consumer<ComponentList> lazyInitializer) {
this.lazyInitializer = lazyInitializer;
}
public void onExpand() {
if (!expanded && lazyInitializer != null) {
lazyInitializer.accept(this);
void doLazyInit() {
if (lazyInitializer != null) {
this.getContent().setAll(lazyInitializer.get());
setNeedsLayout(true);
lazyInitializer = null;
}
expanded = true;
}
@Override
@@ -125,7 +125,7 @@ public class ComponentList extends Control {
return new Skin(this);
}
protected static class Skin extends ControlSkinBase<ComponentList> {
private static final class Skin extends ControlSkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
@@ -133,7 +133,7 @@ public class ComponentList extends Control {
private final ObjectBinding<Node> firstItem;
private final ObjectBinding<Node> lastItem;
protected Skin(ComponentList control) {
Skin(ComponentList control) {
super(control);
list = MappedObservableList.create(control.getContent(), node -> {
@@ -25,6 +25,9 @@ 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;
@@ -35,11 +38,12 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
/**
* @author huangyuhui
*/
class ComponentListCell extends StackPane {
final class ComponentListCell extends StackPane {
private final Node content;
private Animation expandAnimation;
private Rectangle clipRect;
@@ -70,6 +74,7 @@ class ComponentListCell extends StackPane {
}
}
@SuppressWarnings("unchecked")
private void updateLayout() {
if (content instanceof ComponentList) {
ComponentList list = (ComponentList) content;
@@ -134,45 +139,53 @@ class ComponentListCell extends StackPane {
container.getChildren().setAll(content);
groupNode.getChildren().add(container);
Runnable onExpand = () -> {
EventHandler<Event> onExpand = e -> {
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
expandAnimation.stop();
}
setExpanded(!isExpanded());
if (isExpanded()) {
list.onExpand();
boolean expanded = !isExpanded();
setExpanded(expanded);
if (expanded) {
list.doLazyInit();
list.layout();
}
Platform.runLater(() -> {
double newAnimatedHeight = (content.prefHeight(-1) + 8 + 10) * (isExpanded() ? 1 : -1);
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
double newAnimatedHeight = (list.prefHeight(-1) + 8 + 10) * (expanded ? 1 : -1);
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(-1);
double contentHeight = expanded ? newAnimatedHeight : 0;
if (isExpanded()) {
if (expanded) {
updateClip(newHeight);
}
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
));
if (AnimationUtils.isAnimationEnabled()) {
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
));
if (!isExpanded()) {
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
if (!expanded) {
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
}
expandAnimation.play();
} else {
container.setMinHeight(contentHeight);
container.setMaxHeight(contentHeight);
if (!expanded) {
updateClip(newHeight);
}
}
expandAnimation.play();
});
};
headerRippler.setOnMouseClicked(e -> onExpand.run());
expandButton.setOnMouseClicked(e -> onExpand.run());
headerRippler.setOnMouseClicked(onExpand);
expandButton.setOnAction((EventHandler<ActionEvent>) (Object) onExpand);
expandedProperty().addListener((a, b, newValue) ->
expandIcon.setRotate(newValue ? 180 : 0));
expandedProperty().addListener((a, b, newValue) -> expandIcon.setRotate(newValue ? 180 : 0));
getChildren().setAll(groupNode);
} else {
@@ -29,6 +29,7 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
@@ -36,7 +37,7 @@ import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FileSelector extends HBox {
private StringProperty value = new SimpleStringProperty();
private final StringProperty value = new SimpleStringProperty();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
@@ -77,24 +78,30 @@ public class FileSelector extends HBox {
public FileSelector() {
JFXTextField customField = new JFXTextField();
customField.textProperty().bindBidirectional(valueProperty());
FXUtils.bindString(customField, valueProperty());
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
selectButton.setOnMouseClicked(e -> {
selectButton.setOnAction(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
File dir = chooser.showDialog(Controllers.getStage());
if (dir != null)
customField.setText(dir.getAbsolutePath());
if (dir != null) {
String path = dir.getAbsolutePath();
customField.setText(path);
value.setValue(path);
}
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null)
customField.setText(file.getAbsolutePath());
if (file != null) {
String path = file.getAbsolutePath();
customField.setText(path);
value.setValue(path);
}
}
});
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListCell;
import javafx.beans.NamedArg;
import javafx.beans.binding.Bindings;
import javafx.scene.text.Font;
@@ -34,7 +33,7 @@ public class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false;
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
public FontComboBox() {
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new JFXListCell<String>() {
@@ -31,6 +31,8 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.util.Locale;
public class HintPane extends VBox {
private final Text label = new Text();
private final StringProperty text = new SimpleStringProperty(this, "text");
@@ -42,7 +44,7 @@ public class HintPane extends VBox {
public HintPane(MessageDialogPane.MessageType type) {
setFillWidth(true);
getStyleClass().addAll("hint", type.name().toLowerCase());
getStyleClass().addAll("hint", type.name().toLowerCase(Locale.ROOT));
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER_LEFT);
@@ -18,40 +18,45 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import java.util.concurrent.CompletableFuture;
public class InputDialogPane extends StackPane {
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InputDialogPane extends JFXDialogLayout {
private final CompletableFuture<String> future = new CompletableFuture<>();
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private Label title;
@FXML
private VBox vbox;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
private final JFXTextField textField;
private final Label lblCreationWarning;
private final SpinnerPane acceptPane;
public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
title.setText(text);
JFXTextField textField = new JFXTextField();
textField.setText(initialValue);
vbox.getChildren().setAll(textField);
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> {
textField = new JFXTextField(initialValue);
this.setHeading(new HBox(new Label(text)));
this.setBody(new VBox(textField));
lblCreationWarning = new Label();
acceptPane = new SpinnerPane();
acceptPane.getStyleClass().add("small-spinner-pane");
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.getStyleClass().add("dialog-accept");
acceptPane.setContent(acceptButton);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
this.setActions(lblCreationWarning, acceptPane, cancelButton);
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnAction(e -> {
acceptPane.showSpinner();
onResult.call(textField.getText(), () -> {
@@ -1,135 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class JFXCheckBoxTreeTableCell<S, T> extends TreeTableCell<S, T> {
private final CheckBox checkBox;
private boolean showLabel;
private ObservableValue<Boolean> booleanProperty;
public JFXCheckBoxTreeTableCell() {
this(null, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
this(getSelectedProperty, null);
}
public JFXCheckBoxTreeTableCell(
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
final StringConverter<T> converter) {
this.getStyleClass().add("check-box-tree-table-cell");
this.checkBox = new JFXCheckBox();
setGraphic(null);
setSelectedStateCallback(getSelectedProperty);
setConverter(converter);
}
private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
protected void invalidated() {
updateShowLabel();
}
};
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
selectedStateCallback =
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
this, "selectedStateCallback");
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
return selectedStateCallback;
}
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
selectedStateCallbackProperty().set(value);
}
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
return selectedStateCallbackProperty().get();
}
@SuppressWarnings("unchecked")
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty) booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty) booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTreeTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
}
}
private void updateShowLabel() {
this.showLabel = converter != null;
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
}
private ObservableValue<?> getSelectedProperty() {
return getSelectedStateCallback() != null ?
getSelectedStateCallback().call(getIndex()) :
getTableColumn().getCellObservableValue(getIndex());
}
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.scene.control.Hyperlink;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
public class JFXHyperlink extends Hyperlink {
@@ -34,5 +35,9 @@ public class JFXHyperlink extends Hyperlink {
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
}
public void setExternalLink(String externalLink) {
this.setOnAction(e -> FXUtils.openLink(externalLink));
}
}
@@ -22,17 +22,17 @@ import javafx.css.PseudoClass;
import javafx.scene.control.ListCell;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Holder;
public abstract class MDListCell<T> extends ListCell<T> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final StackPane container = new StackPane();
private final StackPane root = new StackPane();
private final MutableObject<Object> lastCell;
private final Holder<Object> lastCell;
public MDListCell(JFXListView<T> listView, MutableObject<Object> lastCell) {
public MDListCell(JFXListView<T> listView, Holder<Object> lastCell) {
this.lastCell = lastCell;
setText(null);
@@ -57,9 +57,9 @@ public abstract class MDListCell<T> extends ListCell<T> {
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (lastCell != null) {
if (this == lastCell.getValue() && !isVisible())
if (this == lastCell.value && !isVisible())
return;
lastCell.setValue(this);
lastCell.value = this;
}
updateControl(item, empty);
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
@@ -45,7 +44,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends StackPane {
public final class MessageDialogPane extends HBox {
public enum MessageType {
ERROR,
@@ -59,35 +58,19 @@ public final class MessageDialogPane extends StackPane {
}
}
@FXML
private StackPane content;
@FXML
private Label graphic;
@FXML
private Label title;
@FXML
private HBox actions;
private final HBox actions;
private @Nullable ButtonBase cancelButton;
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
EnhancedTextFlow textFlow = new EnhancedTextFlow();
textFlow.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction));
if (textFlow.computePrefHeight(400.0) <= 350.0)
content.getChildren().setAll(textFlow);
else {
ScrollPane scrollPane = new ScrollPane(textFlow);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
scrollPane.setFitToWidth(true);
content.getChildren().setAll(scrollPane);
}
if (title != null)
this.title.setText(title);
this.setSpacing(8);
this.getStyleClass().add("jfx-dialog-layout");
Label graphic = new Label();
graphic.setTranslateX(10);
graphic.setTranslateY(10);
graphic.setMinSize(40, 40);
graphic.setMaxSize(40, 40);
switch (type) {
case INFO:
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
@@ -108,6 +91,35 @@ public final class MessageDialogPane extends StackPane {
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
VBox vbox = new VBox();
HBox.setHgrow(vbox, Priority.ALWAYS);
{
StackPane titlePane = new StackPane();
titlePane.getStyleClass().addAll("jfx-layout-heading", "title");
titlePane.getChildren().setAll(new Label(title != null ? title : i18n("message.info")));
StackPane content = new StackPane();
content.getStyleClass().add("jfx-layout-body");
EnhancedTextFlow textFlow = new EnhancedTextFlow(text);
textFlow.setStyle("-fx-font-size: 14px;");
if (textFlow.computePrefHeight(400.0) <= 350.0)
content.getChildren().setAll(textFlow);
else {
ScrollPane scrollPane = new ScrollPane(textFlow);
scrollPane.setPrefHeight(350);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
scrollPane.setFitToWidth(true);
content.getChildren().setAll(scrollPane);
}
actions = new HBox();
actions.getStyleClass().add("jfx-layout-actions");
vbox.getChildren().setAll(titlePane, content, actions);
}
this.getChildren().setAll(graphic, vbox);
onEscPressed(this, () -> {
if (cancelButton != null) {
cancelButton.fire();
@@ -129,6 +141,10 @@ public final class MessageDialogPane extends StackPane {
}
private static final class EnhancedTextFlow extends TextFlow {
EnhancedTextFlow(String text) {
this.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction));
}
@Override
public double computePrefHeight(double width) {
return super.computePrefHeight(width);
@@ -20,9 +20,12 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
@@ -35,23 +38,51 @@ import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends StackPane {
public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor;
private TaskCancellationAction onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
@FXML
private Label lblTitle;
@FXML
private Label lblProgress;
@FXML
private JFXButton btnCancel;
@FXML
private TaskListPane taskListPane;
private final Label lblTitle;
private final Label lblProgress;
private final JFXButton btnCancel;
private final TaskListPane taskListPane;
public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) {
FXUtils.loadFXML(this, "/assets/fxml/task-dialog.fxml");
FXUtils.setLimitWidth(this, 500);
FXUtils.setLimitHeight(this, 300);
VBox center = new VBox();
this.setCenter(center);
center.setPadding(new Insets(16));
{
lblTitle = new Label();
lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;");
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
{
taskListPane = new TaskListPane();
scrollPane.setContent(taskListPane);
}
center.getChildren().setAll(lblTitle, scrollPane);
}
BorderPane bottom = new BorderPane();
this.setBottom(bottom);
bottom.setPadding(new Insets(0, 8, 8, 8));
{
lblProgress = new Label();
bottom.setLeft(lblProgress);
btnCancel = new JFXButton(i18n("button.cancel"));
bottom.setRight(btnCancel);
}
setCancel(cancel);
@@ -37,6 +37,8 @@ import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
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.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
@@ -129,6 +131,10 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
} else if (task instanceof FabricAPIInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
} else if (task instanceof QuiltInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.quilt")));
} else if (task instanceof QuiltAPIInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.quilt-api")));
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
task.setName(i18n("modpack.completion"));
} else if (task instanceof ModpackInstallTask) {
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXSnackbar;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
@@ -35,6 +36,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
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;
@@ -52,8 +54,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Stream;
@@ -88,10 +92,23 @@ public class DecoratorController {
decorator.onBackNavButtonActionProperty().set(e -> back());
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
setupBackground();
setupAuthlibInjectorDnD();
// Setup background
decorator.setContentBackground(getBackground());
changeBackgroundListener = o -> {
final int currentCount = ++this.changeBackgroundCount;
CompletableFuture.supplyAsync(this::getBackground, Schedulers.io())
.thenAcceptAsync(background -> {
if (this.changeBackgroundCount == currentCount)
decorator.setContentBackground(background);
}, Schedulers.javafx());
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener);
config().backgroundImageTypeProperty().addListener(weakListener);
config().backgroundImageProperty().addListener(weakListener);
config().backgroundImageUrlProperty().addListener(weakListener);
// pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> {
if (!(e.getTarget() instanceof Node)) {
@@ -133,37 +150,40 @@ public class DecoratorController {
// ==== Background ====
private void setupBackground() {
decorator.contentBackgroundProperty().bind(
Bindings.createObjectBinding(
() -> {
Image image = null;
if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
.orElse(null);
}
if (config().getBackgroundImageType() == EnumBackgroundImage.NETWORK) {
if (!NetworkUtils.isURL(config().getBackgroundImageUrl())) {
image = loadDefaultBackgroundImage();
} else {
image = new Image(config().getBackgroundImageUrl(), true);
}
} else if (config().getBackgroundImageType() == EnumBackgroundImage.CLASSIC) {
image = newImage("/assets/img/background-classic.jpg");
} else if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
}
if (image == null) {
image = loadDefaultBackgroundImage();
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
},
config().backgroundImageTypeProperty(),
config().backgroundImageProperty(),
config().backgroundImageUrlProperty()));
//FXThread
private int changeBackgroundCount = 0;
@SuppressWarnings("FieldCanBeLocal") // Strong reference
private final InvalidationListener changeBackgroundListener;
private Background getBackground() {
EnumBackgroundImage imageType = config().getBackgroundImageType();
Image image = null;
switch (imageType) {
case CUSTOM:
String backgroundImage = config().getBackgroundImage();
if (backgroundImage != null)
image = tryLoadImage(Paths.get(backgroundImage)).orElse(null);
break;
case NETWORK:
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null && NetworkUtils.isURL(backgroundImageUrl))
image = tryLoadImage(backgroundImageUrl).orElse(null);
break;
case CLASSIC:
image = newImage("/assets/img/background-classic.jpg");
break;
case TRANSLUCENT:
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
}
if (image == null) {
image = loadDefaultBackgroundImage();
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
}
private Image defaultBackground;
private volatile Image defaultBackground;
/**
* Load background image from bg/, background.png, background.jpg, background.gif
@@ -195,9 +215,9 @@ public class DecoratorController {
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(Files::isReadable)
.filter(Files::isReadable)
.filter(it -> {
String ext = getExtension(it).toLowerCase();
String ext = getExtension(it).toLowerCase(Locale.ROOT);
return ext.equals("png") || ext.equals("jpg") || ext.equals("gif");
})
.collect(toList());
@@ -223,9 +243,13 @@ public class DecoratorController {
if (!Files.isReadable(path))
return Optional.empty();
return tryLoadImage(path.toAbsolutePath().toUri().toString());
}
private Optional<Image> tryLoadImage(String url) {
Image img;
try {
img = new Image(path.toAbsolutePath().toUri().toString());
img = new Image(url);
} catch (IllegalArgumentException e) {
LOG.log(WARNING, "Couldn't load background image", e);
return Optional.empty();
@@ -261,7 +261,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
Label titleLabel = new Label();
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.getStyleClass().add("jfx-decorator-title");
if (titleNode != null) {
if (titleNode == null) {
titleLabel.maxWidthProperty().bind(Bindings.createDoubleBinding(
() -> skinnable.getWidth() - 100 - navLeft.getWidth(),
skinnable.widthProperty(), navLeft.widthProperty()));
} else {
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
// 8 (margin-left)
return leftPaneWidth - 8 - navLeft.getWidth();
@@ -82,9 +82,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
modpackTab.setNodeSupplier(loadVersionFor(() -> {
ModpackDownloadListPage page = new ModpackDownloadListPage(Versions::downloadModpackImpl, false);
JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack"));
installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED);
installLocalModpackButton.getStyleClass().add("jfx-button-raised");
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
page.getActions().add(installLocalModpackButton);
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Control;
@@ -35,6 +34,7 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
@@ -119,7 +119,6 @@ public class InstallersPage extends Control implements WizardPage {
public void cleanup(Map<String, Object> settings) {
}
@FXML
protected void onInstall() {
controller.getSettings().put("name", txtName.getText());
controller.onFinish();
@@ -164,10 +163,8 @@ public class InstallersPage extends Control implements WizardPage {
{
JFXButton installButton = new JFXButton(i18n("button.install"));
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
installButton.disableProperty().bind(control.installable.not());
installButton.getStyleClass().add("jfx-button-raised");
installButton.setButtonType(JFXButton.ButtonType.RAISED);
installButton.setPrefWidth(100);
installButton.setPrefHeight(40);
installButton.setOnMouseClicked(e -> control.onInstall());
@@ -17,15 +17,9 @@
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
@@ -39,10 +33,8 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -52,52 +44,21 @@ import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LocalModpackPage extends StackPane implements WizardPage {
private final WizardController controller;
private Modpack manifest = null;
@FXML
private Region borderPane;
@FXML
private Label lblName;
@FXML
private Label lblVersion;
@FXML
private Label lblAuthor;
@FXML
private Label lblModpackLocation;
@FXML
private JFXTextField txtModpackName;
@FXML
private JFXButton btnInstall;
@FXML
private SpinnerPane spinnerPane;
public final class LocalModpackPage extends ModpackPage {
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
private Modpack manifest = null;
private Charset charset;
public LocalModpackPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/modpack.fxml");
super(controller);
Profile profile = (Profile) controller.getSettings().get("PROFILE");
File selectedFile;
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
if (name.isPresent()) {
txtModpackName.setText(name.get());
@@ -116,12 +77,9 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
}
});
btnInstall.disableProperty().bind(
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
.not());
}
File selectedFile;
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
selectedFile = filePath.get();
@@ -138,7 +96,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
controller.getSettings().put(MODPACK_FILE, selectedFile);
}
spinnerPane.showSpinner();
showSpinner();
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
.thenApplyAsync(encoding -> {
charset = encoding;
@@ -147,7 +105,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
})
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
if (exception instanceof ManuallyCreatedModpackException) {
spinnerPane.hideSpinner();
hideSpinner();
lblName.setText(selectedFile.getName());
installAsVersion.set(false);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
@@ -167,7 +125,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
Platform.runLater(controller::onEnd);
} else {
spinnerPane.hideSpinner();
hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
@@ -188,26 +146,19 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
settings.remove(MODPACK_FILE);
}
@FXML
private void onInstall() {
protected void onInstall() {
if (!txtModpackName.validate()) return;
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
controller.getSettings().put(MODPACK_CHARSET, charset);
controller.onFinish();
}
@FXML
private void onDescribe() {
protected void onDescribe() {
if (manifest != null) {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
}
@Override
public String getTitle() {
return i18n("modpack.task.install");
}
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
@@ -0,0 +1,117 @@
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final WizardController controller;
protected final Label lblName;
protected final Label lblVersion;
protected final Label lblAuthor;
protected final Label lblModpackLocation;
protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall;
protected ModpackPage(WizardController controller) {
this.controller = controller;
this.getStyleClass().add("large-spinner-pane");
VBox borderPane = new VBox();
borderPane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(borderPane, 500);
ComponentList componentList = new ComponentList();
{
BorderPane locationPane = new BorderPane();
{
locationPane.setLeft(new Label(i18n("modpack.task.install.will")));
lblModpackLocation = new Label();
BorderPane.setAlignment(lblModpackLocation, Pos.CENTER_RIGHT);
locationPane.setCenter(lblModpackLocation);
}
BorderPane archiveNamePane = new BorderPane();
{
Label label = new Label(i18n("archive.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
archiveNamePane.setLeft(label);
txtModpackName = new JFXTextField();
BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setCenter(txtModpackName);
}
BorderPane modpackNamePane = new BorderPane();
{
modpackNamePane.setLeft(new Label(i18n("modpack.name")));
lblName = new Label();
BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT);
modpackNamePane.setCenter(lblName);
}
BorderPane versionPane = new BorderPane();
{
versionPane.setLeft(new Label(i18n("archive.version")));
lblVersion = new Label();
BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT);
versionPane.setCenter(lblVersion);
}
BorderPane authorPane = new BorderPane();
{
authorPane.setLeft(new Label(i18n("archive.author")));
lblAuthor = new Label();
BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT);
authorPane.setCenter(lblAuthor);
}
BorderPane descriptionPane = new BorderPane();
{
JFXButton btnDescription = new JFXButton(i18n("modpack.description"));
btnDescription.getStyleClass().add("jfx-button-border");
btnDescription.setOnAction(e -> onDescribe());
descriptionPane.setLeft(btnDescription);
btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
btnInstall.setOnAction(e -> onInstall());
btnInstall.disableProperty().bind(createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty()));
descriptionPane.setRight(btnInstall);
}
componentList.getContent().setAll(
locationPane, archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
}
borderPane.getChildren().setAll(componentList);
this.setContent(borderPane);
}
protected abstract void onInstall();
protected abstract void onDescribe();
@Override
public String getTitle() {
return i18n("modpack.task.install");
}
}
@@ -20,8 +20,12 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
@@ -30,6 +34,8 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.TaskCancellationAction;
@@ -48,18 +54,23 @@ import static org.jackhuang.hmcl.ui.download.RemoteModpackPage.MODPACK_SERVER_MA
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackSelectionPage extends StackPane implements WizardPage {
public final class ModpackSelectionPage extends VBox implements WizardPage {
private final WizardController controller;
@FXML private JFXButton btnLocal;
@FXML private JFXButton btnRemote;
public ModpackSelectionPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/modpack-source.fxml");
JFXDepthManager.setDepth(btnLocal, 1);
JFXDepthManager.setDepth(btnRemote, 1);
Label title = new Label(i18n("install.modpack"));
title.setPadding(new Insets(8));
this.getStyleClass().add("jfx-list-view");
this.setMaxSize(400, 150);
this.setSpacing(8);
this.getChildren().setAll(
title,
createButton("local", this::onChooseLocalFile),
createButton("remote", this::onChooseRemoteFile)
);
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
@@ -74,7 +85,30 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
});
}
@FXML
private JFXButton createButton(String type, Runnable action) {
JFXButton button = new JFXButton();
button.getStyleClass().add("card");
button.setStyle("-fx-cursor: HAND;");
button.prefWidthProperty().bind(this.widthProperty());
button.setOnAction(e -> action.run());
BorderPane graphic = new BorderPane();
graphic.setMouseTransparent(true);
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
SVGPath arrow = new SVGPath();
arrow.setContent(SVG.ARROW_RIGHT);
BorderPane.setAlignment(arrow, Pos.CENTER);
graphic.setRight(arrow);
button.setGraphic(graphic);
JFXDepthManager.setDepth(button, 1);
return button;
}
private void onChooseLocalFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
@@ -89,7 +123,6 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
controller.onNext();
}
@FXML
private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
try {
@@ -17,14 +17,7 @@
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
@@ -32,59 +25,21 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RemoteModpackPage extends StackPane implements WizardPage {
private final WizardController controller;
public final class RemoteModpackPage extends ModpackPage {
private final ServerModpackManifest manifest;
@FXML
private Region borderPane;
@FXML
private Label lblName;
@FXML
private Label lblVersion;
@FXML
private Label lblAuthor;
@FXML
private Label lblModpackLocation;
@FXML
private JFXTextField txtModpackName;
@FXML
private JFXButton btnInstall;
@FXML
private SpinnerPane spinnerPane;
public RemoteModpackPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/download/modpack.fxml");
Profile profile = (Profile) controller.getSettings().get("PROFILE");
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
if (name.isPresent()) {
txtModpackName.setText(name.get());
txtModpackName.setDisable(true);
}
super(controller);
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
@@ -102,16 +57,18 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
if (!name.isPresent()) {
Profile profile = (Profile) controller.getSettings().get("PROFILE");
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
if (name.isPresent()) {
txtModpackName.setText(name.get());
txtModpackName.setDisable(true);
} else {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
txtModpackName.getValidators().addAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
btnInstall.disableProperty().bind(
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
.not());
}
}
@@ -120,23 +77,16 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
settings.remove(MODPACK_SERVER_MANIFEST);
}
@FXML
private void onInstall() {
protected void onInstall() {
if (!txtModpackName.validate()) return;
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
controller.onFinish();
}
@FXML
private void onDescribe() {
protected void onDescribe() {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
@Override
public String getTitle() {
return i18n("modpack.task.install");
}
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
@@ -23,15 +23,11 @@ import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
@@ -45,17 +41,18 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.util.EnumMap;
@@ -75,30 +72,18 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private final String title;
private final Navigation navigation;
@FXML
private JFXListView<RemoteVersion> list;
@FXML
private JFXSpinner spinner;
@FXML
private StackPane failedPane;
@FXML
private StackPane emptyPane;
@FXML
private TransitionPane root;
@FXML
private JFXCheckBox chkRelease;
@FXML
private JFXCheckBox chkSnapshot;
@FXML
private JFXCheckBox chkOld;
@FXML
private JFXButton btnRefresh;
@FXML
private HBox checkPane;
@FXML
private ComponentList centrePane;
@FXML
private StackPane center;
private final JFXListView<RemoteVersion> list;
private final JFXSpinner spinner;
private final StackPane failedPane;
private final StackPane emptyPane;
private final TransitionPane root;
private final JFXCheckBox chkRelease;
private final JFXCheckBox chkSnapshot;
private final JFXCheckBox chkOld;
private final JFXButton btnRefresh;
private final HBox checkPane;
private final ComponentList centrePane;
private final StackPane center;
private final VersionList<?> versionList;
private CompletableFuture<?> executor;
@@ -109,7 +94,75 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
this.libraryId = libraryId;
this.navigation = navigation;
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
HintPane hintPane = new HintPane();
hintPane.setText(i18n("sponsor.bmclapi"));
hintPane.getStyleClass().add("sponsor-pane");
hintPane.setOnMouseClicked(e -> onSponsor());
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
this.setTop(hintPane);
root = new TransitionPane();
{
spinner = new JFXSpinner();
center = new StackPane();
center.setStyle("-fx-padding: 10;");
{
centrePane = new ComponentList();
centrePane.getStyleClass().add("no-padding");
{
checkPane = new HBox();
checkPane.setSpacing(10);
{
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
chkRelease.setSelected(true);
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
chkOld = new JFXCheckBox(i18n("version.game.old"));
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
HBox pane = new HBox();
HBox.setHgrow(pane, Priority.ALWAYS);
btnRefresh = new JFXButton(i18n("button.refresh"));
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
btnRefresh.setOnAction(e -> onRefresh());
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld, pane, btnRefresh);
}
list = new JFXListView<>();
list.getStyleClass().add("jfx-list-view-float");
VBox.setVgrow(list, Priority.ALWAYS);
centrePane.getContent().setAll(checkPane, list);
}
center.getChildren().setAll(centrePane);
}
failedPane = new StackPane();
failedPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.refresh"));
label.setOnMouseClicked(e -> onRefresh());
failedPane.getChildren().setAll(label);
}
emptyPane = new StackPane();
emptyPane.getStyleClass().add("notice-pane");
{
Label label = new Label(i18n("download.failed.empty"));
label.setOnMouseClicked(e -> onBack());
emptyPane.getChildren().setAll(label);
}
}
this.setCenter(root);
versionList = downloadProvider.getVersionListById(libraryId);
if (versionList.hasType()) {
@@ -126,7 +179,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
MutableObject<RemoteVersionListCell> lastCell = new MutableObject<>();
Holder<RemoteVersionListCell> lastCell = new Holder<>();
EnumMap<VersionIconType, Image> icons = new EnumMap<>(VersionIconType.class);
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, icons));
@@ -205,15 +258,12 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
executor.cancel(true);
}
@FXML
private void onRefresh() {
refresh();
}
@FXML
private void onBack() { navigation.onPrev(true); }
@FXML
private void onSponsor() {
HMCLService.openRedirectLink("bmclapi_sponsor");
}
@@ -223,10 +273,10 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
final RipplerContainer ripplerContainer = new RipplerContainer(content);
final StackPane pane = new StackPane();
private final MutableObject<RemoteVersionListCell> lastCell;
private final Holder<RemoteVersionListCell> lastCell;
private final EnumMap<VersionIconType, Image> icons;
RemoteVersionListCell(MutableObject<RemoteVersionListCell> lastCell, EnumMap<VersionIconType, Image> icons) {
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, EnumMap<VersionIconType, Image> icons) {
this.lastCell = lastCell;
this.icons = icons;
@@ -236,7 +286,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
}
private Image getIcon(VersionIconType type) {
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl(), 32, 32, false, true));
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl()));
}
@Override
@@ -244,9 +294,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
super.updateItem(remoteVersion, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
if (this == lastCell.value && !isVisible())
return;
lastCell.setValue(this);
lastCell.value = this;
if (empty) {
setGraphic(null);
@@ -17,14 +17,16 @@
*/
package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTreeView;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -47,12 +49,10 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
*/
public final class ModpackFileSelectionPage extends StackPane implements WizardPage {
public final class ModpackFileSelectionPage extends BorderPane implements WizardPage {
private final WizardController controller;
private final String version;
private final ModAdviser adviser;
@FXML
private JFXTreeView<String> treeView;
private final CheckBoxTreeItem<String> rootNode;
public ModpackFileSelectionPage(WizardController controller, Profile profile, String version, ModAdviser adviser) {
@@ -60,10 +60,24 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
this.version = version;
this.adviser = adviser;
FXUtils.loadFXML(this, "/assets/fxml/modpack/selection.fxml");
JFXTreeView<String> treeView = new JFXTreeView<>();
rootNode = getTreeItem(profile.getRepository().getRunDirectory(version), "minecraft");
treeView.setRoot(rootNode);
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
this.setCenter(treeView);
HBox nextPane = new HBox();
nextPane.setPadding(new Insets(16, 16, 16, 0));
nextPane.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton btnNext = FXUtils.newRaisedButton(i18n("wizard.next"));
btnNext.setPrefSize(100, 40);
btnNext.setOnAction(e -> onNext());
nextPane.getChildren().setAll(btnNext);
}
this.setBottom(nextPane);
}
private CheckBoxTreeItem<String> getTreeItem(File file, String basePath) {
@@ -145,7 +159,6 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
controller.getSettings().remove(MODPACK_FILE_SELECTION);
}
@FXML
private void onNext() {
ArrayList<String> list = new ArrayList<>();
getFilesNeeded(rootNode, "minecraft", list);
@@ -377,13 +377,10 @@ public final class ModpackInfoPage extends Control implements WizardPage {
hbox.setAlignment(Pos.CENTER_RIGHT);
borderPane.setBottom(hbox);
JFXButton nextButton = new JFXButton();
JFXButton nextButton = FXUtils.newRaisedButton(i18n("wizard.next"));
nextButton.onMouseClickedProperty().bind(skinnable.next);
nextButton.setPrefWidth(100);
nextButton.setPrefHeight(40);
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
nextButton.setText(i18n("wizard.next"));
nextButton.getStyleClass().add("jfx-button-raised");
nextButton.disableProperty().bind(
// Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill
// our requirement.
@@ -18,13 +18,18 @@
package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.SVGPath;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -33,31 +38,47 @@ import java.util.Map;
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
private final WizardController controller;
@FXML
private JFXButton btnMCBBS;
@FXML
private JFXButton btnMultiMC;
@FXML
private JFXButton btnServer;
public ModpackTypeSelectionPage(WizardController controller) {
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
JFXButton[] buttons = new JFXButton[]{btnMCBBS, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_MCBBS, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
ModpackExportInfo.Options[] options = new ModpackExportInfo.Options[]{McbbsModpackExportTask.OPTION, MultiMCModpackExportTask.OPTION, ServerModpackExportTask.OPTION};
for (int i = 0; i < types.length; ++i) {
String type = types[i];
ModpackExportInfo.Options option = options[i];
buttons[i].setOnMouseClicked(e -> {
controller.getSettings().put(MODPACK_TYPE, type);
controller.getSettings().put(MODPACK_INFO_OPTION, option);
controller.onNext();
});
}
Label title = new Label(i18n("modpack.export.as"));
title.setPadding(new Insets(8));
this.getStyleClass().add("jfx-list-view");
this.setMaxSize(300, 150);
this.getChildren().setAll(
title,
createButton(MODPACK_TYPE_MCBBS, McbbsModpackExportTask.OPTION),
createButton(MODPACK_TYPE_MULTIMC, MultiMCModpackExportTask.OPTION),
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION)
);
}
private JFXButton createButton(String type, ModpackExportInfo.Options option) {
JFXButton button = new JFXButton();
button.setOnAction(e -> {
controller.getSettings().put(MODPACK_TYPE, type);
controller.getSettings().put(MODPACK_INFO_OPTION, option);
controller.onNext();
});
button.prefWidthProperty().bind(this.widthProperty());
BorderPane graphic = new BorderPane();
graphic.setMouseTransparent(true);
graphic.setLeft(new TwoLineListItem(i18n("modpack.type." + type), i18n("modpack.type." + type + ".export")));
SVGPath arrow = new SVGPath();
arrow.setContent(SVG.ARROW_RIGHT);
BorderPane.setAlignment(arrow, Pos.CENTER);
graphic.setRight(arrow);
button.setGraphic(graphic);
return button;
}
@Override
@@ -17,13 +17,13 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXScrollPane;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
@@ -35,13 +35,13 @@ public class AboutPage extends StackPane {
ComponentList about = new ComponentList();
{
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
launcher.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
launcher.setImage(new Image("/assets/img/craft_table.png"));
launcher.setTitle("Hello Minecraft! Launcher");
launcher.setSubtitle(Metadata.VERSION);
launcher.setExternalLink("https://hmcl.huangyuhui.net");
IconedTwoLineListItem author = new IconedTwoLineListItem();
author.setImage(new Image("/assets/img/yellow_fish.jpg", 32, 32, false, true));
author.setImage(new Image("/assets/img/yellow_fish.png"));
author.setTitle("huanghongxun");
author.setSubtitle(i18n("about.author.statement"));
author.setExternalLink("https://space.bilibili.com/1445341");
@@ -52,83 +52,66 @@ public class AboutPage extends StackPane {
ComponentList thanks = new ComponentList();
{
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
yushijinhun.setImage(new Image("/assets/img/yushijinhun.jpg", 32, 32, false, true));
yushijinhun.setImage(new Image("/assets/img/yushijinhun.png"));
yushijinhun.setTitle("yushijinhun");
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
yushijinhun.setExternalLink("https://yushi.moe/");
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
bangbang93.setImage(new Image("/assets/img/bangbang93.jpg", 32, 32, false, true));
bangbang93.setImage(new Image("/assets/img/bangbang93.png"));
bangbang93.setTitle("bangbang93");
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
glavo.setImage(new Image("/assets/img/glavo.jpg", 32, 32, false, true));
glavo.setImage(new Image("/assets/img/glavo.png"));
glavo.setTitle("Glavo");
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
glavo.setExternalLink("https://github.com/Glavo");
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
gamerteam.setTitle("gamerteam");
gamerteam.setImage(new Image("/assets/img/gamerteam.jpg", 32, 32, false, true));
gamerteam.setImage(new Image("/assets/img/gamerteam.png"));
gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
gamerteam.setExternalLink("http://www.zhaisoul.com/");
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
redLnn.setTitle("Red_lnn");
redLnn.setImage(new Image("/assets/img/red_lnn.jpg", 32, 32, false, true));
redLnn.setImage(new Image("/assets/img/red_lnn.png"));
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
mcbbs.setImage(new Image("/assets/img/chest.png", 32, 32, false, true));
mcbbs.setImage(new Image("/assets/img/chest.png"));
mcbbs.setTitle(i18n("about.thanks_to.mcbbs"));
mcbbs.setSubtitle(i18n("about.thanks_to.mcbbs.statement"));
mcbbs.setExternalLink("https://www.mcbbs.net/");
IconedTwoLineListItem mcmod = new IconedTwoLineListItem();
mcmod.setImage(new Image("/assets/img/mcmod.png", 32, 32, false, true));
mcmod.setImage(new Image("/assets/img/mcmod.png"));
mcmod.setTitle(i18n("about.thanks_to.mcmod"));
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(new Image("/assets/img/github.png", 32, 32, false, true));
contributors.setImage(new Image("/assets/img/github.png"));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, mcbbs, mcmod, gamerteam, redLnn, contributors);
}
ComponentList community = new ComponentList();
{
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
users.setImage(new Image("/assets/img/craft_table.png"));
users.setTitle(i18n("about.thanks_to.users"));
users.setSubtitle(i18n("about.thanks_to.users.statement"));
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
IconedTwoLineListItem discord = new IconedTwoLineListItem();
discord.setImage(new Image("/assets/img/discord.png", 32, 32, false, true));
discord.setTitle(i18n("about.community.discord"));
discord.setSubtitle(i18n("about.community.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
kookapp.setImage(new Image("/assets/img/kookapp.png", 32, 32, false, true));
kookapp.setTitle(i18n("about.community.kookapp"));
kookapp.setSubtitle(i18n("about.community.kookapp.statement"));
kookapp.setExternalLink("https://kook.top/Kx7n3t");
community.getContent().setAll(users, discord, kookapp);
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, mcbbs, mcmod, gamerteam, redLnn, contributors, users);
}
ComponentList dep = new ComponentList();
{
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
javafx.setTitle("JavaFX");
javafx.setSubtitle("Copyright © 2013, 2021, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setSubtitle("Copyright © 2013, 2023, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setExternalLink("https://openjfx.io/");
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
@@ -161,17 +144,12 @@ public class AboutPage extends StackPane {
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
IconedTwoLineListItem jstun = new IconedTwoLineListItem();
jstun.setTitle("JSTUN");
jstun.setSubtitle("Copyright © 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
jstun.setExternalLink("https://github.com/tking/JSTUN");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
minecraftJFXSkin.setSubtitle("Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.");
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, minecraftJFXSkin);
}
ComponentList legal = new ComponentList();
@@ -203,9 +181,6 @@ public class AboutPage extends StackPane {
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.community")),
community,
ComponentList.createComponentListTitle(i18n("about.dependency")),
dep,
@@ -216,7 +191,7 @@ public class AboutPage extends StackPane {
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
}
@@ -48,7 +48,7 @@ public class DownloadSettingsPage extends StackPane {
content.setPadding(new Insets(10));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
getChildren().setAll(scrollPane);
@@ -90,7 +90,7 @@ public class DownloadSettingsPage extends StackPane {
JFXComboBox<String> cboDownloadSource = new JFXComboBox<>();
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourcePane.setCenter(cboDownloadSource);
downloadSourcePane.setRight(cboDownloadSource);
FXUtils.setLimitWidth(cboDownloadSource, 420);
cboDownloadSource.getItems().setAll(DownloadProviders.rawProviders.keySet());
@@ -214,7 +214,6 @@ public class DownloadSettingsPage extends StackPane {
GridPane.setColumnIndex(txtProxyHost, 1);
gridPane.getChildren().add(txtProxyHost);
FXUtils.bindString(txtProxyHost, config().proxyHostProperty());
txtProxyHost.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
}
{
@@ -17,454 +17,64 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.HMCLAccounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FeedbackPage extends VBox implements PageAware {
private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList();
private final SpinnerPane spinnerPane = new SpinnerPane();
public class FeedbackPage extends SpinnerPane {
public FeedbackPage() {
setSpacing(10);
setPadding(new Insets(10));
VBox content = new VBox();
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
ComponentList community = new ComponentList();
{
HBox loginPane = new HBox(16);
loginPane.setAlignment(Pos.CENTER_LEFT);
loginPane.getStyleClass().add("card");
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(new Image("/assets/img/craft_table.png"));
users.setTitle(i18n("feedback.qq_group"));
users.setSubtitle(i18n("feedback.qq_group.statement"));
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
TwoLineListItem accountInfo = new TwoLineListItem();
HBox.setHgrow(accountInfo, Priority.ALWAYS);
accountInfo.titleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
.map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
accountInfo.subtitleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
.map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
IconedTwoLineListItem github = new IconedTwoLineListItem();
github.setImage(new Image("/assets/img/github.png"));
github.setTitle(i18n("feedback.github"));
github.setSubtitle(i18n("feedback.github.statement"));
github.setExternalLink("https://github.com/huanghongxun/HMCL/issues/new/choose");
JFXButton logButton = new JFXButton();
logButton.textProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
.map(account -> account == null ? i18n("account.login") : i18n("account.logout")));
logButton.setOnAction(e -> log());
IconedTwoLineListItem discord = new IconedTwoLineListItem();
discord.setImage(new Image("/assets/img/discord.png"));
discord.setTitle(i18n("feedback.discord"));
discord.setSubtitle(i18n("feedback.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
loginPane.getChildren().setAll(accountInfo, logButton);
getChildren().add(loginPane);
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
kookapp.setImage(new Image("/assets/img/kookapp.png"));
kookapp.setTitle(i18n("feedback.kookapp"));
kookapp.setSubtitle(i18n("feedback.kookapp.statement"));
kookapp.setExternalLink("https://kook.top/Kx7n3t");
community.getContent().setAll(users, github, discord, kookapp);
}
{
HBox searchPane = new HBox(8);
searchPane.getStyleClass().add("card");
getChildren().add(searchPane);
content.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("feedback.channel")),
community
);
JFXTextField searchField = new JFXTextField();
searchField.setOnAction(e -> search(searchField.getText(), "time", true));
HBox.setHgrow(searchField, Priority.ALWAYS);
searchField.setPromptText(i18n("search"));
JFXButton searchButton = new JFXButton();
searchButton.getStyleClass().add("toggle-icon4");
searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1));
searchButton.setOnAction(e -> search(searchField.getText(), "time", true));
JFXButton addButton = new JFXButton();
addButton.getStyleClass().add("toggle-icon4");
addButton.setGraphic(SVG.plus(Theme.blackFillBinding(), -1, -1));
addButton.setOnAction(e -> addFeedback());
searchPane.getChildren().setAll(searchField, searchButton, addButton);
}
{
spinnerPane.getStyleClass().add("card");
VBox.setVgrow(spinnerPane, Priority.ALWAYS);
JFXListView<FeedbackResponse> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), feedbacks);
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView, lastCell) {
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton likeButton = new JFXButton();
private final JFXButton unlikeButton = new JFXButton();
private final HBox container;
{
container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(content, Priority.ALWAYS);
content.setMouseTransparent(false);
setSelectable();
likeButton.getStyleClass().add("toggle-icon4");
likeButton.setGraphic(FXUtils.limitingSize(SVG.thumbUpOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
unlikeButton.getStyleClass().add("toggle-icon4");
unlikeButton.setGraphic(FXUtils.limitingSize(SVG.thumbDownOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
container.getChildren().setAll(content, likeButton, unlikeButton);
StackPane.setMargin(container, new Insets(10, 16, 10, 16));
getContainer().getChildren().setAll(container);
}
@Override
protected void updateControl(FeedbackResponse feedback, boolean empty) {
if (empty) return;
content.setTitle(feedback.getTitle());
content.setSubtitle(feedback.getAuthor());
content.getTags().setAll(
"#" + feedback.getId(),
i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)),
i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US)));
content.setOnMouseClicked(e -> {
getFeedback(feedback.getId())
.thenAcceptAsync(Schedulers.javafx(), f -> {
Controllers.dialog(new ViewFeedbackDialog(f));
})
.start();
});
}
});
getChildren().add(spinnerPane);
}
this.setContent(content);
}
@Override
public void onPageShown() {
search("", "time", false);
}
private void search(String keyword, String order, boolean showAll) {
HMCLAccounts.HMCLAccount account = HMCLAccounts.getAccount();
Task.supplyAsync(() -> {
Map<String, String> query = mapOf(
pair("keyword", keyword),
pair("order", order)
);
if (showAll) {
query.put("showAll", "1");
}
HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query));
if (account != null) {
req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider());
}
return req.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
spinnerPane.hideSpinner();
if (exception != null) {
if (exception instanceof ResponseCodeException) {
int responseCode = ((ResponseCodeException) exception).getResponseCode();
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
spinnerPane.setFailedReason(i18n("feedback.failed.permission"));
return;
} else if (responseCode == 429) {
spinnerPane.setFailedReason(i18n("feedback.failed.too_frequently"));
return;
}
}
spinnerPane.setFailedReason(i18n("feedback.failed"));
} else {
feedbacks.setAll(result);
}
}).start();
}
private Task<FeedbackResponse> getFeedback(int id) {
return Task.supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback/" + id).getJson(FeedbackResponse.class));
}
private void log() {
if (HMCLAccounts.getAccount() == null) {
// login
Controllers.dialog(new LoginDialog());
} else {
// logout
HMCLAccounts.setAccount(null);
}
}
private void addFeedback() {
if (HMCLAccounts.getAccount() == null) {
Controllers.dialog(i18n("feedback.add.login"));
return;
}
Controllers.dialog(new AddFeedbackDialog());
}
private static final class LoginDialog extends JFXDialogLayout {
private final SpinnerPane spinnerPane = new SpinnerPane();
private final Label errorLabel = new Label();
private final BooleanProperty logging = new SimpleBooleanProperty();
public LoginDialog() {
setHeading(new Label(i18n("feedback.login")));
VBox vbox = new VBox(8);
setBody(vbox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> i18n("account.hmcl.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
}
});
vbox.getChildren().setAll(hintPane);
JFXButton loginButton = new JFXButton();
spinnerPane.setContent(loginButton);
loginButton.setText(i18n("account.login"));
loginButton.setOnAction(e -> login());
JFXButton cancelButton = new JFXButton();
cancelButton.setText(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
setActions(errorLabel, spinnerPane, cancelButton);
}
private void login() {
spinnerPane.showSpinner();
errorLabel.setText("");
logging.set(true);
HMCLAccounts.login().whenComplete(Schedulers.javafx(), (result, exception) -> {
logging.set(false);
if (exception != null) {
if (exception instanceof IOException) {
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
} else if (exception instanceof JsonParseException) {
errorLabel.setText(i18n("account.failed.server_response_malformed"));
} else {
errorLabel.setText(Accounts.localizeErrorMessage(exception));
}
} else {
fireEvent(new DialogCloseEvent());
}
}).start();
}
}
private static class AddFeedbackDialog extends DialogPane {
JFXTextField titleField = new JFXTextField();
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
JFXTextArea contentArea = new JFXTextArea();
public AddFeedbackDialog() {
setTitle(i18n("feedback.add"));
GridPane body = new GridPane();
body.setVgap(8);
body.setHgap(8);
HintPane searchHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
GridPane.setColumnSpan(searchHintPane, 2);
searchHintPane.setText(i18n("feedback.add.hint.search_before_add"));
body.addRow(0, searchHintPane);
HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFO);
GridPane.setColumnSpan(titleHintPane, 2);
titleHintPane.setText(i18n("feedback.add.hint.title"));
body.addRow(1, titleHintPane);
titleField.setValidators(new RequiredValidator());
body.addRow(2, new Label(i18n("feedback.title")), titleField);
comboBox.setMaxWidth(-1);
comboBox.getItems().setAll(FeedbackType.values());
comboBox.getSelectionModel().select(0);
comboBox.setConverter(stringConverter(e -> i18n("feedback.type." + e.name().toLowerCase())));
body.addRow(3, new Label(i18n("feedback.type")), comboBox);
Label contentLabel = new Label(i18n("feedback.content"));
GridPane.setColumnSpan(contentLabel, 2);
body.addRow(4, contentLabel);
contentArea.setValidators(new RequiredValidator());
contentArea.setPromptText(i18n("feedback.add.hint.content"));
GridPane.setColumnSpan(contentArea, 2);
body.addRow(5, contentArea);
validProperty().bind(Bindings.createBooleanBinding(() -> {
return titleField.validate() && contentArea.validate();
}, titleField.textProperty(), contentArea.textProperty()));
setBody(body);
}
@Override
protected void onAccept() {
setLoading();
addFeedback(titleField.getText(), comboBox.getValue(), contentArea.getText())
.whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
onFailure(exception.getLocalizedMessage());
} else {
onSuccess();
}
})
.start();
}
private Task<?> addFeedback(String title, FeedbackType feedbackType, String content) {
return Task.runAsync(() -> {
HttpRequest.POST("https://hmcl.huangyuhui.net/api/feedback")
.json(mapOf(
pair("title", title),
pair("content", content),
pair("type", feedbackType.name().toLowerCase(Locale.ROOT)),
pair("launcher_version", Metadata.VERSION)
))
.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider())
.getString();
});
}
}
private static class ViewFeedbackDialog extends JFXDialogLayout {
public ViewFeedbackDialog(FeedbackResponse feedback) {
BorderPane heading = new BorderPane();
TwoLineListItem left = new TwoLineListItem();
heading.setLeft(left);
left.setTitle(feedback.getTitle());
left.setSubtitle(feedback.getAuthor());
left.getTags().add("#" + feedback.getId());
left.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)));
left.getTags().add(feedback.getLauncherVersion());
left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
setHeading(heading);
Label content = new Label(feedback.getContent());
content.setWrapText(true);
TwoLineListItem response = new TwoLineListItem();
response.getStyleClass().setAll("two-line-item-second-large");
response.setTitle(i18n("feedback.response"));
response.setSubtitle(StringUtils.isBlank(feedback.getReason())
? i18n("feedback.response.empty")
: feedback.getReason());
VBox body = new VBox(content, response);
body.setSpacing(8);
setBody(body);
JFXButton okButton = new JFXButton();
okButton.setText(i18n("button.ok"));
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
setActions(okButton);
}
}
private static class FeedbackResponse {
private final int id;
private final String title;
private final String content;
private final String author;
@SerializedName("launcher_version")
private final String launcherVersion;
private final FeedbackType type;
private final FeedbackState state;
private final String reason;
public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, FeedbackType type, FeedbackState state, String reason) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
this.launcherVersion = launcherVersion;
this.type = type;
this.state = state;
this.reason = reason;
}
public int getId() {
return id;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public String getAuthor() {
return author;
}
public String getLauncherVersion() {
return launcherVersion;
}
public FeedbackType getType() {
return type;
}
public FeedbackState getState() {
return state;
}
public String getReason() {
return reason;
}
}
private enum FeedbackType {
FEATURE,
BUG
}
private enum FeedbackState {
OPEN,
REJECTED,
ACCEPTED
}
}
@@ -19,13 +19,13 @@ package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.controls.JFXScrollPane;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
@@ -47,7 +47,7 @@ public class HelpPage extends SpinnerPane {
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
@@ -45,6 +45,7 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.AnnouncementCard;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
@@ -254,18 +255,22 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
private void doAnimation(boolean show) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)),
new KeyFrame(duration,
new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE)));
if (show) nowAnimation.getKeyFrames().add(
new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true)));
else nowAnimation.getKeyFrames().add(
new KeyFrame(duration, e -> updatePane.setVisible(false)));
nowAnimation.play();
if (AnimationUtils.isAnimationEnabled()) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)),
new KeyFrame(duration,
new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE)));
if (show) nowAnimation.getKeyFrames().add(
new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true)));
else nowAnimation.getKeyFrames().add(
new KeyFrame(duration, e -> updatePane.setVisible(false)));
nowAnimation.play();
} else {
updatePane.setVisible(show);
}
}
private void launch() {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
@@ -41,6 +40,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.util.Arrays;
@@ -55,7 +55,7 @@ public class PersonalizationPage extends StackPane {
content.setPadding(new Insets(10));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
getChildren().setAll(scrollPane);
@@ -72,7 +72,7 @@ public class PersonalizationPage extends StackPane {
themeColorPickerContainer.setMinHeight(30);
themePane.setRight(themeColorPickerContainer);
ColorPicker picker = new ColorPicker(Color.web(config().getTheme().getColor()));
ColorPicker picker = new ColorPicker(Color.web(Theme.getTheme().getColor()));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
@@ -88,6 +88,12 @@ public class PersonalizationPage extends StackPane {
titleTransparentButton.selectedProperty().bindBidirectional(config().titleTransparentProperty());
titleTransparentButton.setTitle(i18n("settings.launcher.title_transparent"));
}
{
OptionToggleButton animationButton = new OptionToggleButton();
themeList.getContent().add(animationButton);
animationButton.selectedProperty().bindBidirectional(config().animationDisabledProperty());
animationButton.setTitle(i18n("settings.launcher.turn_off_animations"));
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")), themeList);
{
@@ -141,7 +147,7 @@ public class PersonalizationPage extends StackPane {
HBox hBox = new HBox();
hBox.setSpacing(3);
FontComboBox cboLogFont = new FontComboBox(12);
FontComboBox cboLogFont = new FontComboBox();
cboLogFont.valueProperty().bindBidirectional(config().fontFamilyProperty());
JFXTextField txtLogFontSize = new JFXTextField();
@@ -159,7 +165,7 @@ public class PersonalizationPage extends StackPane {
Label lblLogFontDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
lblLogFontDisplay.fontProperty().bind(Bindings.createObjectBinding(
() -> Font.font(config().getFontFamily(), config().getFontSize()),
() -> Font.font(Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT), config().getFontSize()),
config().fontFamilyProperty(), config().fontSizeProperty()));
fontPane.getChildren().add(lblLogFontDisplay);
@@ -191,7 +197,7 @@ public class PersonalizationPage extends StackPane {
HBox hBox = new HBox();
hBox.setSpacing(8);
FontComboBox cboFont = new FontComboBox(12);
FontComboBox cboFont = new FontComboBox();
cboFont.valueProperty().bindBidirectional(config().launcherFontFamilyProperty());
JFXButton clearButton = new JFXButton();
@@ -210,7 +216,7 @@ public class PersonalizationPage extends StackPane {
() -> Font.font(config().getLauncherFontFamily(), 12),
config().launcherFontFamilyProperty()));
config().launcherFontFamilyProperty().addListener((a, b, newValue) -> {
Controllers.getScene().getStylesheets().setAll(config().getTheme().getStylesheets(newValue));
Controllers.getScene().getStylesheets().setAll(Theme.getTheme().getStylesheets(newValue));
});
vbox.getChildren().add(lblFontDisplay);
@@ -50,6 +50,7 @@ import java.io.File;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -156,7 +157,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
multiplayerItem.setActionButtonVisible(false);
multiplayerItem.setTitle(i18n("multiplayer"));
JFXHyperlink link = new JFXHyperlink(i18n("multiplayer.hint.details"));
link.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/multiplayer-migrate"));
link.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/multiplayer-migrate");
multiplayerItem.setOnAction(e -> Controllers.dialog(
new MessageDialogPane.Builder(i18n("multiplayer.hint"), null, MessageDialogPane.MessageType.INFO)
.addAction(link)
@@ -172,13 +173,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// the left sidebar
AdvancedListBox sideBar = new AdvancedListBox()
.startCategory(i18n("account").toUpperCase())
.startCategory(i18n("account").toUpperCase(Locale.ROOT))
.add(accountListItem)
.startCategory(i18n("version").toUpperCase())
.startCategory(i18n("version").toUpperCase(Locale.ROOT))
.add(gameListItem)
.add(gameItem)
.add(downloadItem)
.startCategory(i18n("settings.launcher.general").toUpperCase())
.startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT))
.add(multiplayerItem)
.add(launcherSettingsItem);
@@ -27,10 +27,10 @@ import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.math.BigDecimal;
@@ -85,16 +85,16 @@ public class SponsorPage extends StackPane {
StackPane pane = new StackPane();
pane.getStyleClass().add("card");
listView = new JFXListView<>();
MutableObject<Object> lastCell = new MutableObject<>();
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
@Override
public void updateItem(Sponsor item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
if (this == lastCell.value && !isVisible())
return;
lastCell.setValue(this);
lastCell.value = this;
if (!empty) {
setText(item.getName());
@@ -18,19 +18,27 @@
package org.jackhuang.hmcl.ui.profile;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.RequiredFieldValidator;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.FileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
@@ -40,15 +48,14 @@ import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ProfilePage extends StackPane implements DecoratorPage {
public final class ProfilePage extends BorderPane implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final StringProperty location;
private final Profile profile;
@FXML private JFXTextField txtProfileName;
@FXML private FileItem gameDir;
@FXML private JFXButton btnSave;
@FXML private JFXCheckBox toggleUseRelativePath;
private final JFXTextField txtProfileName;
private final FileItem gameDir;
private final OptionToggleButton toggleUseRelativePath;
/**
* @param profile null if creating a new profile.
@@ -61,37 +68,84 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(".minecraft"));
FXUtils.loadFXML(this, "/assets/fxml/profile.fxml");
txtProfileName.setText(profileDisplayName);
txtProfileName.getValidators().add(new ValidatorBase() {
ScrollPane scroll = new ScrollPane();
this.setCenter(scroll);
scroll.setFitToHeight(true);
scroll.setFitToWidth(true);
{
VBox rootPane = new VBox();
rootPane.setStyle("-fx-padding: 20;");
{
setMessage(i18n("profile.already_exists"));
ComponentList componentList = new ComponentList();
componentList.setDepth(1);
{
BorderPane profileNamePane = new BorderPane();
{
Label label = new Label(i18n("profile.name"));
profileNamePane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
txtProfileName = new JFXTextField();
profileNamePane.setRight(txtProfileName);
RequiredFieldValidator validator = new RequiredFieldValidator();
validator.setMessage(i18n("input.not_empty"));
txtProfileName.getValidators().add(validator);
BorderPane.setMargin(txtProfileName, new Insets(8, 0, 8, 0));
txtProfileName.setText(profileDisplayName);
txtProfileName.getValidators().add(new ValidatorBase() {
{
setMessage(i18n("profile.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
hasErrors.set(Profiles.getProfiles().stream().anyMatch(profile -> profile.getName().equals(control.getText())));
}
});
}
gameDir = new FileItem();
gameDir.setName(i18n("profile.instance_directory"));
gameDir.setTitle(i18n("profile.instance_directory.choose"));
gameDir.pathProperty().bindBidirectional(location);
toggleUseRelativePath = new OptionToggleButton();
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
if (profile != null) {
toggleUseRelativePath.setSelected(profile.isUseRelativePath());
}
componentList.getContent().setAll(profileNamePane, gameDir, toggleUseRelativePath);
}
rootPane.getChildren().setAll(componentList);
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
if (Profiles.getProfiles().stream().anyMatch(profile -> profile.getName().equals(control.getText())))
hasErrors.set(true);
else
hasErrors.set(false);
}
});
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
gameDir.pathProperty().bindBidirectional(location);
FXUtils.onChangeAndOperate(location, it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
if (profile != null) {
toggleUseRelativePath.setSelected(profile.isUseRelativePath());
scroll.setContent(rootPane);
}
BorderPane savePane = new BorderPane();
this.setBottom(savePane);
savePane.setPickOnBounds(false);
savePane.setStyle("-fx-padding: 20;");
StackPane.setAlignment(savePane, Pos.BOTTOM_RIGHT);
{
JFXButton saveButton = FXUtils.newRaisedButton(i18n("button.save"));
savePane.setRight(saveButton);
BorderPane.setAlignment(savePane, Pos.BOTTOM_RIGHT);
StackPane.setAlignment(saveButton, Pos.BOTTOM_RIGHT);
saveButton.setPrefSize(100, 40);
saveButton.setOnAction(e -> onSave());
saveButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtProfileName.validate() || StringUtils.isBlank(getLocation()),
txtProfileName.textProperty(), location));
}
}
@FXML
private void onSave() {
if (profile != null) {
profile.setName(txtProfileName.getText());
@@ -0,0 +1,258 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.game.Renderer;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class AdvancedVersionSettingPage extends StackPane implements DecoratorPage {
private final ObjectProperty<State> stateProperty;
private final Profile profile;
private final String versionId;
private final VersionSetting versionSetting;
private final JFXTextField txtJVMArgs;
private final JFXTextField txtGameArgs;
private final JFXTextField txtEnvironmentVariables;
private final JFXTextField txtMetaspace;
private final JFXTextField txtWrapper;
private final JFXTextField txtPreLaunchCommand;
private final JFXTextField txtPostExitCommand;
private final OptionToggleButton noJVMArgsPane;
private final OptionToggleButton noGameCheckPane;
private final OptionToggleButton noJVMCheckPane;
private final OptionToggleButton noNativesPatchPane;
private final OptionToggleButton useNativeGLFWPane;
private final OptionToggleButton useNativeOpenALPane;
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final JFXComboBox<Renderer> cboRenderer;
public AdvancedVersionSettingPage(Profile profile, String versionId, VersionSetting versionSetting) {
this.profile = profile;
this.versionId = versionId;
this.versionSetting = versionSetting;
this.stateProperty = new SimpleObjectProperty<>(State.fromTitle(
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
));
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
getChildren().setAll(scrollPane);
VBox rootPane = new VBox();
rootPane.setFillWidth(true);
scrollPane.setContent(rootPane);
FXUtils.smoothScrolling(scrollPane);
rootPane.getStyleClass().add("card-list");
ComponentList customCommandsPane = new ComponentList();
{
GridPane pane = new GridPane();
pane.setHgap(16);
pane.setVgap(8);
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
txtGameArgs = new JFXTextField();
txtGameArgs.setPromptText(i18n("settings.advanced.minecraft_arguments.prompt"));
txtGameArgs.getStyleClass().add("fit-width");
pane.addRow(0, new Label(i18n("settings.advanced.minecraft_arguments")), txtGameArgs);
txtPreLaunchCommand = new JFXTextField();
txtPreLaunchCommand.setPromptText(i18n("settings.advanced.precall_command.prompt"));
txtPreLaunchCommand.getStyleClass().add("fit-width");
pane.addRow(1, new Label(i18n("settings.advanced.precall_command")), txtPreLaunchCommand);
txtWrapper = new JFXTextField();
txtWrapper.setPromptText(i18n("settings.advanced.wrapper_launcher.prompt"));
txtWrapper.getStyleClass().add("fit-width");
pane.addRow(2, new Label(i18n("settings.advanced.wrapper_launcher")), txtWrapper);
txtPostExitCommand = new JFXTextField();
txtPostExitCommand.setPromptText(i18n("settings.advanced.post_exit_command.prompt"));
txtPostExitCommand.getStyleClass().add("fit-width");
pane.addRow(3, new Label(i18n("settings.advanced.post_exit_command")), txtPostExitCommand);
HintPane hintPane = new HintPane();
hintPane.setText(i18n("settings.advanced.custom_commands.hint"));
GridPane.setColumnSpan(hintPane, 2);
pane.addRow(4, hintPane);
customCommandsPane.getContent().setAll(pane);
}
ComponentList jvmPane = new ComponentList();
{
GridPane pane = new GridPane();
ColumnConstraints title = new ColumnConstraints();
ColumnConstraints value = new ColumnConstraints();
value.setFillWidth(true);
value.setHgrow(Priority.ALWAYS);
pane.setHgap(16);
pane.setVgap(8);
pane.getColumnConstraints().setAll(title, value);
txtJVMArgs = new JFXTextField();
txtJVMArgs.getStyleClass().add("fit-width");
pane.addRow(0, new Label(i18n("settings.advanced.jvm_args")), txtJVMArgs);
HintPane hintPane = new HintPane();
hintPane.setText(i18n("settings.advanced.jvm_args.prompt"));
GridPane.setColumnSpan(hintPane, 2);
pane.addRow(4, hintPane);
txtMetaspace = new JFXTextField();
txtMetaspace.setPromptText(i18n("settings.advanced.java_permanent_generation_space.prompt"));
txtMetaspace.getStyleClass().add("fit-width");
FXUtils.setValidateWhileTextChanged(txtMetaspace, true);
txtMetaspace.setValidators(new NumberValidator(i18n("input.number"), true));
pane.addRow(1, new Label(i18n("settings.advanced.java_permanent_generation_space")), txtMetaspace);
txtEnvironmentVariables = new JFXTextField();
txtEnvironmentVariables.getStyleClass().add("fit-width");
pane.addRow(2, new Label(i18n("settings.advanced.environment_variables")), txtEnvironmentVariables);
jvmPane.getContent().setAll(pane);
}
ComponentList workaroundPane = new ComponentList();
HintPane workaroundWarning = new HintPane(MessageDialogPane.MessageType.WARNING);
workaroundWarning.setText(i18n("settings.advanced.workaround.warning"));
{
nativesDirItem = new MultiFileItem<>();
nativesDirSublist = new ComponentSublist();
nativesDirSublist.getContent().add(nativesDirItem);
nativesDirSublist.setTitle(i18n("settings.advanced.natives_directory"));
nativesDirSublist.setHasSubtitle(true);
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.advanced.natives_directory.custom"), NativesDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
.setDirectory(true);
nativesDirItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
nativesDirCustomOption
));
HintPane nativesDirHint = new HintPane(MessageDialogPane.MessageType.WARNING);
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
nativesDirItem.getChildren().add(nativesDirHint);
BorderPane rendererPane = new BorderPane();
{
Label label = new Label(i18n("settings.advanced.renderer"));
rendererPane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
cboRenderer = new JFXComboBox<>();
cboRenderer.getItems().setAll(Renderer.values());
cboRenderer.setConverter(stringConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT))));
rendererPane.setRight(cboRenderer);
BorderPane.setAlignment(cboRenderer, Pos.CENTER_RIGHT);
FXUtils.setLimitWidth(cboRenderer, 300);
}
noJVMArgsPane = new OptionToggleButton();
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
noGameCheckPane = new OptionToggleButton();
noGameCheckPane.setTitle(i18n("settings.advanced.dont_check_game_completeness"));
noJVMCheckPane = new OptionToggleButton();
noJVMCheckPane.setTitle(i18n("settings.advanced.dont_check_jvm_validity"));
noNativesPatchPane = new OptionToggleButton();
noNativesPatchPane.setTitle(i18n("settings.advanced.dont_patch_natives"));
useNativeGLFWPane = new OptionToggleButton();
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
useNativeOpenALPane = new OptionToggleButton();
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
workaroundPane.getContent().setAll(
nativesDirSublist, rendererPane,
noJVMArgsPane, noGameCheckPane, noJVMCheckPane, noNativesPatchPane,
useNativeGLFWPane, useNativeOpenALPane);
}
rootPane.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.advanced.custom_commands")), customCommandsPane,
ComponentList.createComponentListTitle(i18n("settings.advanced.jvm")), jvmPane,
ComponentList.createComponentListTitle(i18n("settings.advanced.workaround")), workaroundWarning, workaroundPane
);
bindProperties();
}
void bindProperties() {
nativesDirCustomOption.bindBidirectional(versionSetting.nativesDirProperty());
FXUtils.bindString(txtJVMArgs, versionSetting.javaArgsProperty());
FXUtils.bindString(txtGameArgs, versionSetting.minecraftArgsProperty());
FXUtils.bindString(txtMetaspace, versionSetting.permSizeProperty());
FXUtils.bindString(txtEnvironmentVariables, versionSetting.environmentVariablesProperty());
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.bindEnum(cboRenderer, versionSetting.rendererProperty());
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
noNativesPatchPane.selectedProperty().bindBidirectional(versionSetting.notPatchNativesProperty());
useNativeGLFWPane.selectedProperty().bindBidirectional(versionSetting.useNativeGLFWProperty());
useNativeOpenALPane.selectedProperty().bindBidirectional(versionSetting.useNativeOpenALProperty());
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
}
void unbindProperties() {
nativesDirCustomOption.valueProperty().unbindBidirectional(versionSetting.nativesDirProperty());
FXUtils.unbind(txtJVMArgs, versionSetting.javaArgsProperty());
FXUtils.unbind(txtGameArgs, versionSetting.minecraftArgsProperty());
FXUtils.unbind(txtMetaspace, versionSetting.permSizeProperty());
FXUtils.unbind(txtEnvironmentVariables, versionSetting.environmentVariablesProperty());
FXUtils.unbind(txtWrapper, versionSetting.wrapperProperty());
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
FXUtils.unbindEnum(cboRenderer);
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
noNativesPatchPane.selectedProperty().unbindBidirectional(versionSetting.notPatchNativesProperty());
useNativeGLFWPane.selectedProperty().unbindBidirectional(versionSetting.useNativeGLFWProperty());
useNativeOpenALPane.selectedProperty().unbindBidirectional(versionSetting.useNativeOpenALProperty());
nativesDirItem.selectedDataProperty().unbindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirSublist.subtitleProperty().unbind();
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return stateProperty;
}
}
@@ -118,7 +118,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
if (!searchInitialized) {
searchInitialized = true;
search("", null, 0, "", RemoteModRepository.SortType.DATE_CREATED);
search("", null, 0, "", RemoteModRepository.SortType.POPULARITY);
}
if (versionSelection) {
@@ -334,10 +334,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
sortComboBox.getSelectionModel().select(0);
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
JFXButton searchButton = new JFXButton();
searchButton.setText(i18n("search"));
searchButton.getStyleClass().add("jfx-button-raised");
searchButton.setButtonType(JFXButton.ButtonType.RAISED);
JFXButton searchButton = FXUtils.newRaisedButton(i18n("search"));
ObservableList<Node> last = FXCollections.observableArrayList(searchButton);
HBox searchBox = new HBox(8);
actions.appendList(control.actions);
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
@@ -233,7 +232,7 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.getStyleClass().add("gray-background");
pane.setPadding(new Insets(10));
ScrollPane scrollPane = new ScrollPane(pane);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
@@ -261,18 +260,18 @@ public class DownloadPage extends Control implements DecoratorPage {
if (getSkinnable().mod != null) {
JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod"));
openMcmodButton.setOnAction(e -> FXUtils.openLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod)));
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
descriptionPane.getChildren().add(openMcmodButton);
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
openMcbbsButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs())));
openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs()));
descriptionPane.getChildren().add(openMcbbsButton);
}
}
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getPageUrl()));
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
descriptionPane.getChildren().add(openUrlButton);
}
@@ -323,12 +322,10 @@ public class DownloadPage extends Control implements DecoratorPage {
for (String gameVersion : control.versions.keys().stream()
.sorted(VersionNumber.VERSION_COMPARATOR.reversed())
.collect(Collectors.toList())) {
ComponentList sublist = new ComponentList();
sublist.setLazyInitializer(self -> {
self.getContent().setAll(control.versions.get(gameVersion).stream()
.map(version -> new ModItem(version, control))
.collect(Collectors.toList()));
});
ComponentList sublist = new ComponentList(() ->
control.versions.get(gameVersion).stream()
.map(version -> new ModItem(version, control))
.collect(Collectors.toList()));
sublist.getStyleClass().add("no-padding");
sublist.setTitle(gameVersion);
@@ -370,6 +367,7 @@ public class DownloadPage extends Control implements DecoratorPage {
}
private static final class ModItem extends StackPane {
ModItem(RemoteMod.Version dataItem, DownloadPage selfPage) {
HBox pane = new HBox(8);
pane.setPadding(new Insets(8));
@@ -379,9 +377,7 @@ public class DownloadPage extends Control implements DecoratorPage {
JFXButton saveAsButton = new JFXButton();
RipplerContainer container = new RipplerContainer(pane);
container.setOnMouseClicked(e -> {
selfPage.download(dataItem);
});
container.setOnMouseClicked(e -> selfPage.download(dataItem));
getChildren().setAll(container);
saveAsButton.getStyleClass().add("toggle-icon4");
@@ -392,7 +388,7 @@ public class DownloadPage extends Control implements DecoratorPage {
content.setTitle(dataItem.getName());
content.setSubtitle(FORMATTER.format(dataItem.getDatePublished().toInstant()));
saveAsButton.setOnMouseClicked(e -> selfPage.saveAs(dataItem));
saveAsButton.setOnAction(e -> selfPage.saveAs(dataItem));
switch (dataItem.getVersionType()) {
case Release:
@@ -408,6 +404,9 @@ public class DownloadPage extends Control implements DecoratorPage {
content.getTags().add(i18n("version.game.snapshot"));
break;
}
// Workaround for https://github.com/huanghongxun/HMCL/issues/2129
this.setMinHeight(50);
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.google.gson.JsonParseException;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -82,8 +83,8 @@ public class GameItem extends Control {
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
if (config == null) return;
tag.set(config.getVersion());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read modpack configuration from ", e);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Failed to read modpack configuration from " + version, e);
}
}, Platform::runLater)
.exceptionally(handleUncaught);
@@ -179,7 +179,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void checkUpdates() {
Controllers.taskDialog(Task
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(versionId);
if (gameVersion.isPresent()) {
@@ -190,12 +190,23 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Controllers.dialog("Failed to check updates", "failed", MessageDialogPane.MessageType.ERROR);
} else if (result.isEmpty()) {
Controllers.dialog(i18n("mods.check_updates.empty"));
} else {
Controllers.navigate(new ModUpdatesPage(modManager, result));
}
})
.withStagesHint(Collections.singletonList("mods.check_updates")),
i18n("update.checking"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(versionId)) {
Controllers.confirm(
i18n("mods.update_modpack_mod.warning"), null,
MessageDialogPane.MessageType.WARNING,
action, null);
} else {
action.run();
}
}
public void download() {
@@ -23,6 +23,7 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
@@ -31,7 +32,6 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Theme;
@@ -43,6 +43,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -57,17 +58,32 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class ModListPageSkin extends SkinBase<ModListPage> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
private final HBox toolbarNormal;
private final HBox toolbarSelecting;
private final JFXListView<ModInfoObject> listView;
private final JFXTextField searchField;
// FXThread
private boolean isSearching = false;
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
@@ -77,18 +93,45 @@ class ModListPageSkin extends SkinBase<ModListPage> {
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
JFXListView<ModInfoObject> listView = new JFXListView<>();
listView = new JFXListView<>();
{
TransitionPane toolBarPane = new TransitionPane();
HBox toolbarNormal = new HBox();
toolbarPane = new TransitionPane();
searchBar = new HBox();
toolbarNormal = new HBox();
toolbarSelecting = new HBox();
// Search Bar
searchBar.setAlignment(Pos.CENTER);
searchBar.setPadding(new Insets(0, 5, 0, 5));
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
searchField.setOnAction(e -> search());
JFXButton closeSearchBar = createToolbarButton2(null, SVG::close,
() -> {
changeToolbar(toolbarNormal);
isSearching = false;
searchField.clear();
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
});
searchBar.getChildren().setAll(searchField, closeSearchBar);
// Toolbar Normal
toolbarNormal.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, skinnable::openModFolder),
createToolbarButton2(i18n("mods.check_updates"), SVG::update, skinnable::checkUpdates),
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download));
HBox toolbarSelecting = new HBox();
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download),
createToolbarButton2(i18n("search"), SVG::magnify, () -> changeToolbar(searchBar))
);
// Toolbar Selecting
toolbarSelecting.getChildren().setAll(
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
@@ -102,15 +145,17 @@ class ModListPageSkin extends SkinBase<ModListPage> {
createToolbarButton2(i18n("button.select_all"), SVG::selectAll, () ->
listView.getSelectionModel().selectAll()),
createToolbarButton2(i18n("button.cancel"), SVG::cancel, () ->
listView.getSelectionModel().clearSelection()));
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), selectedItem -> {
if (selectedItem == null) {
toolBarPane.setContent(toolbarNormal, ContainerAnimations.FADE.getAnimationProducer());
} else {
toolBarPane.setContent(toolbarSelecting, ContainerAnimations.FADE.getAnimationProducer());
}
});
root.getContent().add(toolBarPane);
listView.getSelectionModel().clearSelection())
);
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
selectedItem -> {
if (selectedItem == null)
changeToolbar(isSearching ? searchBar : toolbarNormal);
else
changeToolbar(toolbarSelecting);
});
root.getContent().add(toolbarPane);
}
{
@@ -119,7 +164,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(skinnable.loadingProperty());
MutableObject<Object> lastCell = new MutableObject<>();
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ModInfoListCell(listView, lastCell));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
@@ -132,7 +177,6 @@ class ModListPageSkin extends SkinBase<ModListPage> {
label.prefWidthProperty().bind(pane.widthProperty().add(-100));
FXUtils.onChangeAndOperate(skinnable.moddedProperty(), modded -> {
if (modded) pane.getChildren().setAll(root);
else pane.getChildren().setAll(label);
});
@@ -140,6 +184,47 @@ class ModListPageSkin extends SkinBase<ModListPage> {
getChildren().setAll(pane);
}
private void changeToolbar(HBox newToolbar) {
Node oldToolbar = toolbarPane.getCurrentNode();
if (newToolbar != oldToolbar) {
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE.getAnimationProducer());
}
}
private void search() {
isSearching = true;
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
String queryString = searchField.getText();
if (StringUtils.isBlank(queryString)) {
listView.getItems().setAll(getSkinnable().getItems());
} else {
listView.getItems().clear();
Predicate<String> predicate;
if (queryString.startsWith("regex:")) {
try {
Pattern pattern = Pattern.compile(queryString.substring("regex:".length()));
predicate = s -> pattern.matcher(s).find();
} catch (Throwable e) {
LOG.log(Level.WARNING, "Illegal regular expression", e);
return;
}
} else {
String lowerQueryString = queryString.toLowerCase(Locale.ROOT);
predicate = s -> s.toLowerCase(Locale.ROOT).contains(lowerQueryString);
}
// Do we need to search in the background thread?
for (ModInfoObject item : getSkinnable().getItems()) {
if (predicate.test(item.getModInfo().getFileName())) {
listView.getItems().add(item);
}
}
}
}
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
private final BooleanProperty active;
private final LocalModFile localModFile;
@@ -223,8 +308,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
setBody(description);
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
JFXHyperlink officialPageButton = new JFXHyperlink();
officialPageButton.setText(i18n("mods.url"));
JFXHyperlink officialPageButton = new JFXHyperlink(i18n("mods.url"));
officialPageButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(modInfo.getModInfo().getUrl());
@@ -234,8 +318,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
}
if (modInfo.getMod() != null && StringUtils.isNotBlank(modInfo.getMod().getMcbbs())) {
JFXHyperlink mcbbsButton = new JFXHyperlink();
mcbbsButton.setText(i18n("mods.mcbbs"));
JFXHyperlink mcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
mcbbsButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(ModManager.getMcbbsUrl(modInfo.getMod().getMcbbs()));
@@ -244,8 +327,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
}
if (modInfo.getMod() == null || StringUtils.isBlank(modInfo.getMod().getMcmod())) {
JFXHyperlink searchButton = new JFXHyperlink();
searchButton.setText(i18n("mods.mcmod.search"));
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
searchButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
@@ -256,8 +338,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
});
getActions().add(searchButton);
} else {
JFXHyperlink mcmodButton = new JFXHyperlink();
mcmodButton.setText(i18n("mods.mcmod.page"));
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
mcmodButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getMod()));
@@ -286,7 +367,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
JFXButton revealButton = new JFXButton();
BooleanProperty booleanProperty;
ModInfoListCell(JFXListView<ModInfoObject> listView, MutableObject<Object> lastCell) {
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
super(listView, lastCell);
HBox container = new HBox(8);
@@ -17,37 +17,36 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -61,38 +60,37 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
private final ModManager modManager;
private final ObservableList<ModUpdateObject> objects;
@SuppressWarnings("unchecked")
public ModUpdatesPage(ModManager modManager, List<LocalModFile.ModUpdate> updates) {
this.modManager = modManager;
getStyleClass().add("gray-background");
JFXTreeTableColumn<ModUpdateObject, Boolean> enabledColumn = new JFXTreeTableColumn<>();
enabledColumn.setCellFactory(column -> new JFXCheckBoxTreeTableCell<>());
TableColumn<ModUpdateObject, Boolean> enabledColumn = new TableColumn<>();
enabledColumn.setCellFactory(CheckBoxTableCell.forTableColumn(enabledColumn));
setupCellValueFactory(enabledColumn, ModUpdateObject::enabledProperty);
enabledColumn.setEditable(true);
enabledColumn.setMaxWidth(40);
enabledColumn.setMinWidth(40);
JFXTreeTableColumn<ModUpdateObject, String> fileNameColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.file"));
TableColumn<ModUpdateObject, String> fileNameColumn = new TableColumn<>(i18n("mods.check_updates.file"));
fileNameColumn.setPrefWidth(200);
setupCellValueFactory(fileNameColumn, ModUpdateObject::fileNameProperty);
JFXTreeTableColumn<ModUpdateObject, String> currentVersionColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.current_version"));
TableColumn<ModUpdateObject, String> currentVersionColumn = new TableColumn<>(i18n("mods.check_updates.current_version"));
currentVersionColumn.setPrefWidth(200);
setupCellValueFactory(currentVersionColumn, ModUpdateObject::currentVersionProperty);
JFXTreeTableColumn<ModUpdateObject, String> targetVersionColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.target_version"));
TableColumn<ModUpdateObject, String> targetVersionColumn = new TableColumn<>(i18n("mods.check_updates.target_version"));
targetVersionColumn.setPrefWidth(200);
setupCellValueFactory(targetVersionColumn, ModUpdateObject::targetVersionProperty);
JFXTreeTableColumn<ModUpdateObject, String> sourceColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.source"));
TableColumn<ModUpdateObject, String> sourceColumn = new TableColumn<>(i18n("mods.check_updates.source"));
setupCellValueFactory(sourceColumn, ModUpdateObject::sourceProperty);
objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList()));
RecursiveTreeItem<ModUpdateObject> root = new RecursiveTreeItem<>(
objects,
RecursiveTreeObject::getChildren);
JFXTreeTableView<ModUpdateObject> table = new JFXTreeTableView<>(root);
table.setShowRoot(false);
TableView<ModUpdateObject> table = new TableView<>(objects);
table.setEditable(true);
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn);
@@ -102,14 +100,10 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
actions.setPadding(new Insets(8));
actions.setAlignment(Pos.CENTER_RIGHT);
JFXButton nextButton = new JFXButton(i18n("mods.check_updates.update"));
nextButton.getStyleClass().add("jfx-button-raised");
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
JFXButton nextButton = FXUtils.newRaisedButton(i18n("mods.check_updates.update"));
nextButton.setOnAction(e -> updateMods());
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("jfx-button-raised");
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
onEscPressed(this, cancelButton::fire);
@@ -117,13 +111,8 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
setBottom(actions);
}
private <T> void setupCellValueFactory(JFXTreeTableColumn<ModUpdateObject, T> column, Function<ModUpdateObject, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> {
if (column.validateValue(param))
return mapper.apply(param.getValue().getValue());
else
return column.getComputedValue(param);
});
private <T> void setupCellValueFactory(TableColumn<ModUpdateObject, T> column, Function<ModUpdateObject, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> mapper.apply(param.getValue()));
}
private void updateMods() {
@@ -138,7 +127,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
fireEvent(new PageCloseEvent());
if (!task.getFailedMods().isEmpty()) {
Controllers.dialog(i18n("mods.check_updates.failed") + "\n" +
task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")),
task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")),
i18n("install.failed"),
MessageDialogPane.MessageType.ERROR);
}
@@ -156,32 +145,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
return state;
}
public static class ModUpdateCell extends MDListCell<LocalModFile.ModUpdate> {
TwoLineListItem content = new TwoLineListItem();
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView, MutableObject<Object> lastCell) {
super(listView, lastCell);
getContainer().getChildren().setAll(content);
}
@Override
protected void updateControl(LocalModFile.ModUpdate item, boolean empty) {
if (empty) return;
ModTranslations.Mod mod = ModTranslations.MOD.getModById(item.getLocalMod().getId());
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : item.getCurrentVersion().getName());
content.setSubtitle(item.getLocalMod().getFileName());
content.getTags().setAll();
if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) {
content.getTags().add("Curseforge");
} else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ProjectVersion) {
content.getTags().add("Modrinth");
}
}
}
private static class ModUpdateObject extends RecursiveTreeObject<ModUpdateObject> {
private static final class ModUpdateObject {
final LocalModFile.ModUpdate data;
final BooleanProperty enabled = new SimpleBooleanProperty();
final StringProperty fileName = new SimpleStringProperty();
@@ -192,7 +156,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
public ModUpdateObject(LocalModFile.ModUpdate data) {
this.data = data;
enabled.set(true);
enabled.set(!data.getLocalMod().getModManager().isDisabled(data.getLocalMod().getFile()));
fileName.set(data.getLocalMod().getFileName());
currentVersion.set(data.getCurrentVersion().getVersion());
targetVersion.set(data.getCandidates().get(0).getVersion());
@@ -274,28 +238,37 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
setStage("mods.check_updates.update");
getProperties().put("total", mods.size());
dependents = mods.stream()
.map(mod -> {
return Task
.runAsync(Schedulers.javafx(), () -> mod.getKey().setOld(true))
.thenComposeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(
new URL(mod.getValue().getFile().getUrl()),
modManager.getModsDirectory().resolve(mod.getValue().getFile().getFilename()).toFile());
this.dependents = new ArrayList<>();
for (Pair<LocalModFile, RemoteMod.Version> mod : mods) {
LocalModFile local = mod.getKey();
RemoteMod.Version remote = mod.getValue();
boolean isDisabled = local.getModManager().isDisabled(local.getFile());
task.setName(mod.getValue().getName());
return task;
})
.whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
// restore state if failed
mod.getKey().setOld(false);
failedMods.add(mod.getKey());
}
})
.withCounter("mods.check_updates.update");
})
.collect(Collectors.toList());
dependents.add(Task
.runAsync(Schedulers.javafx(), () -> local.setOld(true))
.thenComposeAsync(() -> {
String fileName = remote.getFile().getFilename();
if (isDisabled)
fileName += ModManager.DISABLED_EXTENSION;
FileDownloadTask task = new FileDownloadTask(
new URL(remote.getFile().getUrl()),
modManager.getModsDirectory().resolve(fileName).toFile());
task.setName(remote.getName());
return task;
})
.whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
// restore state if failed
local.setOld(false);
if (isDisabled)
local.disable();
failedMods.add(local);
}
})
.withCounter("mods.check_updates.update"));
}
}
public List<LocalModFile> getFailedMods() {
Loaded 100 of 245 files, more files were not shown because too many files have changed in this diff. Show more