Compare commits
241
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1187ae4f04 | ||
|
|
8efdc536ee | ||
|
|
78f080b6cc | ||
|
|
cc8bcfd924 | ||
|
|
806ca3f64a | ||
|
|
4a16fa13ee | ||
|
|
b88d7c4fd2 | ||
|
|
c0838468b1 | ||
|
|
90cfe3d0b7 | ||
|
|
baf1d22bc4 | ||
|
|
7ca5744559 | ||
|
|
57e728991b | ||
|
|
e08ba4d421 | ||
|
|
ba91c60e95 | ||
|
|
389b93178e | ||
|
|
37aaf153f8 | ||
|
|
2249f4ee0e | ||
|
|
eadfff349a | ||
|
|
cb4cd696a0 | ||
|
|
c85c074eee | ||
|
|
b26c478d4a | ||
|
|
2d83de7c31 | ||
|
|
20d93dd085 | ||
|
|
24fc436870 | ||
|
|
3a6c23aa9d | ||
|
|
0ee8b2dd82 | ||
|
|
7cce90bed8 | ||
|
|
c806a5d412 | ||
|
|
14e8b9212f | ||
|
|
67e96fb84f | ||
|
|
c39c19f215 | ||
|
|
88d7493870 | ||
|
|
404bc88ec8 | ||
|
|
e2355ca2ff | ||
|
|
aab271ac75 | ||
|
|
0da0a1f386 | ||
|
|
59316a866d | ||
|
|
d3087bd763 | ||
|
|
df7b63c491 | ||
|
|
dff893ba4b | ||
|
|
c539702a1b | ||
|
|
6fad149410 | ||
|
|
a072fb7bea | ||
|
|
08a3850dcf | ||
|
|
030fba4630 | ||
|
|
559aefd6b6 | ||
|
|
2ee5d9ceed | ||
|
|
20ed42ab8a | ||
|
|
815b90845a | ||
|
|
e9846ea644 | ||
|
|
02065094ca | ||
|
|
9f6124d617 | ||
|
|
76229a894c | ||
|
|
03de1598c8 | ||
|
|
3a13ff0e10 | ||
|
|
63345f52ce | ||
|
|
0bff282d3d | ||
|
|
a81462425d | ||
|
|
e9ae43b1f1 | ||
|
|
dc31f0d5c9 | ||
|
|
76f11b6a6f | ||
|
|
d6ea2ef1f9 | ||
|
|
14db21211f | ||
|
|
f354f65adb | ||
|
|
83e8ea3735 | ||
|
|
0e9e9811eb | ||
|
|
420d77de9f | ||
|
|
9361719a67 | ||
|
|
b09a5868d4 | ||
|
|
56c48e5595 | ||
|
|
4d6cb547ea | ||
|
|
95ecd29018 | ||
|
|
5d26a106e9 | ||
|
|
95707560ef | ||
|
|
c54cb88db8 | ||
|
|
95afaa5112 | ||
|
|
fe608e32f1 | ||
|
|
82fc1c067d | ||
|
|
94890e2924 | ||
|
|
9a76b1ff27 | ||
|
|
8db8693132 | ||
|
|
59cc5b9114 | ||
|
|
c1acd0b0b5 | ||
|
|
fb6538f042 | ||
|
|
20fe5430dd | ||
|
|
7de07319d9 | ||
|
|
8e3c19651e | ||
|
|
2740bdeb77 | ||
|
|
59ab49e0c5 | ||
|
|
bc4aa43834 | ||
|
|
c7f3f2a229 | ||
|
|
ffeabbf1f1 | ||
|
|
c9ddb7aab4 | ||
|
|
997552fb7e | ||
|
|
7d9595945d | ||
|
|
70157fa8de | ||
|
|
22e1e6344f | ||
|
|
3952b6e3a8 | ||
|
|
dd2a09399e | ||
|
|
7f7068a556 | ||
|
|
a79a2014b0 | ||
|
|
4c41f25314 | ||
|
|
c26eb2bc69 | ||
|
|
e93e5effa6 | ||
|
|
7acc39154b | ||
|
|
19cae84a01 | ||
|
|
0bcda9eb46 | ||
|
|
7556ef21c4 | ||
|
|
9290c26c0b | ||
|
|
5388020b35 | ||
|
|
017d688e32 | ||
|
|
4102029d6d | ||
|
|
d286f93a7e | ||
|
|
207b4d0033 | ||
|
|
351a59149f | ||
|
|
7880c2f3d9 | ||
|
|
644866a24b | ||
|
|
43fee4878f | ||
|
|
1d8ead2cbb | ||
|
|
4cbf5ce979 | ||
|
|
1eb53b247c | ||
|
|
f53aed9a08 | ||
|
|
5db3c123a0 | ||
|
|
e1e9215f2c | ||
|
|
23f58e63aa | ||
|
|
2cad6f33f5 | ||
|
|
308086b876 | ||
|
|
9bdf08af1e | ||
|
|
ddcead7c85 | ||
|
|
89b5b64943 | ||
|
|
931808d808 | ||
|
|
b9a6401ef3 | ||
|
|
59a9b3a926 | ||
|
|
96270b1706 | ||
|
|
af784dd0d0 | ||
|
|
1a1f2133f7 | ||
|
|
3f1be961a6 | ||
|
|
8f2afdfcff | ||
|
|
532ba6ec2f | ||
|
|
8678c0dcf2 | ||
|
|
2116cfd789 | ||
|
|
45d9033252 | ||
|
|
96df35f728 | ||
|
|
16b0335fcf | ||
|
|
b69dadc80c | ||
|
|
7619e48145 | ||
|
|
fbc667798d | ||
|
|
4e22585faf | ||
|
|
5d3660ffb8 | ||
|
|
4149876e04 | ||
|
|
e6e4e36537 | ||
|
|
5b7149b967 | ||
|
|
f558be7193 | ||
|
|
a9159e1a72 | ||
|
|
b0e3ae7684 | ||
|
|
242df8a81a | ||
|
|
333fd0ef80 | ||
|
|
5defff2bb0 | ||
|
|
bdcbe6c948 | ||
|
|
cc192c840c | ||
|
|
e5ece58d17 | ||
|
|
2391b26bd6 | ||
|
|
c7fad05bae | ||
|
|
1d9d31f906 | ||
|
|
d584c101d4 | ||
|
|
ab41feff42 | ||
|
|
cb7975515a | ||
|
|
91f684ae2e | ||
|
|
55c55e9bda | ||
|
|
453ba6eb75 | ||
|
|
5aeaba4636 | ||
|
|
df9499b723 | ||
|
|
f84be1e109 | ||
|
|
31872f4f77 | ||
|
|
54188bf8f2 | ||
|
|
9fb2bc4c7e | ||
|
|
5d8a56cf66 | ||
|
|
833ac2e32e | ||
|
|
50173a3987 | ||
|
|
9d5202c19f | ||
|
|
e1bbd8b326 | ||
|
|
06f81b9b09 | ||
|
|
150baa0e30 | ||
|
|
413d962db0 | ||
|
|
74118049ce | ||
|
|
05086fa76d | ||
|
|
c1ffd26c80 | ||
|
|
a8b0f81b68 | ||
|
|
d83b508ce4 | ||
|
|
fe622579cf | ||
|
|
edcdeaf018 | ||
|
|
9d4eb9662e | ||
|
|
42e88fe6f5 | ||
|
|
61165dd71d | ||
|
|
83120afb63 | ||
|
|
4fca632efb | ||
|
|
03b20a70bb | ||
|
|
20ee9c8ce6 | ||
|
|
9c0e1b1dc8 | ||
|
|
f29e350b1a | ||
|
|
bbe0f5d750 | ||
|
|
371295d9fe | ||
|
|
cc8a4ddf19 | ||
|
|
f3ff037f1c | ||
|
|
3b36bbc88e | ||
|
|
80b53119ed | ||
|
|
4d88c2b1bb | ||
|
|
29582f6c50 | ||
|
|
623c54cf58 | ||
|
|
6eee8b89e1 | ||
|
|
c74ef3eca8 | ||
|
|
372b6376fc | ||
|
|
0c3bfc933c | ||
|
|
2414db1c61 | ||
|
|
008fe17035 | ||
|
|
2be95fdb51 | ||
|
|
e5842d5a49 | ||
|
|
9a59ca95a1 | ||
|
|
421f6f8b6d | ||
|
|
a5ee6b05e8 | ||
|
|
c9465d89f5 | ||
|
|
097a8eaf26 | ||
|
|
648b671210 | ||
|
|
75aeed9747 | ||
|
|
c87f7f39c7 | ||
|
|
32b10038d9 | ||
|
|
204cbcbed0 | ||
|
|
da5a04b3ba | ||
|
|
126a6dbbed | ||
|
|
ab43d4a6cf | ||
|
|
5f6e11f709 | ||
|
|
cf61abcf03 | ||
|
|
9549819571 | ||
|
|
95b85c13eb | ||
|
|
d46efe8410 | ||
|
|
501ecac4d5 | ||
|
|
0dce5bc9a6 | ||
|
|
9092c8b7a6 | ||
|
|
a5858495ef | ||
|
|
88984378e8 | ||
|
|
470c3d6c24 |
No files matched your search
@@ -1,32 +1,27 @@
|
||||
name: Bug Feedback
|
||||
name: Bug 反馈 | Bug Feedback
|
||||
description:
|
||||
反馈一个错误。
|
||||
为了提高反馈效率,请优先在 KOOK、赞助频道或者 Discord 频道中反馈。若确认是 HMCL 问题,请发布 issue。
|
||||
反馈一个 HMCL 错误。| Bug Feedback.
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
|
||||
提交前请确认:
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[KOOK 频道](https://kook.top/Kx7n3t)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) 下载最新快照版本。
|
||||
|
||||
我们发现,对于不经常检查邮箱的人来说,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
|
||||
id: hmcl-version
|
||||
attributes:
|
||||
label: 启动器版本 Launcher Version
|
||||
description: 请输入您所使用的的 HMCL 启动器版本。 Please enter the HMCL launcher version you are using.
|
||||
placeholder: e.g. 3.5.3.211
|
||||
validations:
|
||||
required: true
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[KOOK 频道](https://kook.top/Kx7n3t)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
|
||||
Before submitting, please confirm:
|
||||
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [KOOK channel](https://kook.top/Kx7n3t) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
* Your launcher version is **the latest snapshot version**. You can click [here](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) to download the latest snapshot version.
|
||||
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [KOOK channel](https://kook.top/Kx7n3t) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台 Platform
|
||||
label: 平台 | Platform
|
||||
description: 请输入您遇到 BUG 的平台。 Please enter the platform on which you encountered the bug.
|
||||
placeholder: e.g. Windows 11
|
||||
validations:
|
||||
@@ -34,41 +29,25 @@ body:
|
||||
- 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.
|
||||
label: 问题描述 | Problem Description
|
||||
description: 请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。 Please describe the problem you met in as much detail as possible. Besides, please describe how to trigger this problem again.
|
||||
placeholder: |
|
||||
1. 点击 HMCL 上的某个按钮 | Click a button named ...
|
||||
2. 向下翻页 | Scroll down
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: java-version
|
||||
attributes:
|
||||
label: Java
|
||||
description: 请输入您所使用的 Java 的名称与版本。 Please enter the name and version of Java you are using.
|
||||
placeholder: e.g. LibericaJDK Full JDK 17.0.1
|
||||
- type: input
|
||||
id: game-version
|
||||
attributes:
|
||||
label: 游戏版本 Game Version
|
||||
description: 如果您是在启动通过 HMCL 启动器自动下载的游戏时遇到了问题,请输入您所启动的游戏版本。If you are having trouble launching a game that was automatically downloaded through the HMCL launcher, please enter the version of the game you are launching.
|
||||
placeholder: e.g. 1.19
|
||||
- type: input
|
||||
id: game-modify
|
||||
attributes:
|
||||
label: 对游戏所做的修改 Modifications Made to the Game
|
||||
description: 如果您为游戏安装了 OptiFine、Forge,或者是其他的 mod,请将您所做的修改填入下方。 If you have OptiFine, Forge, or other mods installed for the game, please enter your modifications below.
|
||||
placeholder: e.g. OptiFine HD_U_G9, Rift
|
||||
- type: textarea
|
||||
id: game-crash-report
|
||||
id: hmcl-crash-report-or-logs
|
||||
attributes:
|
||||
label: 游戏崩溃报告 Game Crash Report
|
||||
description: 如果有游戏崩溃报告的话,请填入(或将文件拖入)下方。 If there is a game crash report, please fill in (or drag the file into) below.
|
||||
- type: textarea
|
||||
id: hmcl-crash-report
|
||||
attributes:
|
||||
label: 启动器崩溃报告 Launcher Crash Report
|
||||
description: 如果有启动器崩溃报告的话,请填入(或将文件拖入)下方。 If there is a launcher crash report, please fill it in (or drag the file in) below.
|
||||
- type: textarea
|
||||
id: hmcl-logs
|
||||
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.
|
||||
|
||||
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
|
||||
description: |
|
||||
如果您的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
|
||||
如果您的启动器没有崩溃,请在遇到问题后不要退出启动器,在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。
|
||||
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
|
||||
|
||||
If your launcher crashes, please fill it in (or drag the file in) below.
|
||||
If your launcher doesn't crash, please DO NOT EXIT your launcher, click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
|
||||
**Attention: The crash report or the log file is the key to solve the problem. Please update it!**
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,14 +1,14 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: KOOK 频道
|
||||
- name: KOOK 频道 | KOOK Channel
|
||||
url: https://kook.top/Kx7n3t
|
||||
about: Hello Minecraft! Launcher 的官方 KOOK 频道。
|
||||
- name: Discord 频道
|
||||
about: Hello Minecraft! Launcher 的官方 KOOK 频道。| The official KOOK channel of Hello Minecraft! Laucher.
|
||||
- name: Discord 频道 | Discord Channel
|
||||
url: https://discord.gg/jVvC7HfM6U
|
||||
about: Hello Minecraft! Launcher 的官方 Discord 频道。
|
||||
- name: 赞助通道
|
||||
url: https://afdian.net/@huanghongxun
|
||||
about: 前往爱发电赞助 HMCL。赞助后可以申请加入官方 QQ 群反馈问题。
|
||||
- name: 其他反馈
|
||||
url: https://github.com/huanghongxun/HMCL/discussions/new/choose
|
||||
about: 通过 Discussions 反馈其他问题。
|
||||
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
|
||||
- name: QQ 群 | QQ Groups
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
|
||||
- name: 其他反馈 | Others
|
||||
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
|
||||
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
|
||||
@@ -1,21 +1,22 @@
|
||||
name: "Feature Request"
|
||||
description: "为 HMCL 提出新功能"
|
||||
labels: feature request
|
||||
name: 新功能 | Feature Request
|
||||
description: 为 HMCL 提出新功能 | A new feature for HMCL
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "提交前请确保查看 issues 列表是否有重复的项目!"
|
||||
value: 请确认 Issues 列表无重复的项目。| Please make sure that no duplicated issues has already been delivered.
|
||||
- type: textarea
|
||||
id: "yml-2"
|
||||
id: description
|
||||
attributes:
|
||||
label: 描述 Description
|
||||
description: "详细描述你想加入的新功能"
|
||||
label: 描述 | Description
|
||||
description: 请详细描述你想加入的新功能。| Please describe the new feature detaily.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: "yml-3"
|
||||
id: reason
|
||||
attributes:
|
||||
label: 原因 Why
|
||||
description: "描述该功能带来的好处及原因"
|
||||
label: 原因 | Reason
|
||||
description: 请描述该功能带来的好处及原因。| Please describe why you want to add the feature into HMCL.
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Build HMCLauncher
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'HMCLauncher/**'
|
||||
- '.github/workflows/build-launcher.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'HMCLauncher/**'
|
||||
- '.github/workflows/build-launcher.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
with:
|
||||
msbuild-architecture: x86
|
||||
- name: Build HMCLauncher
|
||||
run: msbuild /p:Configuration=Release /t:Rebuild /verbosity:detailed .\HMCLauncher\
|
||||
- name: Copy HMCLauncher to assets
|
||||
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\src\main\resources\assets\HMCLauncher.exe
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: .\gradlew makeExecutables --no-daemon
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=$("${{ github.sha }}".SubString(0, 7))" >> $env:GITHUB_ENV
|
||||
- name: Copy HMCLauncher to libs
|
||||
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\build\libs\HMCLauncher.exe
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCLauncher-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs/*.exe
|
||||
@@ -1,21 +1,25 @@
|
||||
name: Check Style
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain
|
||||
run: ./gradlew checkstyleMain --no-daemon
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest
|
||||
run: ./gradlew checkstyleTest --no-daemon
|
||||
@@ -1,19 +1,23 @@
|
||||
name: Check Translations
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.properties'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.properties'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations
|
||||
run: ./gradlew checkTranslations --no-daemon
|
||||
@@ -5,12 +5,15 @@ on:
|
||||
schedule:
|
||||
- cron: '30 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
dev-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
@@ -36,20 +39,88 @@ jobs:
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.exe | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
stable-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
needs: dev-check-update
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
|
||||
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
|
||||
|
||||
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- name: Check for existing tags
|
||||
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.exe | $(cat HMCL-$HMCL_VERSION.exe.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.jar | $(cat HMCL-$HMCL_VERSION.jar.sha256) |" >> RELEASE_NOTE
|
||||
echo "| HMCL-$HMCL_VERSION.sh | $(cat HMCL-$HMCL_VERSION.sh.sha256) |" >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -57,11 +128,8 @@ jobs:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh.sha1
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
@@ -5,13 +5,13 @@ on:
|
||||
|
||||
jobs:
|
||||
run:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror Github to Gitee
|
||||
uses: Yikun/hub-mirror-action@v1.1
|
||||
- name: Mirror GitHub to Gitee
|
||||
uses: Yikun/hub-mirror-action@v1.3
|
||||
with:
|
||||
src: github/huanghongxun
|
||||
src: github/HMCL-dev
|
||||
dst: gitee/huanghongxun
|
||||
static_list: 'HMCL'
|
||||
force_update: true
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: Java CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'HMCLauncher/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -8,15 +13,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
run: ./gradlew build --no-daemon
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
@@ -24,7 +29,7 @@ jobs:
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
@@ -46,6 +46,9 @@ minecraft-exported-crash-info*
|
||||
# macos
|
||||
.DS_Store
|
||||
|
||||
# vscode
|
||||
.vscode/
|
||||
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
+10
-6
@@ -12,14 +12,15 @@ plugins {
|
||||
}
|
||||
|
||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
||||
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun" && System.getenv("GITHUB_BASE_REF").isNullOrEmpty())
|
||||
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
|
||||
.isNullOrEmpty())
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||
if (number != null) {
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7)
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
||||
val prefix = if (isOfficial) "dev" else "unofficial"
|
||||
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
@@ -38,7 +39,7 @@ dependencies {
|
||||
implementation("libs:JFoenix")
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray) = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
|
||||
fun createChecksum(file: File) {
|
||||
val algorithms = linkedMapOf(
|
||||
@@ -107,9 +108,11 @@ val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
exclude("**/package-info.class")
|
||||
exclude("META-INF/maven/**")
|
||||
|
||||
minimize {
|
||||
exclude(dependency("com.google.code.gson:.*:.*"))
|
||||
exclude(dependency("com.github.steveice10:.*:.*"))
|
||||
exclude(dependency("libs:JFoenix:.*"))
|
||||
}
|
||||
|
||||
@@ -136,7 +139,8 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
"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"
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach"
|
||||
).joinToString(" ")
|
||||
)
|
||||
|
||||
@@ -187,7 +191,7 @@ tasks.processResources {
|
||||
dependsOn(tasks["java11Classes"])
|
||||
|
||||
into("assets") {
|
||||
from(project.buildDir.resolve("openjfx-dependencies.json"))
|
||||
from(project.layout.buildDirectory.file("openjfx-dependencies.json"))
|
||||
}
|
||||
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -12,12 +12,13 @@ import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Translate;
|
||||
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class SkinCanvas extends Group {
|
||||
|
||||
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 Image ALEX = FXUtils.newBuiltinImage("/assets/img/skin/alex.png");
|
||||
public static final Image STEVE = FXUtils.newBuiltinImage("/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);
|
||||
|
||||
@@ -25,6 +25,9 @@ import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
@@ -50,8 +53,10 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -66,6 +71,8 @@ public final class Launcher extends Application {
|
||||
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
register();
|
||||
|
||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
@@ -121,6 +128,45 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static void register() {
|
||||
Skin.registerDefaultSkinLoader((type) -> {
|
||||
switch (type) {
|
||||
case ALEX:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/alex.png");
|
||||
case ARI:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/ari.png");
|
||||
case EFE:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/efe.png");
|
||||
case KAI:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/kai.png");
|
||||
case MAKENA:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/makena.png");
|
||||
case NOOR:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/noor.png");
|
||||
case STEVE:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/steve.png");
|
||||
case SUNNY:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/sunny.png");
|
||||
case ZURI:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/zuri.png");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
RemoteMod.registerEmptyRemoteMod(new RemoteMod("", "", i18n("mods.broken_dependency.title"), i18n("mods.broken_dependency.desc"), new ArrayList<>(), "", "/assets/img/icon@8x.png", new RemoteMod.IMod() {
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
@@ -143,7 +189,7 @@ public final class Launcher extends Application {
|
||||
|| configPath.contains("\\INetCache\\")
|
||||
|| configPath.contains("\\$Recycle.Bin\\")
|
||||
|| configPath.contains("\\recycler\\");
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
return configPath.startsWith("/tmp/")
|
||||
|| configPath.startsWith("/var/tmp/")
|
||||
|| configPath.startsWith("/var/cache/")
|
||||
@@ -230,15 +276,18 @@ public final class Launcher extends Application {
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
LOG.info("Current Directory: " + System.getProperty("user.dir"));
|
||||
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
LOG.info("HMCL Jar Path: " + JarUtils.thisJar().map(it -> it.toAbsolutePath().toString()).orElse("Not Found"));
|
||||
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
|
||||
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
|
||||
.filter(bean -> bean.getName().equals("Metaspace"))
|
||||
.findAny()
|
||||
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
|
||||
.orElse("Unknown"));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
|
||||
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
||||
}
|
||||
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.FractureiserDetector;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
@@ -56,8 +55,8 @@ public final class Main {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
|
||||
checkDirectoryPath();
|
||||
@@ -72,6 +71,7 @@ public final class Main {
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkJavaFX();
|
||||
verifyJavaFX();
|
||||
detectFractureiser();
|
||||
|
||||
Launcher.main(args);
|
||||
@@ -106,18 +106,26 @@ public final class Main {
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
if (Architecture.CURRENT_ARCH == Architecture.MIPS64EL
|
||||
|| Architecture.CURRENT_ARCH == Architecture.LOONGARCH64
|
||||
|| Architecture.CURRENT_ARCH == Architecture.LOONGARCH64_OW)
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible.loongson"));
|
||||
else
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if JavaFX exists but is incomplete
|
||||
*/
|
||||
private static void verifyJavaFX() {
|
||||
try {
|
||||
Class.forName("javafx.beans.binding.Binding"); // javafx.base
|
||||
Class.forName("javafx.stage.Stage"); // javafx.graphics
|
||||
Class.forName("javafx.scene.control.Skin"); // javafx.controls
|
||||
} catch (Exception e) {
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
@@ -155,7 +163,7 @@ public final class Main {
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
static void fixLetsEncrypt() {
|
||||
private static void fixLetsEncrypt() {
|
||||
try {
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
|
||||
@@ -37,11 +37,11 @@ public final class Metadata {
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
|
||||
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
|
||||
public static final String HELP_URL = "https://docs.hmcl.net";
|
||||
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
||||
public static final String PUBLISH_URL = "https://www.mcbbs.net/thread-142335-1-1.html";
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.html";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
@@ -53,7 +53,7 @@ public final class Metadata {
|
||||
static {
|
||||
String hmclHome = System.getProperty("hmcl.home");
|
||||
if (hmclHome == null) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||
if (StringUtils.isNotBlank(xdgData)) {
|
||||
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
|
||||
|
||||
@@ -20,10 +20,9 @@ package org.jackhuang.hmcl.countly;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@@ -40,7 +39,7 @@ public class Countly {
|
||||
pair("metrics", metrics),
|
||||
pair("device_id", deviceId),
|
||||
pair("timestamp", Long.toString(System.currentTimeMillis())),
|
||||
pair("tz", Integer.toString(TimeZone.getDefault().getOffset(new Date().getTime()) / 60000)),
|
||||
pair("tz", Integer.toString(getTimezoneOffset())),
|
||||
pair("hour", Integer.toString(currentHour())),
|
||||
pair("dow", Integer.toString(currentDayOfWeek())),
|
||||
pair("app_key", APP_KEY),
|
||||
@@ -50,7 +49,7 @@ public class Countly {
|
||||
}
|
||||
|
||||
private static int getTimezoneOffset() {
|
||||
return TimeZone.getDefault().getOffset(new Date().getTime()) / 60000;
|
||||
return ZonedDateTime.now().getOffset().getTotalSeconds() / 60;
|
||||
}
|
||||
|
||||
private static String getLocale() {
|
||||
|
||||
@@ -7,8 +7,8 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -29,6 +29,10 @@ public class CrashReport {
|
||||
nonFatal = false;
|
||||
}
|
||||
|
||||
public Throwable getThrowable() {
|
||||
return this.throwable;
|
||||
}
|
||||
|
||||
public CrashReport setNonFatal() {
|
||||
nonFatal = true;
|
||||
return this;
|
||||
@@ -38,6 +42,9 @@ public class CrashReport {
|
||||
if (!stackTrace.contains("org.jackhuang"))
|
||||
return false;
|
||||
|
||||
if (throwable instanceof VirtualMachineError)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,7 +68,7 @@ public class CrashReport {
|
||||
public String getDisplayText() {
|
||||
return "---- Hello Minecraft! Crash Report ----\n" +
|
||||
" Version: " + Metadata.VERSION + "\n" +
|
||||
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
|
||||
" Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + "\n" +
|
||||
" Thread: " + thread + "\n" +
|
||||
"\n Content: \n " +
|
||||
stackTrace + "\n\n" +
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -61,17 +62,43 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
|
||||
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
|
||||
|
||||
if (optionsFile.exists())
|
||||
if (optionsFile.exists()) {
|
||||
return;
|
||||
if (configFolder.isDirectory())
|
||||
if (findFiles(configFolder, "options.txt"))
|
||||
}
|
||||
|
||||
if (configFolder.isDirectory()) {
|
||||
if (findFiles(configFolder, "options.txt")) {
|
||||
return;
|
||||
try {
|
||||
// TODO: Dirty implementation here
|
||||
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
|
||||
FileUtils.writeText(optionsFile, "lang:zh_CN\nforceUnicodeFont:true\n");
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (I18n.getCurrentLocale().getLocale() != Locale.CHINA) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lang;
|
||||
/*
|
||||
1.0- :没有语言选项,遇到这些版本时不设置
|
||||
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
|
||||
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
|
||||
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
|
||||
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
|
||||
*/
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
|
||||
if (gameVersion.compareTo("1.1") < 0) {
|
||||
lang = null;
|
||||
} else if (gameVersion.compareTo("1.11") < 0) {
|
||||
lang = "zh_CN";
|
||||
} else {
|
||||
lang = "zh_cn";
|
||||
}
|
||||
|
||||
if (lang != null) {
|
||||
try {
|
||||
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,16 +43,18 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@@ -102,7 +104,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
public Stream<Version> getDisplayVersions() {
|
||||
return getVersions().stream()
|
||||
.filter(v -> !v.isHidden())
|
||||
.sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime())
|
||||
.sorted(Comparator.comparing((Version v) -> Lang.requireNonNullElse(v.getReleaseTime(), Instant.EPOCH))
|
||||
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
|
||||
// https://github.com/huanghongxun/HMCL/issues/938
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}
|
||||
|
||||
@@ -164,7 +166,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
Files.move(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
@@ -263,22 +265,43 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public Image getVersionIconImage(String id) {
|
||||
if (id == null || !isLoaded())
|
||||
return newImage("/assets/img/grass.png");
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
|
||||
VersionSetting vs = getLocalVersionSettingOrCreate(id);
|
||||
VersionIconType iconType = Optional.ofNullable(vs).map(VersionSetting::getVersionIcon).orElse(VersionIconType.DEFAULT);
|
||||
VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT;
|
||||
|
||||
if (iconType == VersionIconType.DEFAULT) {
|
||||
Version version = getVersion(id).resolve(this);
|
||||
File iconFile = getVersionIconFile(id);
|
||||
if (iconFile.exists())
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if (LibraryAnalyzer.isModded(this, version))
|
||||
return newImage("/assets/img/furnace.png");
|
||||
else
|
||||
return newImage("/assets/img/grass.png");
|
||||
if (iconFile.exists()) {
|
||||
try (InputStream inputStream = new FileInputStream(iconFile)) {
|
||||
return new Image(inputStream);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load version icon of " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (LibraryAnalyzer.isModded(this, version)) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
|
||||
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
return VersionIconType.FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
|
||||
return VersionIconType.CHICKEN.getIcon();
|
||||
else
|
||||
return VersionIconType.FURNACE.getIcon();
|
||||
}
|
||||
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
} else {
|
||||
return newImage(iconType.getResourceUrl());
|
||||
return iconType.getIcon();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null);
|
||||
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
|
||||
// reinstall libraries
|
||||
// libraries of Forge and OptiFine should be obtained by installation.
|
||||
|
||||
@@ -33,7 +33,10 @@ import org.jackhuang.hmcl.launch.*;
|
||||
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
@@ -42,6 +45,7 @@ import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
@@ -216,9 +220,10 @@ public final class LauncherHelper {
|
||||
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
||||
});
|
||||
}
|
||||
}).thenRunAsync(() -> {
|
||||
launchingLatch.await();
|
||||
}).withStage("launch.state.waiting_launching"))
|
||||
}).withFakeProgress(
|
||||
i18n("message.doing"),
|
||||
() -> launchingLatch.getCount() == 0, 6.95
|
||||
).withStage("launch.state.waiting_launching"))
|
||||
.withStagesHint(Lang.immutableListOf(
|
||||
"launch.state.java",
|
||||
"launch.state.dependencies",
|
||||
@@ -328,7 +333,8 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
|
||||
VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown"));
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
|
||||
|
||||
if (setting.isNotCheckJVM()) {
|
||||
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
|
||||
@@ -412,7 +418,6 @@ public final class LauncherHelper {
|
||||
JavaVersionConstraint violatedMandatoryConstraint = null;
|
||||
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
|
||||
@@ -506,10 +511,8 @@ public final class LauncherHelper {
|
||||
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)
|
||||
String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE).orElse(null);
|
||||
if (forgePatchVersion != null && VersionNumber.compare(forgePatchVersion, "37.0.60") < 0)
|
||||
suggestions.add(i18n("launch.advice.forge37_0_60"));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.modded_java", 16, gameVersion));
|
||||
@@ -547,14 +550,14 @@ public final class LauncherHelper {
|
||||
// Forge 2760~2773 will crash game with LiteLoader.
|
||||
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
|
||||
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
|
||||
}
|
||||
|
||||
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
||||
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
|
||||
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
|
||||
}
|
||||
|
||||
@@ -617,7 +620,7 @@ public final class LauncherHelper {
|
||||
/**
|
||||
* Directly start java downloading.
|
||||
*
|
||||
* @param javaVersion target Java version
|
||||
* @param javaVersion target Java version
|
||||
* @param downloadProvider download provider
|
||||
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
|
||||
*/
|
||||
@@ -745,7 +748,7 @@ public final class LauncherHelper {
|
||||
|
||||
if (showLogs)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
logWindow = new LogWindow(process);
|
||||
logWindow.showNormal();
|
||||
logWindowLatch.countDown();
|
||||
});
|
||||
@@ -831,6 +834,10 @@ public final class LauncherHelper {
|
||||
|
||||
@Override
|
||||
public void onExit(int exitCode, ExitType exitType) {
|
||||
if (showLogs) {
|
||||
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
}
|
||||
|
||||
launchingLatch.countDown();
|
||||
|
||||
if (exitType == ExitType.INTERRUPTED)
|
||||
|
||||
@@ -21,44 +21,87 @@ import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.ui.versions.ModTranslations;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
|
||||
private static final int CONTAIN_CHINESE_WEIGHT = 10;
|
||||
|
||||
private static final int INITIAL_CAPACITY = 16;
|
||||
|
||||
protected abstract RemoteModRepository getBackedRemoteModRepository();
|
||||
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
String newSearchFilter;
|
||||
if (StringUtils.containsChinese(searchFilter)) {
|
||||
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
|
||||
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
|
||||
List<String> searchFilters = new ArrayList<>();
|
||||
int count = 0;
|
||||
for (ModTranslations.Mod mod : mods) {
|
||||
String englishName = mod.getName();
|
||||
if (StringUtils.isNotBlank(mod.getSubname())) {
|
||||
englishName = mod.getSubname();
|
||||
}
|
||||
|
||||
searchFilters.add(englishName);
|
||||
|
||||
count++;
|
||||
if (count >= 3) break;
|
||||
}
|
||||
newSearchFilter = String.join(" ", searchFilters);
|
||||
} else {
|
||||
newSearchFilter = searchFilter;
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder);
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
|
||||
int count = 0;
|
||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
|
||||
for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) {
|
||||
if (englishSearchFiltersSet.contains(englishWord)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
englishSearchFiltersSet.add(englishWord);
|
||||
}
|
||||
|
||||
count++;
|
||||
if (count >= 3) break;
|
||||
}
|
||||
|
||||
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||
{
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.log(Level.WARNING, "Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoteMod remoteMod = iterator.next();
|
||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
|
||||
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
|
||||
searchResultArray[chineseIndex++] = remoteMod;
|
||||
} else {
|
||||
searchResultArray[englishIndex--] = remoteMod;
|
||||
}
|
||||
}
|
||||
totalPages = searchResult.getTotalPages();
|
||||
}
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
|
||||
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
|
||||
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
||||
}
|
||||
String chineseRemoteModName = chineseRemoteMod.getName();
|
||||
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
|
||||
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
|
||||
}
|
||||
int l = levCalculator.calc(searchFilter, chineseRemoteModName);
|
||||
for (int i = 0; i < searchFilter.length(); i++) {
|
||||
if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) {
|
||||
l -= CONTAIN_CHINESE_WEIGHT;
|
||||
}
|
||||
}
|
||||
return Pair.pair(remoteMod, l);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -65,37 +65,15 @@ public final class LogExporter {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
Path logsDir = runDirectory.resolve("logs");
|
||||
if (Files.exists(logsDir.resolve("debug.log"))) {
|
||||
zipper.putFile(logsDir.resolve("debug.log"), "debug.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");
|
||||
}
|
||||
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
|
||||
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
|
||||
processLogs(runDirectory, "*.log", "runDirectory", zipper);
|
||||
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
|
||||
|
||||
zipper.putTextFile(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)) {
|
||||
@@ -107,4 +85,26 @@ public final class LogExporter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
|
||||
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
|
||||
if (time.toMillis() >= processStartTime) {
|
||||
try {
|
||||
String crashLog = Logging.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
|
||||
zipper.putTextFile(crashLog, file.getFileName().toString());
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read log file: " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to find any log on " + logDirectory, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
@@ -29,9 +32,13 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
@@ -141,16 +148,7 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
Image skin;
|
||||
try {
|
||||
skin = new Image(path);
|
||||
if (skin.isError())
|
||||
throw skin.getException();
|
||||
} catch (Throwable e) {
|
||||
throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
|
||||
}
|
||||
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(skin, singletonMap("model", model.modelName)));
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(FXUtils.newBuiltinImage(path), singletonMap("model", model.modelName)));
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
@@ -188,27 +186,61 @@ public final class TexturesLoader {
|
||||
}, uuidFallback);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
|
||||
public static ObservableValue<LoadedTexture> skinBinding(Account account) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
|
||||
return BindingMapping.of(account.getTextures())
|
||||
.map(textures -> textures
|
||||
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
|
||||
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
|
||||
.asyncMap(it -> {
|
||||
if (it.isPresent()) {
|
||||
Texture texture = it.get();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
if (account instanceof OfflineAccount) {
|
||||
OfflineAccount offlineAccount = (OfflineAccount) account;
|
||||
|
||||
SimpleObjectProperty<LoadedTexture> binding = new SimpleObjectProperty<>();
|
||||
InvalidationListener listener = o -> {
|
||||
Skin skin = offlineAccount.getSkin();
|
||||
String username = offlineAccount.getUsername();
|
||||
|
||||
binding.set(uuidFallback);
|
||||
if (skin != null) {
|
||||
skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture", exception);
|
||||
} else if (result != null && result.getSkin() != null && result.getSkin().getImage() != null) {
|
||||
Map<String, String> metadata;
|
||||
if (result.getModel() != null) {
|
||||
metadata = singletonMap("model", result.getModel().modelName);
|
||||
} else {
|
||||
metadata = emptyMap();
|
||||
}
|
||||
}, POOL);
|
||||
} else {
|
||||
|
||||
binding.set(new LoadedTexture(result.getSkin().getImage(), metadata));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
};
|
||||
|
||||
listener.invalidated(offlineAccount);
|
||||
|
||||
binding.addListener(new Holder<>(listener));
|
||||
offlineAccount.addListener(new WeakInvalidationListener(listener));
|
||||
|
||||
return binding;
|
||||
} else {
|
||||
return BindingMapping.of(account.getTextures())
|
||||
.asyncMap(textures -> {
|
||||
if (textures.isPresent()) {
|
||||
Texture texture = textures.get().get(TextureType.SKIN);
|
||||
if (texture != null && StringUtils.isNotBlank(texture.getUrl())) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
}, POOL);
|
||||
}
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(uuidFallback);
|
||||
}
|
||||
}, uuidFallback);
|
||||
}, uuidFallback);
|
||||
}
|
||||
}
|
||||
|
||||
// ====
|
||||
@@ -277,9 +309,9 @@ public final class TexturesLoader {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
final WeakReference<Canvas> canvasRef;
|
||||
final ObjectBinding<LoadedTexture> binding;
|
||||
final ObservableValue<LoadedTexture> binding;
|
||||
|
||||
SkinBindingChangeListener(Canvas canvas, ObjectBinding<LoadedTexture> binding) {
|
||||
SkinBindingChangeListener(Canvas canvas, ObservableValue<LoadedTexture> binding) {
|
||||
this.canvasRef = new WeakReference<>(canvas);
|
||||
this.binding = binding;
|
||||
}
|
||||
@@ -293,14 +325,14 @@ public final class TexturesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<LoadedTexture> skinBinding) {
|
||||
public static void fxAvatarBinding(Canvas canvas, ObservableValue<LoadedTexture> skinBinding) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
|
||||
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
|
||||
listener.changed(skinBinding, null, skinBinding.get());
|
||||
listener.changed(skinBinding, null, skinBinding.getValue());
|
||||
skinBinding.addListener(listener);
|
||||
|
||||
SkinBindingChangeListener.hole.put(canvas, listener);
|
||||
@@ -312,7 +344,7 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, Account account) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount)
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof OfflineAccount)
|
||||
fxAvatarBinding(canvas, skinBinding(account));
|
||||
else {
|
||||
unbindAvatar(canvas);
|
||||
|
||||
@@ -33,8 +33,6 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
@@ -83,17 +81,15 @@ public final class Accounts {
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||
static {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("yggdrasil", FACTORY_MOJANG);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
type2factory.put("microsoft", FACTORY_MICROSOFT);
|
||||
|
||||
@@ -126,8 +122,6 @@ public final class Accounts {
|
||||
return FACTORY_OFFLINE;
|
||||
else if (account instanceof AuthlibInjectorAccount)
|
||||
return FACTORY_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return FACTORY_MOJANG;
|
||||
else if (account instanceof MicrosoftAccount)
|
||||
return FACTORY_MICROSOFT;
|
||||
else
|
||||
@@ -414,7 +408,6 @@ public final class Accounts {
|
||||
// ==== Login type name i18n ===
|
||||
private static final Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
||||
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
|
||||
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
|
||||
|
||||
|
||||
@@ -22,70 +22,76 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class AuthlibInjectorServers implements Validation {
|
||||
public final class AuthlibInjectorServers implements Validation {
|
||||
|
||||
public static final String CONFIG_FILENAME = "authlib-injectors.json";
|
||||
|
||||
private static final Set<AuthlibInjectorServer> servers = new CopyOnWriteArraySet<>();
|
||||
|
||||
public static Set<AuthlibInjectorServer> getServers() {
|
||||
return servers;
|
||||
}
|
||||
|
||||
private final List<String> urls;
|
||||
|
||||
public AuthlibInjectorServers(List<String> urls) {
|
||||
private AuthlibInjectorServers(List<String> urls) {
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
public List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (urls == null)
|
||||
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (this.urls == null) {
|
||||
throw new JsonParseException("authlib-injectors.json -> urls cannot be null.");
|
||||
}
|
||||
}
|
||||
|
||||
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
|
||||
private static AuthlibInjectorServers configInstance;
|
||||
|
||||
public synchronized static void init() {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
|
||||
public static void init() {
|
||||
Path configLocation;
|
||||
Path jarPath = JarUtils.thisJarPath();
|
||||
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
configLocation = jarPath.getParent().resolve(CONFIG_FILENAME);
|
||||
} else {
|
||||
configLocation = Paths.get(CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
configInstance = new AuthlibInjectorServers(Collections.emptyList());
|
||||
|
||||
if (Files.exists(configLocation)) {
|
||||
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
|
||||
AuthlibInjectorServers configInstance;
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configInstance.urls.isEmpty()) {
|
||||
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
|
||||
for (String url : configInstance.urls) {
|
||||
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
|
||||
.thenAcceptAsync(Schedulers.javafx(), server -> {
|
||||
config().getAuthlibInjectorServers().add(server);
|
||||
servers.add(server);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
|
||||
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
|
||||
for (String url : AuthlibInjectorServers.getConfigInstance().getUrls()) {
|
||||
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
|
||||
.thenAcceptAsync(Schedulers.javafx(), server -> config().getAuthlibInjectorServers().add(server))
|
||||
.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthlibInjectorServers getConfigInstance() {
|
||||
return configInstance;
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,12 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("proxyPassword")
|
||||
private StringProperty proxyPass = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("x")
|
||||
private DoubleProperty x = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("y")
|
||||
private DoubleProperty y = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("width")
|
||||
private DoubleProperty width = new SimpleDoubleProperty();
|
||||
|
||||
@@ -125,13 +131,13 @@ public final class Config implements Cloneable, Observable {
|
||||
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
||||
|
||||
@SerializedName("autoDownloadThreads")
|
||||
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true);
|
||||
|
||||
@SerializedName("downloadThreads")
|
||||
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
|
||||
|
||||
@SerializedName("downloadType")
|
||||
private StringProperty downloadType = new SimpleStringProperty("mcbbs");
|
||||
private StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID);
|
||||
|
||||
@SerializedName("autoChooseDownloadType")
|
||||
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
|
||||
@@ -158,7 +164,7 @@ public final class Config implements Cloneable, Observable {
|
||||
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("logLines")
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(100);
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
|
||||
|
||||
@SerializedName("titleTransparent")
|
||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
@@ -379,6 +385,30 @@ public final class Config implements Cloneable, Observable {
|
||||
return proxyPass;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x.get();
|
||||
}
|
||||
|
||||
public DoubleProperty xProperty() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
this.x.set(x);
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y.get();
|
||||
}
|
||||
|
||||
public DoubleProperty yProperty() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
this.y.set(y);
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width.get();
|
||||
}
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -74,7 +74,7 @@ public final class ConfigHolder {
|
||||
return ownerChanged;
|
||||
}
|
||||
|
||||
public synchronized static void init() throws IOException {
|
||||
public static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
}
|
||||
@@ -89,6 +89,8 @@ public final class ConfigHolder {
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
|
||||
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
I18n.setLocale(configInstance.getLocalization());
|
||||
Settings.init();
|
||||
|
||||
if (newlyCreated) {
|
||||
@@ -121,7 +123,7 @@ public final class ConfigHolder {
|
||||
private static Path locateConfig() {
|
||||
Path exePath = Paths.get("").toAbsolutePath();
|
||||
try {
|
||||
Path jarPath = JarUtils.thisJar().orElse(null);
|
||||
Path jarPath = JarUtils.thisJarPath();
|
||||
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
jarPath = jarPath.getParent();
|
||||
exePath = jarPath;
|
||||
@@ -167,8 +169,7 @@ public final class ConfigHolder {
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
|
||||
ConfigUpgrader.upgradeConfig(deserialized, raw);
|
||||
ConfigUpgrader.upgradeConfig(deserialized, content);
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
|
||||
@@ -17,96 +17,90 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
final class ConfigUpgrader {
|
||||
private static final int VERSION = 0;
|
||||
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
/**
|
||||
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
|
||||
* This method is for the compatibility with old HMCL versions.
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
* @return true if config version is upgraded
|
||||
* @param rawContent raw json content of the config settings without modification
|
||||
*/
|
||||
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
boolean upgraded;
|
||||
if (deserialized.getConfigVersion() < VERSION) {
|
||||
deserialized.setConfigVersion(VERSION);
|
||||
// TODO: Add upgrade code here.
|
||||
upgraded = true;
|
||||
} else {
|
||||
upgraded = false;
|
||||
static void upgradeConfig(Config deserialized, String rawContent) {
|
||||
int configVersion = deserialized.getConfigVersion();
|
||||
|
||||
if (configVersion == CURRENT_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeV2(deserialized, rawJson);
|
||||
upgradeV3(deserialized, rawJson);
|
||||
if (configVersion > CURRENT_VERSION) {
|
||||
LOG.log(Level.WARNING, String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
return upgraded;
|
||||
}
|
||||
LOG.log(Level.INFO, String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
|
||||
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
|
||||
|
||||
/**
|
||||
* Upgrade configuration of HMCL 2.x
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
|
||||
// Convert OfflineAccounts whose stored uuid is important.
|
||||
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
|
||||
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
|
||||
String selected = rawJson.containsKey("selectedAccount") ? null
|
||||
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
|
||||
if (configVersion < 1) {
|
||||
// Upgrade configuration of HMCL 2.x: Convert OfflineAccounts whose stored uuid is important.
|
||||
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
|
||||
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
|
||||
String selected = rawJson.containsKey("selectedAccount") ? null
|
||||
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
|
||||
|
||||
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
|
||||
((Map<?, ?>) uuidMap).forEach((key, value) -> {
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("type", "offline");
|
||||
storage.put("username", key);
|
||||
storage.put("uuid", value);
|
||||
if (key.equals(selected)) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
deserialized.getAccountStorages().add(storage);
|
||||
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
|
||||
((Map<?, ?>) uuidMap).forEach((key, value) -> {
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("type", "offline");
|
||||
storage.put("username", key);
|
||||
storage.put("uuid", value);
|
||||
if (key.equals(selected)) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
deserialized.getAccountStorages().add(storage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade configuration of HMCL earlier than 3.1.70
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
|
||||
if (!rawJson.containsKey("commonDirType"))
|
||||
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
|
||||
if (!rawJson.containsKey("backgroundType"))
|
||||
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
|
||||
if (!rawJson.containsKey("hasProxy"))
|
||||
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
|
||||
if (!rawJson.containsKey("hasProxyAuth"))
|
||||
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
|
||||
// Upgrade configuration of HMCL earlier than 3.1.70
|
||||
if (!rawJson.containsKey("commonDirType"))
|
||||
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
|
||||
if (!rawJson.containsKey("backgroundType"))
|
||||
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
|
||||
if (!rawJson.containsKey("hasProxy"))
|
||||
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
|
||||
if (!rawJson.containsKey("hasProxyAuth"))
|
||||
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
|
||||
|
||||
if (!rawJson.containsKey("downloadType")) {
|
||||
tryCast(rawJson.get("downloadtype"), Number.class)
|
||||
.map(Number::intValue)
|
||||
.ifPresent(id -> {
|
||||
if (id == 0) {
|
||||
deserialized.setDownloadType("mojang");
|
||||
} else if (id == 1) {
|
||||
deserialized.setDownloadType("bmclapi");
|
||||
}
|
||||
});
|
||||
if (!rawJson.containsKey("downloadType")) {
|
||||
tryCast(rawJson.get("downloadtype"), Number.class)
|
||||
.map(Number::intValue)
|
||||
.ifPresent(id -> {
|
||||
if (id == 0) {
|
||||
deserialized.setDownloadType("mojang");
|
||||
} else if (id == 1) {
|
||||
deserialized.setDownloadType("bmclapi");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
}
|
||||
}
|
||||
@@ -55,10 +55,9 @@ public final class DownloadProviders {
|
||||
|
||||
private static final MojangDownloadProvider MOJANG;
|
||||
private static final BMCLAPIDownloadProvider BMCLAPI;
|
||||
private static final BMCLAPIDownloadProvider MCBBS;
|
||||
|
||||
public static final String DEFAULT_PROVIDER_ID = "balanced";
|
||||
public static final String DEFAULT_RAW_PROVIDER_ID = "mcbbs";
|
||||
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
|
||||
|
||||
private static final InvalidationListener observer;
|
||||
|
||||
@@ -69,21 +68,19 @@ public final class DownloadProviders {
|
||||
|
||||
MOJANG = new MojangDownloadProvider();
|
||||
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
|
||||
MCBBS = new BMCLAPIDownloadProvider("https://download.mcbbs.net");
|
||||
rawProviders = mapOf(
|
||||
pair("mojang", MOJANG),
|
||||
pair("bmclapi", BMCLAPI),
|
||||
pair("mcbbs", MCBBS)
|
||||
pair("bmclapi", BMCLAPI)
|
||||
);
|
||||
|
||||
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
|
||||
fileProvider.setDownloadProviderCandidates(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
|
||||
BalancedDownloadProvider balanced = new BalancedDownloadProvider(Arrays.asList(MCBBS, BMCLAPI, MOJANG));
|
||||
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
|
||||
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
|
||||
|
||||
providersById = mapOf(
|
||||
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
|
||||
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
|
||||
pair("mirror", new AutoDownloadProvider(MCBBS, fileProvider)));
|
||||
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(
|
||||
@@ -102,12 +99,11 @@ public final class DownloadProviders {
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
if (!rawProviders.containsKey(downloadType)) {
|
||||
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
|
||||
return;
|
||||
}
|
||||
if (!rawProviders.containsKey(config().getDownloadType())) {
|
||||
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
|
||||
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
fileDownloadProvider.setDownloadProviderCandidates(
|
||||
|
||||
@@ -21,9 +21,7 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
@@ -33,7 +31,7 @@ import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||
public class GlobalConfig implements Cloneable, Observable {
|
||||
public final class GlobalConfig implements Cloneable, Observable {
|
||||
|
||||
@Nullable
|
||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||
@@ -47,17 +45,13 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
|
||||
|
||||
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public GlobalConfig() {
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
@@ -106,37 +100,10 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
this.platformPromptVersion.set(platformPromptVersion);
|
||||
}
|
||||
|
||||
public boolean isMultiplayerRelay() {
|
||||
return multiplayerRelay.get();
|
||||
}
|
||||
|
||||
public BooleanProperty multiplayerRelayProperty() {
|
||||
return multiplayerRelay;
|
||||
}
|
||||
|
||||
public void setMultiplayerRelay(boolean multiplayerRelay) {
|
||||
this.multiplayerRelay.set(multiplayerRelay);
|
||||
}
|
||||
|
||||
public int getMultiplayerAgreementVersion() {
|
||||
return multiplayerAgreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty multiplayerAgreementVersionProperty() {
|
||||
return multiplayerAgreementVersion;
|
||||
}
|
||||
|
||||
public void setMultiplayerAgreementVersion(int multiplayerAgreementVersion) {
|
||||
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"platformPromptVersion",
|
||||
"multiplayerToken",
|
||||
"multiplayerRelay",
|
||||
"multiplayerAgreementVersion"
|
||||
"platformPromptVersion"
|
||||
));
|
||||
|
||||
@Override
|
||||
@@ -148,8 +115,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
|
||||
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
|
||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
||||
}
|
||||
@@ -166,8 +131,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
|
||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
||||
if (!knownFields.contains(entry.getKey())) {
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class HMCLAccounts {
|
||||
|
||||
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
||||
|
||||
private HMCLAccounts() {
|
||||
}
|
||||
|
||||
public static HMCLAccount getAccount() {
|
||||
return account.get();
|
||||
}
|
||||
|
||||
public static ObjectProperty<HMCLAccount> accountProperty() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public static void setAccount(HMCLAccount account) {
|
||||
HMCLAccounts.account.set(account);
|
||||
}
|
||||
|
||||
public static Task<Void> login() {
|
||||
String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
String scope = "openid offline_access";
|
||||
|
||||
return Task.supplyAsync(() -> {
|
||||
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
|
||||
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
mapOf(
|
||||
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
|
||||
pair("response_type", "id_token code"),
|
||||
pair("response_mode", "form_post"),
|
||||
pair("scope", scope),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("nonce", nonce)
|
||||
)));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
|
||||
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
|
||||
.getString();
|
||||
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
OAuth.AuthorizationResponse.class);
|
||||
|
||||
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
|
||||
.header("Token-Type", response.tokenType)
|
||||
.header("Access-Token", response.accessToken)
|
||||
.header("Authorization-Provider", "microsoft")
|
||||
.authorization("Bearer", session.getIdToken())
|
||||
.getJson(HMCLAccountProfile.class);
|
||||
|
||||
return new HMCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken);
|
||||
}).thenAcceptAsync(Schedulers.javafx(), account -> {
|
||||
setAccount(account);
|
||||
});
|
||||
}
|
||||
|
||||
public static class HMCLAccount implements HttpRequest.Authorization {
|
||||
private final String provider;
|
||||
private final String nickname;
|
||||
private final String email;
|
||||
private final String role;
|
||||
private final String idToken;
|
||||
private final String tokenType;
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
public HMCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) {
|
||||
this.provider = provider;
|
||||
this.nickname = nickname;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
this.idToken = idToken;
|
||||
this.tokenType = tokenType;
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public String getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class HMCLAccountProfile {
|
||||
@SerializedName("ID")
|
||||
String id;
|
||||
@SerializedName("Provider")
|
||||
String provider;
|
||||
@SerializedName("Email")
|
||||
String email;
|
||||
@SerializedName("NickName")
|
||||
String nickname;
|
||||
@SerializedName("Role")
|
||||
String role;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,8 +24,6 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
public final class Settings {
|
||||
@@ -47,9 +45,6 @@ public final class Settings {
|
||||
}
|
||||
|
||||
private Settings() {
|
||||
config().localizationProperty().addListener(unused -> updateSystemLocale());
|
||||
updateSystemLocale();
|
||||
|
||||
DownloadProviders.init();
|
||||
ProxyManager.init();
|
||||
Accounts.init();
|
||||
@@ -81,8 +76,4 @@ public final class Settings {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateSystemLocale() {
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
}
|
||||
}
|
||||
@@ -192,8 +192,6 @@ public class Theme {
|
||||
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() {
|
||||
@@ -206,12 +204,12 @@ public class Theme {
|
||||
return FOREGROUND_FILL;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> blackFillBinding() {
|
||||
return BLACK_FILL;
|
||||
public static Color blackFill() {
|
||||
return BLACK;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> whiteFillBinding() {
|
||||
return WHITE_FILL;
|
||||
public static Color whiteFill() {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
DEFAULT("/assets/img/grass.png"),
|
||||
|
||||
@@ -24,9 +27,11 @@ public enum VersionIconType {
|
||||
CHEST("/assets/img/chest.png"),
|
||||
CHICKEN("/assets/img/chicken.png"),
|
||||
COMMAND("/assets/img/command.png"),
|
||||
OPTIFINE("/assets/img/optifine.png"),
|
||||
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||
FABRIC("/assets/img/fabric.png"),
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
|
||||
@@ -38,7 +43,7 @@ public enum VersionIconType {
|
||||
this.resourceUrl = resourceUrl;
|
||||
}
|
||||
|
||||
public String getResourceUrl() {
|
||||
return resourceUrl;
|
||||
public Image getIcon() {
|
||||
return FXUtils.newBuiltinImage(resourceUrl);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
@@ -644,11 +644,11 @@ public final class VersionSetting implements Cloneable {
|
||||
launcherVisibilityProperty.set(launcherVisibility);
|
||||
}
|
||||
|
||||
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version) {
|
||||
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version) {
|
||||
return getJavaVersion(gameVersion, version, true);
|
||||
}
|
||||
|
||||
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) {
|
||||
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version, boolean checkJava) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
if (StringUtils.isBlank(getJava())) {
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom");
|
||||
@@ -844,6 +844,7 @@ public final class VersionSetting implements Cloneable {
|
||||
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
|
||||
vs.setJavaDir(Optional.ofNullable(obj.get("javaDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
|
||||
|
||||
@@ -24,21 +24,20 @@ import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.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.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
@@ -51,10 +50,7 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
@@ -65,11 +61,15 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
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 {
|
||||
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
|
||||
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
|
||||
public static final Screen SCREEN = Screen.getPrimary();
|
||||
private static InvalidationListener stageSizeChangeListener;
|
||||
private static DoubleProperty stageX = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageY = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
|
||||
|
||||
@@ -146,6 +146,14 @@ public final class Controllers {
|
||||
|
||||
public static void onApplicationStop() {
|
||||
stageSizeChangeListener = null;
|
||||
if (stageX != null) {
|
||||
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
|
||||
stageX = null;
|
||||
}
|
||||
if (stageY != null) {
|
||||
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
|
||||
stageY = null;
|
||||
}
|
||||
if (stageHeight != null) {
|
||||
config().setHeight(stageHeight.get());
|
||||
stageHeight = null;
|
||||
@@ -163,7 +171,28 @@ public final class Controllers {
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
|
||||
DoubleProperty targetProperty = "width".equals(sourceProperty.getName()) ? stageWidth : stageHeight;
|
||||
DoubleProperty targetProperty;
|
||||
switch (sourceProperty.getName()) {
|
||||
case "x": {
|
||||
targetProperty = stageX;
|
||||
break;
|
||||
}
|
||||
case "y": {
|
||||
targetProperty = stageY;
|
||||
break;
|
||||
}
|
||||
case "width": {
|
||||
targetProperty = stageWidth;
|
||||
break;
|
||||
}
|
||||
case "height": {
|
||||
targetProperty = stageHeight;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
targetProperty = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
@@ -174,13 +203,41 @@ public final class Controllers {
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
|
||||
|
||||
double initHeight = config().getHeight();
|
||||
double initWidth = config().getWidth();
|
||||
double initWidth = Math.max(MIN_WIDTH, config().getWidth());
|
||||
double initHeight = Math.max(MIN_HEIGHT, config().getHeight());
|
||||
|
||||
{
|
||||
double initX = config().getX() * SCREEN.getBounds().getWidth();
|
||||
double initY = config().getY() * SCREEN.getBounds().getHeight();
|
||||
|
||||
boolean invalid = true;
|
||||
double border = 20D;
|
||||
for (Screen screen : Screen.getScreens()) {
|
||||
Rectangle2D bound = screen.getBounds();
|
||||
|
||||
if (bound.getMinX() + border <= initX + initWidth && initX <= bound.getMaxX() - border && bound.getMinY() + border <= initY && initY <= bound.getMaxY() - border) {
|
||||
invalid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
initX = (0.5D - initWidth / SCREEN.getBounds().getWidth() / 2) * SCREEN.getBounds().getWidth();
|
||||
initY = (0.5D - initHeight / SCREEN.getBounds().getHeight() / 2) * SCREEN.getBounds().getHeight();
|
||||
}
|
||||
|
||||
stage.setX(initX);
|
||||
stage.setY(initY);
|
||||
stageX.set(initX);
|
||||
stageY.set(initY);
|
||||
}
|
||||
|
||||
stage.setHeight(initHeight);
|
||||
stage.setWidth(initWidth);
|
||||
stageHeight.set(initHeight);
|
||||
stageWidth.set(initWidth);
|
||||
stage.xProperty().addListener(weakListener);
|
||||
stage.yProperty().addListener(weakListener);
|
||||
stage.heightProperty().addListener(weakListener);
|
||||
stage.widthProperty().addListener(weakListener);
|
||||
|
||||
@@ -198,13 +255,13 @@ public final class Controllers {
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
stage.setMinHeight(450 + 2 + 40 + 16); // bg height + border width*2 + toolbar height + shadow width*2
|
||||
stage.setMinWidth(800 + 2 + 16); // bg width + border width*2 + shadow width*2
|
||||
stage.setMinWidth(MIN_WIDTH);
|
||||
stage.setMinHeight(MIN_HEIGHT);
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
|
||||
stage.getIcons().add(newImage("/assets/img/icon.png"));
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
@@ -216,6 +273,11 @@ public final class Controllers {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.osx_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
|
||||
(Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64
|
||||
|| Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW
|
||||
|| Architecture.SYSTEM_ARCH == Architecture.MIPS64EL)) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.loongarch"), null, MessageType.INFO, continueAction);
|
||||
} else {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform"), null, MessageType.WARNING, continueAction);
|
||||
}
|
||||
@@ -352,5 +414,7 @@ public final class Controllers {
|
||||
stage = null;
|
||||
scene = null;
|
||||
onApplicationStop();
|
||||
|
||||
FXUtils.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,9 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.countly.CrashReport;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
@@ -37,16 +37,18 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class CrashWindow extends Stage {
|
||||
|
||||
public CrashWindow(String text) {
|
||||
public CrashWindow(CrashReport report) {
|
||||
Label lblCrash = new Label();
|
||||
if (UpdateChecker.isOutdated())
|
||||
lblCrash.setText(i18n("launcher.crash_out_dated"));
|
||||
if (report.getThrowable() instanceof InternalError)
|
||||
lblCrash.setText(i18n("launcher.crash.java_internal_error"));
|
||||
else if (UpdateChecker.isOutdated())
|
||||
lblCrash.setText(i18n("launcher.crash.hmcl_out_dated"));
|
||||
else
|
||||
lblCrash.setText(i18n("launcher.crash"));
|
||||
lblCrash.setWrapText(true);
|
||||
|
||||
TextArea textArea = new TextArea();
|
||||
textArea.setText(text);
|
||||
textArea.setText(report.getDisplayText());
|
||||
textArea.setEditable(false);
|
||||
|
||||
Button btnContact = new Button();
|
||||
@@ -67,7 +69,7 @@ public class CrashWindow extends Stage {
|
||||
|
||||
Scene scene = new Scene(pane, 800, 480);
|
||||
setScene(scene);
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
FXUtils.setIcon(this);
|
||||
setTitle(i18n("message.error"));
|
||||
|
||||
setOnCloseRequest(e -> System.exit(1));
|
||||
|
||||
@@ -31,10 +31,10 @@ import javafx.beans.value.WritableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -43,10 +43,15 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.glavo.png.PNGType;
|
||||
import org.glavo.png.PNGWriter;
|
||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
@@ -74,11 +79,12 @@ import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
@@ -94,7 +100,23 @@ public final class FXUtils {
|
||||
private FXUtils() {
|
||||
}
|
||||
|
||||
public static String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
|
||||
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
|
||||
try {
|
||||
Files.deleteIfExists(entry.getValue());
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, String.format("Failed to delete cache file %s.", entry.getValue()), e);
|
||||
}
|
||||
remoteImageCache.remove(entry.getKey());
|
||||
}
|
||||
|
||||
builtinImageCache.clear();
|
||||
}
|
||||
|
||||
public static void runInFX(Runnable runnable) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
@@ -359,7 +381,7 @@ public final class FXUtils {
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && new File("/usr/bin/xdg-open").exists())
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
else
|
||||
openCommand = null;
|
||||
@@ -449,7 +471,8 @@ public final class FXUtils {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
|
||||
}
|
||||
} if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
@@ -655,6 +678,16 @@ public final class FXUtils {
|
||||
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
|
||||
}
|
||||
|
||||
public static void setIcon(Stage stage) {
|
||||
String icon;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
icon = "/assets/img/icon.png";
|
||||
} else {
|
||||
icon = "/assets/img/icon@4x.png";
|
||||
}
|
||||
stage.getIcons().add(newBuiltinImage(icon));
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
|
||||
*
|
||||
@@ -663,14 +696,113 @@ public final class FXUtils {
|
||||
* @see org.jackhuang.hmcl.util.CrashReporter
|
||||
* @see ResourceNotFoundError
|
||||
*/
|
||||
public static Image newImage(String url) {
|
||||
public static Image newBuiltinImage(String url) {
|
||||
try {
|
||||
return new Image(url);
|
||||
return builtinImageCache.computeIfAbsent(url, Image::new);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResourceNotFoundError("Cannot access image: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file within the jar.
|
||||
* @param requestedWidth the image's bounding box width
|
||||
* @param requestedHeight the image's bounding box height
|
||||
* @param preserveRatio indicates whether to preserve the aspect ratio of
|
||||
* the original image when scaling to fit the image within the
|
||||
* specified bounding box
|
||||
* @param smooth indicates whether to use a better quality filtering
|
||||
* algorithm or a faster one when scaling this image to fit within
|
||||
* the specified bounding box
|
||||
* @return the image resource within the jar.
|
||||
* @see org.jackhuang.hmcl.util.CrashReporter
|
||||
* @see ResourceNotFoundError
|
||||
*/
|
||||
public static Image newBuiltinImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
try {
|
||||
return new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResourceNotFoundError("Cannot access image: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url) {
|
||||
return newRemoteImage(url, 0, 0, false, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @param requestedWidth the image's bounding box width
|
||||
* @param requestedHeight the image's bounding box height
|
||||
* @param preserveRatio indicates whether to preserve the aspect ratio of
|
||||
* the original image when scaling to fit the image within the
|
||||
* specified bounding box
|
||||
* @param smooth indicates whether to use a better quality filtering
|
||||
* algorithm or a faster one when scaling this image to fit within
|
||||
* the specified bounding box
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
|
||||
Path currentPath = remoteImageCache.get(url);
|
||||
if (currentPath != null) {
|
||||
if (Files.isReadable(currentPath)) {
|
||||
try (InputStream inputStream = Files.newInputStream(currentPath)) {
|
||||
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "An exception encountered while reading data from cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// The file is unavailable or unreadable.
|
||||
remoteImageCache.remove(url);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(currentPath);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "An exception encountered while deleting broken cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
|
||||
image.progressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
|
||||
Task.runAsync(() -> {
|
||||
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
|
||||
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
|
||||
OutputStream outputStream = Files.newOutputStream(newPath);
|
||||
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
|
||||
) {
|
||||
writer.write(PNGJavaFXUtils.asArgbImage(image));
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.delete(newPath);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
throw e2;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
|
||||
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
return image;
|
||||
}
|
||||
|
||||
public static JFXButton newRaisedButton(String text) {
|
||||
JFXButton button = new JFXButton(text);
|
||||
button.getStyleClass().add("jfx-button-raised");
|
||||
@@ -678,6 +810,13 @@ public final class FXUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static JFXButton newBorderButton(String text) {
|
||||
JFXButton button = new JFXButton(text);
|
||||
button.getStyleClass().add("jfx-button-border");
|
||||
button.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
applyDragListener(node, filter, callback, null);
|
||||
}
|
||||
@@ -766,13 +905,6 @@ public final class FXUtils {
|
||||
}
|
||||
};
|
||||
|
||||
public static Runnable withJFXPopupClosing(Runnable runnable, JFXPopup popup) {
|
||||
return () -> {
|
||||
runnable.run();
|
||||
popup.hide();
|
||||
};
|
||||
}
|
||||
|
||||
public static void onEscPressed(Node node, Runnable action) {
|
||||
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
@@ -787,7 +919,9 @@ public final class FXUtils {
|
||||
content.putString(text);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
if (!Controllers.isStopped()) {
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
@@ -810,7 +944,14 @@ public final class FXUtils {
|
||||
if ("a".equals(element.getTagName())) {
|
||||
String href = element.getAttribute("href");
|
||||
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
|
||||
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
|
||||
hyperlink.setOnAction(e -> {
|
||||
String link = href;
|
||||
try {
|
||||
link = new URI(href).toASCIIString();
|
||||
} catch (URISyntaxException ignored) {
|
||||
}
|
||||
hyperlinkAction.accept(link);
|
||||
});
|
||||
texts.add(hyperlink);
|
||||
} else if ("b".equals(element.getTagName())) {
|
||||
Text text = new Text(element.getTextContent());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2023 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -41,22 +41,24 @@ import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
@@ -65,7 +67,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -97,11 +99,11 @@ public class GameCrashWindow extends Stage {
|
||||
this.version = version;
|
||||
this.launchOptions = launchOptions;
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version);
|
||||
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
|
||||
total_memory = Optional.ofNullable(OperatingSystem.TOTAL_MEMORY).map(i -> i + " MB").orElse("-");
|
||||
total_memory = OperatingSystem.TOTAL_MEMORY + " MB";
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
@@ -114,16 +116,18 @@ public class GameCrashWindow extends Stage {
|
||||
setScene(new Scene(view, 800, 480));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("game.crash.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
analyzeCrashReport();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void analyzeCrashReport() {
|
||||
loading.set(true);
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
Task.allOf(Task.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
|
||||
Set<String> keywords = Collections.emptySet();
|
||||
|
||||
// Get the crash-report from the crash-reports/xxx, or the output of console.
|
||||
String crashReport = null;
|
||||
try {
|
||||
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
@@ -133,67 +137,99 @@ public class GameCrashWindow extends Stage {
|
||||
if (crashReport == null) {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
|
||||
return pair(CrashReportAnalyzer.anaylze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
return pair(
|
||||
CrashReportAnalyzer.anaylze(rawLog),
|
||||
keywords);
|
||||
}).whenCompleteAsync((pair, exception) -> {
|
||||
|
||||
String log;
|
||||
try {
|
||||
log = FileUtils.readText(latestLog);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read logs/latest.log", e);
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
|
||||
return pair(CrashReportAnalyzer.anaylze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
|
||||
})).whenComplete(Schedulers.javafx(), (taskResult, exception) -> {
|
||||
loading.set(false);
|
||||
|
||||
if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
|
||||
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
|
||||
} else {
|
||||
List<CrashReportAnalyzer.Result> results = pair.getKey();
|
||||
Set<String> keywords = pair.getValue();
|
||||
EnumMap<CrashReportAnalyzer.Rule, CrashReportAnalyzer.Result> results = new EnumMap<>(CrashReportAnalyzer.Rule.class);
|
||||
Set<String> keywords = new HashSet<>();
|
||||
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
|
||||
for (CrashReportAnalyzer.Result result : pair.getKey()) {
|
||||
results.put(result.getRule(), result);
|
||||
}
|
||||
keywords.addAll(pair.getValue());
|
||||
}
|
||||
|
||||
List<Node> segments = new ArrayList<>();
|
||||
for (CrashReportAnalyzer.Result result : results) {
|
||||
List<Node> segments = new ArrayList<>(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
|
||||
|
||||
LOG.log(Level.INFO, "Number of reasons: " + results.size());
|
||||
if (results.size() > 1) {
|
||||
segments.add(new Text("\n"));
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.multiple"), Controllers::onHyperlinkAction));
|
||||
} else {
|
||||
segments.add(new Text("\n\n"));
|
||||
}
|
||||
|
||||
for (CrashReportAnalyzer.Result result : results.values()) {
|
||||
String message;
|
||||
switch (result.getRule()) {
|
||||
case TOO_OLD_JAVA:
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.too_old_java",
|
||||
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))), Controllers::onHyperlinkAction));
|
||||
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
|
||||
break;
|
||||
case MOD_RESOLUTION_CONFLICT:
|
||||
case MOD_RESOLUTION_MISSING:
|
||||
case MOD_RESOLUTION_COLLECTION:
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("sourcemod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod"))), Controllers::onHyperlinkAction));
|
||||
parseFabricModId(result.getMatcher().group("destmod")));
|
||||
break;
|
||||
case MOD_RESOLUTION_MISSING_MINECRAFT:
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version")), Controllers::onHyperlinkAction));
|
||||
result.getMatcher().group("version"));
|
||||
break;
|
||||
case MOD_FOREST_OPTIFINE:
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.mod", "OptiFine"), Controllers::onHyperlinkAction));
|
||||
case PERFORMANT_FOREST_OPTIFINE:
|
||||
case JADE_FOREST_OPTIFINE:
|
||||
message = i18n("game.crash.reason.mod", "OptiFine");
|
||||
LOG.log(Level.INFO, "Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
.toArray()), Controllers::onHyperlinkAction));
|
||||
.toArray());
|
||||
break;
|
||||
}
|
||||
segments.add(new Text("\n"));
|
||||
LOG.log(Level.INFO, "Crash cause: " + result.getRule() + ": " + message);
|
||||
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
|
||||
segments.add(new Text("\n\n"));
|
||||
}
|
||||
if (results.isEmpty()) {
|
||||
if (!keywords.isEmpty()) {
|
||||
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.stacktrace", String.join(", ", keywords))));
|
||||
LOG.log(Level.INFO, "Crash reason unknown, but some log keywords have been found: " + String.join(", ", keywords));
|
||||
} else {
|
||||
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
|
||||
LOG.log(Level.INFO, "Crash reason unknown");
|
||||
}
|
||||
|
||||
feedbackTextFlow.setVisible(true);
|
||||
} else {
|
||||
feedbackTextFlow.setVisible(false);
|
||||
reasonTextFlow.getChildren().setAll(segments);
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?<modid>.*?) @ (?<version>.*?)}");
|
||||
@@ -226,10 +262,11 @@ public class GameCrashWindow extends Stage {
|
||||
}
|
||||
|
||||
private void showLogWindow() {
|
||||
LogWindow logWindow = new LogWindow();
|
||||
LogWindow logWindow = new LogWindow(managedProcess);
|
||||
|
||||
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
|
||||
if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
if (managedProcess.getClasspath() != null)
|
||||
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
|
||||
@@ -243,15 +280,20 @@ public class GameCrashWindow extends Stage {
|
||||
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.thenRunAsync(() -> {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
.handleAsync((result, exception) -> {
|
||||
Alert alert;
|
||||
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to export game crash info", exception);
|
||||
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
|
||||
}
|
||||
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
}, Schedulers.javafx())
|
||||
.exceptionally(e -> {
|
||||
LOG.log(Level.WARNING, "Failed to export game crash info", e);
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@@ -273,6 +315,9 @@ public class GameCrashWindow extends Stage {
|
||||
case APPLICATION_ERROR:
|
||||
title.setText(i18n("launch.failed.exited_abnormally"));
|
||||
break;
|
||||
case SIGKILL:
|
||||
title.setText(i18n("launch.failed.sigkill"));
|
||||
break;
|
||||
}
|
||||
|
||||
titlePane.setAlignment(Pos.CENTER);
|
||||
@@ -347,11 +392,13 @@ public class GameCrashWindow extends Stage {
|
||||
gameDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
gameDir.setTitle(i18n("game.directory"));
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
|
||||
runInFX(() -> FXUtils.installFastTooltip(gameDir, i18n("game.directory")));
|
||||
|
||||
TwoLineListItem javaDir = new TwoLineListItem();
|
||||
javaDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
javaDir.setTitle(i18n("settings.game.java_directory"));
|
||||
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
|
||||
runInFX(() -> FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory")));
|
||||
|
||||
Label reasonTitle = new Label(i18n("game.crash.reason"));
|
||||
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
|
||||
@@ -380,10 +427,15 @@ public class GameCrashWindow extends Stage {
|
||||
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
|
||||
logButton.setOnMouseClicked(e -> showLogWindow());
|
||||
|
||||
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
|
||||
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
|
||||
runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help")));
|
||||
|
||||
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton);
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -30,14 +31,20 @@ import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -47,7 +54,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class InstallerItem extends Control {
|
||||
private final String id;
|
||||
private final String imageUrl;
|
||||
private final VersionIconType iconType;
|
||||
public final StringProperty libraryVersion = new SimpleStringProperty();
|
||||
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
|
||||
public final StringProperty dependencyName = new SimpleStringProperty();
|
||||
@@ -74,27 +81,30 @@ public class InstallerItem extends Control {
|
||||
|
||||
switch (id) {
|
||||
case "game":
|
||||
imageUrl = "/assets/img/grass.png";
|
||||
iconType = VersionIconType.GRASS;
|
||||
break;
|
||||
case "fabric":
|
||||
case "fabric-api":
|
||||
imageUrl = "/assets/img/fabric.png";
|
||||
iconType = VersionIconType.FABRIC;
|
||||
break;
|
||||
case "forge":
|
||||
imageUrl = "/assets/img/forge.png";
|
||||
iconType = VersionIconType.FORGE;
|
||||
break;
|
||||
case "liteloader":
|
||||
imageUrl = "/assets/img/chicken.png";
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
break;
|
||||
case "optifine":
|
||||
imageUrl = "/assets/img/command.png";
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
break;
|
||||
case "quilt":
|
||||
case "quilt-api":
|
||||
imageUrl = "/assets/img/quilt.png";
|
||||
iconType = VersionIconType.QUILT;
|
||||
break;
|
||||
case "neoforge":
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
break;
|
||||
default:
|
||||
imageUrl = null;
|
||||
iconType = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -118,44 +128,68 @@ public class InstallerItem extends Control {
|
||||
return new InstallerItemSkin(this);
|
||||
}
|
||||
|
||||
public static class InstallerItemGroup {
|
||||
public final static class InstallerItemGroup {
|
||||
public final InstallerItem game = new InstallerItem(MINECRAFT);
|
||||
public final InstallerItem fabric = new InstallerItem(FABRIC);
|
||||
public final InstallerItem fabricApi = new InstallerItem(FABRIC_API);
|
||||
public final InstallerItem forge = new InstallerItem(FORGE);
|
||||
public final InstallerItem neoForge = new InstallerItem(NEO_FORGE);
|
||||
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
|
||||
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
|
||||
public final InstallerItem quilt = new InstallerItem(QUILT);
|
||||
public final InstallerItem quiltApi = new InstallerItem(QUILT_API);
|
||||
|
||||
public InstallerItemGroup() {
|
||||
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion, quilt.libraryVersion));
|
||||
private final InstallerItem[] libraries;
|
||||
|
||||
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion, quilt.libraryVersion));
|
||||
private final HashMap<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
|
||||
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion, quilt.libraryVersion));
|
||||
private Set<InstallerItem> getIncompatibles(InstallerItem item) {
|
||||
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
|
||||
}
|
||||
|
||||
for (InstallerItem fabric : new InstallerItem[]{fabric, fabricApi}) {
|
||||
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
|
||||
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
|
||||
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
|
||||
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
|
||||
if (quiltApi.libraryVersion.get() != null) return QUILT_API.getPatchId();
|
||||
return null;
|
||||
}, forge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion, quilt.libraryVersion, quiltApi.libraryVersion));
|
||||
private void addIncompatibles(InstallerItem item, InstallerItem... others) {
|
||||
Set<InstallerItem> set = getIncompatibles(item);
|
||||
for (InstallerItem other : others) {
|
||||
set.add(other);
|
||||
getIncompatibles(other).add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void mutualIncompatible(InstallerItem... items) {
|
||||
for (InstallerItem item : items) {
|
||||
Set<InstallerItem> set = getIncompatibles(item);
|
||||
|
||||
for (InstallerItem item2 : items) {
|
||||
if (item2 != item) {
|
||||
set.add(item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItemGroup(String gameVersion) {
|
||||
mutualIncompatible(forge, fabric, quilt, neoForge, liteLoader);
|
||||
addIncompatibles(optiFine, fabric, quilt, neoForge);
|
||||
addIncompatibles(fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
|
||||
addIncompatibles(quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
|
||||
String incompatibleId = null;
|
||||
for (InstallerItem other : entry.getValue()) {
|
||||
if (other.libraryVersion.get() != null) {
|
||||
incompatibleId = other.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
item.incompatibleLibraryName.set(incompatibleId);
|
||||
}
|
||||
};
|
||||
for (InstallerItem item : incompatibleMap.keySet()) {
|
||||
item.libraryVersion.addListener(listener);
|
||||
}
|
||||
|
||||
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
|
||||
@@ -163,25 +197,22 @@ public class InstallerItem extends Control {
|
||||
else return null;
|
||||
}, fabric.libraryVersion));
|
||||
|
||||
for (InstallerItem quilt : new InstallerItem[]{quilt, quiltApi}) {
|
||||
quilt.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
|
||||
if (fabricApi.libraryVersion.get() != null) return FABRIC_API.getPatchId();
|
||||
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
|
||||
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
|
||||
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
|
||||
return null;
|
||||
}, fabric.libraryVersion, fabricApi.libraryVersion, forge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion));
|
||||
}
|
||||
|
||||
quiltApi.dependencyName.bind(Bindings.createStringBinding(() -> {
|
||||
if (quilt.libraryVersion.get() == null) return QUILT.getPatchId();
|
||||
else return null;
|
||||
}, quilt.libraryVersion));
|
||||
|
||||
if (gameVersion == null) {
|
||||
this.libraries = new InstallerItem[]{game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
|
||||
} else {
|
||||
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItem[] getLibraries() {
|
||||
return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
return libraries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,8 +237,8 @@ public class InstallerItem extends Control {
|
||||
pane.pseudoClassStateChanged(LIST_ITEM, control.style == Style.LIST_ITEM);
|
||||
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
||||
|
||||
if (control.imageUrl != null) {
|
||||
ImageView view = new ImageView(new Image(control.imageUrl));
|
||||
if (control.iconType != null) {
|
||||
ImageView view = new ImageView(control.iconType.getIcon());
|
||||
Node node = FXUtils.limitingSize(view, 32, 32);
|
||||
node.setMouseTransparent(true);
|
||||
node.getStyleClass().add("installer-item-image");
|
||||
@@ -252,7 +283,7 @@ public class InstallerItem extends Control {
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
|
||||
closeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
|
||||
closeButton.getStyleClass().add("toggle-icon4");
|
||||
closeButton.visibleProperty().bind(control.removable);
|
||||
closeButton.managedProperty().bind(closeButton.visibleProperty());
|
||||
@@ -261,8 +292,8 @@ public class InstallerItem extends Control {
|
||||
|
||||
JFXButton arrowButton = new JFXButton();
|
||||
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
|
||||
? SVG.update(Theme.blackFillBinding(), -1, -1)
|
||||
: SVG.arrowRight(Theme.blackFillBinding(), -1, -1),
|
||||
? SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1)
|
||||
: SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1),
|
||||
control.upgradable));
|
||||
arrowButton.getStyleClass().add("toggle-icon4");
|
||||
arrowButton.visibleProperty().bind(Bindings.createBooleanBinding(
|
||||
|
||||
@@ -69,7 +69,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
FXUtils.setLimitHeight(btnAdd, 40);
|
||||
btnAdd.getStyleClass().add("jfx-button-raised-round");
|
||||
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
|
||||
btnAdd.setGraphic(SVG.PLUS.createIcon(Theme.whiteFill(), -1, -1));
|
||||
btnAdd.setOnMouseClicked(e -> skinnable.add());
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
@@ -77,7 +77,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
FXUtils.setLimitHeight(btnRefresh, 40);
|
||||
btnRefresh.getStyleClass().add("jfx-button-raised-round");
|
||||
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1, -1));
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
|
||||
vBox.getChildren().setAll(btnAdd);
|
||||
|
||||
@@ -33,15 +33,19 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -56,14 +60,12 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LogWindow extends Stage {
|
||||
@@ -89,13 +91,17 @@ public final class LogWindow extends Stage {
|
||||
|
||||
private boolean stopCheckLogCount = false;
|
||||
|
||||
public LogWindow() {
|
||||
private final ManagedProcess gameProcess;
|
||||
|
||||
public LogWindow(ManagedProcess gameProcess) {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("logwindow.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||
|
||||
this.gameProcess = gameProcess;
|
||||
}
|
||||
|
||||
public void logLine(String filteredLine, Log4jLevel level) {
|
||||
@@ -144,6 +150,7 @@ public final class LogWindow extends Stage {
|
||||
private static class Log {
|
||||
private final String log;
|
||||
private final Log4jLevel level;
|
||||
private boolean selected = false;
|
||||
|
||||
public Log(String log, Log4jLevel level) {
|
||||
this.log = log;
|
||||
@@ -177,7 +184,7 @@ public final class LogWindow extends Stage {
|
||||
cboLines.getSelectionModel().select(2);
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue));
|
||||
});
|
||||
|
||||
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||
@@ -217,6 +224,35 @@ public final class LogWindow extends Stage {
|
||||
});
|
||||
}
|
||||
|
||||
private void onExportDump(JFXButton button) {
|
||||
thread(() -> {
|
||||
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
|
||||
if (SystemUtils.supportJVMAttachment()) {
|
||||
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
|
||||
|
||||
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||
|
||||
try {
|
||||
if (gameProcess.isRunning()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to create minecraft jstack dump", e);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button"));
|
||||
alert.setTitle(i18n("message.error"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onExportGameCrashInfo() {
|
||||
if (exportGameCrashInfoCallback == null) return;
|
||||
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
||||
@@ -236,6 +272,9 @@ public final class LogWindow extends Stage {
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final Set<ListCell<Log>> selected = new HashSet<>();
|
||||
|
||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
@@ -298,7 +337,7 @@ public final class LogWindow extends Stage {
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||
@@ -306,6 +345,27 @@ public final class LogWindow extends Stage {
|
||||
setPadding(new Insets(2));
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if (!event.isControlDown()) {
|
||||
for (ListCell<Log> logListCell: selected) {
|
||||
if (logListCell != this) {
|
||||
logListCell.pseudoClassStateChanged(SELECTED, false);
|
||||
if (logListCell.getItem() != null) {
|
||||
logListCell.getItem().selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected.clear();
|
||||
}
|
||||
|
||||
selected.add(this);
|
||||
pseudoClassStateChanged(SELECTED, true);
|
||||
if (getItem() != null) {
|
||||
getItem().selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -324,6 +384,8 @@ public final class LogWindow extends Stage {
|
||||
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(SELECTED, !empty && item.selected);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
} else {
|
||||
@@ -332,6 +394,23 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
});
|
||||
|
||||
listView.setOnKeyPressed(event -> {
|
||||
if (event.isControlDown() && event.getCode() == KeyCode.C) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (Log item : listView.getItems()) {
|
||||
if (item != null && item.selected) {
|
||||
if (item.log != null) {
|
||||
stringBuilder.append(item.log);
|
||||
}
|
||||
stringBuilder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
FXUtils.copyText(stringBuilder.toString());
|
||||
}
|
||||
});
|
||||
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
vbox.getChildren().add(listView);
|
||||
}
|
||||
@@ -353,15 +432,25 @@ public final class LogWindow extends Stage {
|
||||
autoScrollCheckBox.setSelected(true);
|
||||
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
||||
|
||||
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
||||
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
|
||||
|
||||
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
||||
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
|
||||
|
||||
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
||||
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
|
||||
|
||||
JFXButton exportDumpButton = new JFXButton();
|
||||
if (SystemUtils.supportJVMAttachment()) {
|
||||
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
|
||||
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
|
||||
} else {
|
||||
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
|
||||
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
|
||||
exportDumpButton.setDisable(true);
|
||||
}
|
||||
|
||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
|
||||
|
||||
vbox.getChildren().add(bottom);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
@@ -25,21 +25,104 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
|
||||
public final class SVG {
|
||||
private SVG() {
|
||||
public enum SVG {
|
||||
GEAR("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"),
|
||||
GEAR_OUTLINE("M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"),
|
||||
BACK("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"),
|
||||
CANCEL("M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z"),
|
||||
CHAT("M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z"),
|
||||
CLOSE("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"),
|
||||
COPY("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"),
|
||||
DOTS_VERTICAL("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z"),
|
||||
DOTS_HORIZONTAL("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"),
|
||||
DELETE("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"),
|
||||
DELETE_OUTLINE("M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z"),
|
||||
ACCOUNT_EDIT("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z"),
|
||||
EXPAND("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"),
|
||||
COLLAPSE("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"),
|
||||
NAVIGATE("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
|
||||
ROCKET_LAUNCH_OUTLINE("M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z"),
|
||||
LAUNCH_OUTLINE("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
|
||||
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"),
|
||||
PENCIL("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z"),
|
||||
PENCIL_OUTLINE("M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z"),
|
||||
REFRESH("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z"),
|
||||
FOLDER_OPEN("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z"),
|
||||
FOLDER_OUTLINE("M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z"),
|
||||
UPDATE("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"),
|
||||
CLOSE_CIRCLE("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"),
|
||||
CHECK_CIRCLE("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"),
|
||||
INFO_CIRCLE("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
|
||||
HELP_CIRCLE("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"),
|
||||
HELP_CIRCLE_OUTLINE("M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z"),
|
||||
ALERT("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"),
|
||||
ALERT_OUTLINE("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"),
|
||||
PLUS("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"),
|
||||
PLUS_CIRCLE_OUTLINE("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z"),
|
||||
IMPORT_ICON("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z"),
|
||||
EXPORT("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z"),
|
||||
OPEN_IN_NEW("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"),
|
||||
TRIANGLE("M1,21H23L12,2"),
|
||||
HOME("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"),
|
||||
VIEW_LIST("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"),
|
||||
CHECK("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"),
|
||||
ARROW_RIGHT("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z"),
|
||||
WRENCH("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"),
|
||||
WRENCH_OUTLINE("M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z"),
|
||||
UPLOAD("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z"),
|
||||
HANGER("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z"),
|
||||
PUZZLE("M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z"),
|
||||
CUBE("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"),
|
||||
PACK("M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z"),
|
||||
TEXTURE_BOX("M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z"),
|
||||
GAMEPAD("M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z"),
|
||||
FIRE("M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z"),
|
||||
MONITOR_SCREENSHOT("M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2"),
|
||||
TEXTURE("M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z"),
|
||||
ALPHA_CIRCLE_OUTLINE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"),
|
||||
BETA_CIRCLE_OUTLINE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"),
|
||||
RELEASE_CIRCLE_OUTLINE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"),
|
||||
INFORMATION_OUTLINE("M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"),
|
||||
HAND_HEAR_OUTLINE("M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z"),
|
||||
MOJANG("m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z"),
|
||||
MICROSOFT("M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z"),
|
||||
ACCOUNT_OUTLINE("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z"),
|
||||
ACCOUNT_GROUP_OUTLINE("M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z"),
|
||||
ACCOUNT_ARROW_RIGHT_OUTLINE("M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z"),
|
||||
STYLE_OUTLINE("M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z"),
|
||||
DOWNLOAD_OUTLINE("M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z"),
|
||||
APPLICATION_OUTLINE("M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z"),
|
||||
EARTH("M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
|
||||
BELL("M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21"),
|
||||
CONTENT_SAVE_MOVE_OUTLINE("M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z"),
|
||||
ACCOUNT("M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"),
|
||||
SERVER("M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z"),
|
||||
MESSAGE_ALERT_OUTLINE("M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z"),
|
||||
CHECK_CIRCLE_OUTLINE("M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"),
|
||||
CLOSE_CIRCLE_OUTLINE("M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z"),
|
||||
CLOCK_OUTLINE("M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z"),
|
||||
MAGNIFY("M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"),
|
||||
MENU_UP("M7,15L12,10L17,15H7Z"),
|
||||
MENU_DOWN("M7,10L12,15L17,10H7Z"),
|
||||
RESTORE("M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z"),
|
||||
BUG("M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"),
|
||||
DISCORD("M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z"),
|
||||
LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"),
|
||||
THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"),
|
||||
THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"),
|
||||
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z");
|
||||
|
||||
private final String path;
|
||||
|
||||
SVG(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public interface SVGIcon {
|
||||
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
SVGPath path = new SVGPath();
|
||||
path.getStyleClass().add("svg");
|
||||
path.setContent(d);
|
||||
if (fill != null)
|
||||
path.fillProperty().bind(fill);
|
||||
|
||||
private static Node createIcon(SVGPath path, double width, double height) {
|
||||
if (width < 0 || height < 0) {
|
||||
StackPane pane = new StackPane(path);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
@@ -54,490 +137,24 @@ 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";
|
||||
public Node createIcon(ObservableValue<? extends Paint> fill, double width, double height) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.getStyleClass().add("svg");
|
||||
p.setContent(path);
|
||||
if (fill != null)
|
||||
p.fillProperty().bind(fill);
|
||||
|
||||
// default fill: white, width: 24, height 24
|
||||
|
||||
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node gearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width,
|
||||
height);
|
||||
}
|
||||
|
||||
public static Node cancel(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill,
|
||||
width, height);
|
||||
}
|
||||
|
||||
public static Node deleteOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8,9H16V19H8V9M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node collapse(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node rocketLaunchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M19.22 4C19.5 4 19.75 4 19.96 4.05C20.13 5.44 19.94 8.3 16.66 11.58C14.96 13.29 12.93 14.6 10.65 15.47L8.5 13.37C9.42 11.06 10.73 9.03 12.42 7.34C15.18 4.58 17.64 4 19.22 4M19.22 2C17.24 2 14.24 2.69 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.62 17.27 10.13 17.5 10.66 17.5C10.89 17.5 11.13 17.44 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39S20.7 2 19.22 2M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node launchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node pencilOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z",
|
||||
fill, width, height);
|
||||
return createIcon(p, width, height);
|
||||
}
|
||||
|
||||
public static Node folderOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node helpCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node alertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node plusCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M1,21H23L12,2", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(ARROW_RIGHT, fill, width,
|
||||
height);
|
||||
}
|
||||
|
||||
public static Node wrench(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node wrenchOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M22.61,19L13.53,9.91C14.46,7.57 14,4.81 12.09,2.91C9.79,0.61 6.21,0.4 3.66,2.26L7.5,6.11L6.08,7.5L2.25,3.69C0.39,6.23 0.6,9.82 2.9,12.11C4.76,13.97 7.47,14.46 9.79,13.59L18.9,22.7C19.29,23.09 19.92,23.09 20.31,22.7L22.61,20.4C23,20 23,19.39 22.61,19M19.61,20.59L10.15,11.13C9.54,11.58 8.86,11.85 8.15,11.95C6.79,12.15 5.36,11.74 4.32,10.7C3.37,9.76 2.93,8.5 3,7.26L6.09,10.35L10.33,6.11L7.24,3C8.5,2.95 9.73,3.39 10.68,4.33C11.76,5.41 12.17,6.9 11.92,8.29C11.8,9 11.5,9.66 11.04,10.25L20.5,19.7L19.61,20.59Z",
|
||||
fill, width, height);
|
||||
}
|
||||
public Node createIcon(Paint fill, double width, double height) {
|
||||
SVGPath p = new SVGPath();
|
||||
p.getStyleClass().add("svg");
|
||||
p.setContent(path);
|
||||
if (fill != null)
|
||||
p.fillProperty().set(fill);
|
||||
|
||||
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
|
||||
return createIcon(p, width, height);
|
||||
}
|
||||
|
||||
public static Node hanger(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node puzzle(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node cube(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node pack(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node textureBox(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M20 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H20C21.11 22 22 21.11 22 20V4C22 2.9 21.11 2 20 2M4 6L6 4H10.9L4 10.9V6M4 13.7L13.7 4H18.6L4 18.6V13.7M20 18L18 20H13.1L20 13.1V18M20 10.3L10.3 20H5.4L20 5.4V10.3Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node gamepad(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M6,9H8V11H10V13H8V15H6V13H4V11H6V9M18.5,9A1.5,1.5 0 0,1 20,10.5A1.5,1.5 0 0,1 18.5,12A1.5,1.5 0 0,1 17,10.5A1.5,1.5 0 0,1 18.5,9M15.5,12A1.5,1.5 0 0,1 17,13.5A1.5,1.5 0 0,1 15.5,15A1.5,1.5 0 0,1 14,13.5A1.5,1.5 0 0,1 15.5,12M17,5A7,7 0 0,1 24,12A7,7 0 0,1 17,19C15.04,19 13.27,18.2 12,16.9C10.73,18.2 8.96,19 7,19A7,7 0 0,1 0,12A7,7 0 0,1 7,5H17M7,7A5,5 0 0,0 2,12A5,5 0 0,0 7,17C8.64,17 10.09,16.21 11,15H13C13.91,16.21 15.36,17 17,17A5,5 0 0,0 22,12A5,5 0 0,0 17,7H7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node fire(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node monitorScreenshot(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M9,6H5V10H7V8H9M19,10H17V12H15V14H19M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node texture(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M9.29,21H12.12L21,12.12V9.29M19,21C19.55,21 20.05,20.78 20.41,20.41C20.78,20.05 21,19.55 21,19V17L17,21M5,3A2,2 0 0,0 3,5V7L7,3M11.88,3L3,11.88V14.71L14.71,3M19.5,3.08L3.08,19.5C3.17,19.85 3.35,20.16 3.59,20.41C3.84,20.65 4.15,20.83 4.5,20.92L20.93,4.5C20.74,3.8 20.2,3.26 19.5,3.08Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node alphaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node betaCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node releaseCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node informationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node handHearOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node mojang(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"m 13.965847,0 c -1.010612,0.82802228 -1.197232,2.1950303 -1.265172,3.4179557 0.02123,1.0021189 1.341654,1.2994159 1.953117,0.590289 C 15.091158,2.6579315 14.369242,1.2738804 13.965847,0 Z M 10.913012,2.9296764 C 10.755901,3.6982508 10.628413,4.4668999 10.492533,5.2354744 8.9893533,3.9913178 7.1504705,3.0824837 5.1419856,3.2947971 3.4944341,3.4646478 0.94293227,2.6961479 0.14038761,4.645185 -0.12288102,8.3139606 0.07223999,12.01236 0.03402357,15.689629 c -0.12314178,1.222925 0.86170213,2.420422 2.14407513,2.280295 4.2207899,0.03397 8.4502143,0.04723 12.6710043,-0.0038 1.265389,0.135918 1.957646,-1.010748 2.13599,-2.0893 C 13.269608,16.437357 9.1760813,16.929609 5.7111265,15.129192 2.5986124,13.58355 2.246023,8.3138817 5.5581114,6.7979639 9.3203049,5.1758896 13.859607,8.0382886 14.942405,11.787743 15.613316,12.11046 16.284433,12.424684 16.95959,12.743154 16.624135,10.348258 16.653651,7.800456 15.579346,5.5881508 15.057054,4.7473897 14.097531,6.2714182 13.379911,5.6217388 12.416008,4.865903 11.749527,3.8128948 10.913012,2.9296713 Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node microsoft(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M2,3H11V12H2V3M11,22H2V13H11V22M21,3V12H12V3H21M21,22H12V13H21V22Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountGroupOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node accountArrowRightOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,21V19H15V17H19V15L22,18L19,21M13,18C13,18.71 13.15,19.39 13.42,20H2V17C2,14.79 5.58,13 10,13C11,13 11.96,13.09 12.85,13.26C13.68,13.42 14.44,13.64 15.11,13.92C13.83,14.83 13,16.32 13,18M4,17V18H11C11,16.96 11.23,15.97 11.64,15.08L10,15C6.69,15 4,15.9 4,17M10,4A4,4 0 0,1 14,8A4,4 0 0,1 10,12A4,4 0 0,1 6,8A4,4 0 0,1 10,4M10,6A2,2 0 0,0 8,8A2,2 0 0,0 10,10A2,2 0 0,0 12,8A2,2 0 0,0 10,6Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node styleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M2.5 19.6L3.8 20.2V11.2L1.4 17C1 18.1 1.5 19.2 2.5 19.6M15.2 4.8L20.2 16.8L12.9 19.8L7.9 7.9V7.8L15.2 4.8M15.3 2.8C15 2.8 14.8 2.8 14.5 2.9L7.1 6C6.4 6.3 5.9 7 5.9 7.8C5.9 8 5.9 8.3 6 8.6L11 20.5C11.3 21.3 12 21.7 12.8 21.7C13.1 21.7 13.3 21.7 13.6 21.6L21 18.5C22 18.1 22.5 16.9 22.1 15.9L17.1 4C16.8 3.2 16 2.8 15.3 2.8M10.5 9.9C9.9 9.9 9.5 9.5 9.5 8.9S9.9 7.9 10.5 7.9C11.1 7.9 11.5 8.4 11.5 8.9S11.1 9.9 10.5 9.9M5.9 19.8C5.9 20.9 6.8 21.8 7.9 21.8H9.3L5.9 13.5V19.8Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node downloadOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node applicationOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 20H3V6H21V20Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node earth(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node bell(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node contentSaveMoveOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13 17H17V14L22 18.5L17 23V20H13V17M14 12.8C13.5 12.31 12.78 12 12 12C10.34 12 9 13.34 9 15C9 16.31 9.84 17.41 11 17.82C11.07 15.67 12.27 13.8 14 12.8M11.09 19H5V5H16.17L19 7.83V12.35C19.75 12.61 20.42 13 21 13.54V7L17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H11.81C11.46 20.39 11.21 19.72 11.09 19M6 10H15V6H6V10Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node account(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node server(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,19H14A1,1 0 0,1 15,20H22V22H15A1,1 0 0,1 14,23H10A1,1 0 0,1 9,22H2V20H9A1,1 0 0,1 10,19H11V17H4A1,1 0 0,1 3,16V12A1,1 0 0,1 4,11H20A1,1 0 0,1 21,12V16A1,1 0 0,1 20,17H13V19M4,3H20A1,1 0 0,1 21,4V8A1,1 0 0,1 20,9H4A1,1 0 0,1 3,8V4A1,1 0 0,1 4,3M9,7H10V5H9V7M9,15H10V13H9V15M5,5V7H7V5H5M5,13V15H7V13H5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node messageAlertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,10H11V6H13V10M13,12H11V14H13V12M22,4V16A2,2 0 0,1 20,18H6L2,22V4A2,2 0 0,1 4,2H20A2,2 0 0,1 22,4M20,4H4V17.2L5.2,16H20V4Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node checkCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node closeCircleOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node clockOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node magnify(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node menuUp(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M7,15L12,10L17,15H7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node menuDown(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M7,10L12,15L17,10H7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node restore(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node bug(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node discord(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node lan(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node thumbUpOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node thumbDownOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node selectAll(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
}
|
||||
@@ -85,30 +85,30 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
|
||||
public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createToolbarButton2(String text, SVG.SVGIcon creator, Runnable onClick) {
|
||||
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.blackFillBinding(), -1, -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1, -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
|
||||
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
|
||||
JFXButton ret = new JFXButton();
|
||||
ret.getStyleClass().add("jfx-decorator-button");
|
||||
ret.textFillProperty().bind(Theme.foregroundFillBinding());
|
||||
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1, -1)));
|
||||
FXUtils.installFastTooltip(ret, tooltip);
|
||||
ret.setOnMouseClicked(e -> onClick.run());
|
||||
return ret;
|
||||
|
||||
@@ -29,7 +29,6 @@ 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;
|
||||
|
||||
public class WebStage extends Stage {
|
||||
protected final StackPane pane = new StackPane();
|
||||
@@ -44,7 +43,7 @@ public class WebStage extends Stage {
|
||||
public WebStage(int width, int height) {
|
||||
setScene(new Scene(pane, width, height));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
FXUtils.setIcon(this);
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
webView.setContextMenuEnabled(false);
|
||||
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
|
||||
|
||||
@@ -120,19 +120,15 @@ public class AccountListItem extends RadioButton {
|
||||
}
|
||||
|
||||
public ObservableBooleanValue canUploadSkin() {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
|
||||
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
|
||||
return createBooleanBinding(() -> {
|
||||
Set<TextureType> uploadableTextures = profile.get()
|
||||
.map(AuthlibInjectorAccount::getUploadableTextures)
|
||||
.orElse(emptySet());
|
||||
return uploadableTextures.contains(TextureType.SKIN);
|
||||
}, profile);
|
||||
} else {
|
||||
return createBooleanBinding(() -> true);
|
||||
}
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
|
||||
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
|
||||
return createBooleanBinding(() -> {
|
||||
Set<TextureType> uploadableTextures = profile.get()
|
||||
.map(AuthlibInjectorAccount::getUploadableTextures)
|
||||
.orElse(emptySet());
|
||||
return uploadableTextures.contains(TextureType.SKIN);
|
||||
}, profile);
|
||||
} else if (account instanceof OfflineAccount || account instanceof MicrosoftAccount) {
|
||||
return createBooleanBinding(() -> true);
|
||||
} else {
|
||||
|
||||
@@ -117,10 +117,10 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
});
|
||||
btnMove.getStyleClass().add("toggle-icon4");
|
||||
if (skinnable.getAccount().isPortable()) {
|
||||
btnMove.setGraphic(SVG.earth(Theme.blackFillBinding(), -1, -1));
|
||||
btnMove.setGraphic(SVG.EARTH.createIcon(Theme.blackFill(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global")));
|
||||
} else {
|
||||
btnMove.setGraphic(SVG.export(Theme.blackFillBinding(), -1, -1));
|
||||
btnMove.setGraphic(SVG.EXPORT.createIcon(Theme.blackFill(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable")));
|
||||
}
|
||||
spinnerMove.setContent(btnMove);
|
||||
@@ -142,7 +142,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
.start();
|
||||
});
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
||||
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.blackFill(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
spinnerRefresh.setContent(btnRefresh);
|
||||
right.getChildren().add(spinnerRefresh);
|
||||
@@ -159,7 +159,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
}
|
||||
});
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
|
||||
btnUpload.setGraphic(SVG.HANGER.createIcon(Theme.blackFill(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
|
||||
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
|
||||
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
|
||||
@@ -167,11 +167,20 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
spinnerUpload.getStyleClass().add("small-spinner-pane");
|
||||
right.getChildren().add(spinnerUpload);
|
||||
|
||||
JFXButton btnCopyUUID = new JFXButton();
|
||||
SpinnerPane spinnerCopyUUID = new SpinnerPane();
|
||||
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
|
||||
btnCopyUUID.setOnMouseClicked(e -> FXUtils.copyText(skinnable.getAccount().getUUID().toString()));
|
||||
btnCopyUUID.setGraphic(SVG.COPY.createIcon(Theme.blackFill(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid")));
|
||||
spinnerCopyUUID.setContent(btnCopyUUID);
|
||||
right.getChildren().add(spinnerCopyUUID);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
|
||||
btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
@@ -103,23 +103,15 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG::account));
|
||||
offlineItem.setLeftGraphic(wrap(SVG.ACCOUNT));
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
boxMethods.getChildren().add(offlineItem);
|
||||
|
||||
AdvancedListItem mojangItem = new AdvancedListItem();
|
||||
mojangItem.getStyleClass().add("navigation-drawer-item");
|
||||
mojangItem.setActionButtonVisible(false);
|
||||
mojangItem.setTitle(i18n("account.methods.yggdrasil"));
|
||||
mojangItem.setLeftGraphic(wrap(SVG::mojang));
|
||||
mojangItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
|
||||
boxMethods.getChildren().add(mojangItem);
|
||||
|
||||
AdvancedListItem microsoftItem = new AdvancedListItem();
|
||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||
microsoftItem.setActionButtonVisible(false);
|
||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG::microsoft));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
boxMethods.getChildren().add(microsoftItem);
|
||||
|
||||
@@ -127,7 +119,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setLeftGraphic(wrap(SVG::server));
|
||||
item.setLeftGraphic(wrap(SVG.SERVER));
|
||||
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
@@ -136,7 +128,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
e.consume();
|
||||
});
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
|
||||
btnRemove.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 14, 14));
|
||||
item.setRightGraphic(btnRemove);
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
@@ -164,7 +156,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
addAuthServerItem.setTitle(i18n("account.injector.add"));
|
||||
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
|
||||
addAuthServerItem.setActionButtonVisible(false);
|
||||
addAuthServerItem.setLeftGraphic(wrap(SVG::plusCircleOutline));
|
||||
addAuthServerItem.setLeftGraphic(wrap(SVG.PLUS_CIRCLE_OUTLINE));
|
||||
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
|
||||
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
|
||||
}
|
||||
|
||||
@@ -24,20 +24,15 @@ import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.ClassicAccount;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
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.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
@@ -84,20 +79,6 @@ public class ClassicAccountLoginDialog extends StackPane {
|
||||
txtPassword.setPromptText(i18n("account.password"));
|
||||
|
||||
body.getChildren().setAll(usernameLabel, txtPassword);
|
||||
|
||||
if (oldAccount instanceof YggdrasilAccount && !(oldAccount instanceof AuthlibInjectorAccount)) {
|
||||
HBox linkPane = new HBox(8);
|
||||
body.getChildren().add(linkPane);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setExternalLink(YggdrasilService.PROFILE_URL);
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setExternalLink(YggdrasilService.MIGRATION_FAQ_URL);
|
||||
|
||||
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
|
||||
}
|
||||
|
||||
dialogLayout.setBody(body);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ import org.jackhuang.hmcl.auth.authlibinjector.BoundAuthlibInjectorAccountFactor
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
@@ -69,6 +68,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
@@ -80,6 +80,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.classPropertyFor;
|
||||
|
||||
public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private static final Pattern USERNAME_CHECKER_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
|
||||
private boolean showMethodSwitcher;
|
||||
private AccountFactory<?> factory;
|
||||
@@ -222,35 +223,52 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
additionalData = null;
|
||||
}
|
||||
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
Runnable doCreate = () -> {
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
// adding an already-added account
|
||||
// instead of discarding the new account, we first remove the existing one then add the new one
|
||||
Accounts.getAccounts().remove(oldIndex);
|
||||
Accounts.getAccounts().add(oldIndex, account);
|
||||
}
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
// adding an already-added account
|
||||
// instead of discarding the new account, we first remove the existing one then add the new one
|
||||
Accounts.getAccounts().remove(oldIndex);
|
||||
Accounts.getAccounts().add(oldIndex, account);
|
||||
}
|
||||
|
||||
// select the new account
|
||||
Accounts.setSelectedAccount(account);
|
||||
// select the new account
|
||||
Accounts.setSelectedAccount(account);
|
||||
|
||||
spinner.hideSpinner();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, exception -> {
|
||||
if (exception instanceof NoSelectedCharacterException) {
|
||||
spinner.hideSpinner();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblErrorMessage.setText(Accounts.localizeErrorMessage(exception));
|
||||
}, exception -> {
|
||||
if (exception instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblErrorMessage.setText(Accounts.localizeErrorMessage(exception));
|
||||
}
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}).executor(true);
|
||||
};
|
||||
|
||||
if (factory instanceof OfflineAccountFactory && username != null && !USERNAME_CHECKER_PATTERN.matcher(username).matches()) {
|
||||
Controllers.confirm(
|
||||
i18n("account.methods.offline.name.invalid"), i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
doCreate,
|
||||
() -> {
|
||||
lblErrorMessage.setText(i18n("account.methods.offline.name.invalid"));
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}).executor(true);
|
||||
);
|
||||
} else {
|
||||
doCreate.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
@@ -293,7 +311,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a");
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
||||
@@ -325,24 +343,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
detailsPane = vbox;
|
||||
} else if (factory == Accounts.FACTORY_MOJANG) {
|
||||
VBox vbox = new VBox(8);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setText(i18n("account.methods.yggdrasil.migration.hint"));
|
||||
|
||||
HBox linkPane = new HBox(8);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
|
||||
|
||||
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
|
||||
|
||||
vbox.getChildren().setAll(hintPane, linkPane);
|
||||
detailsPane = vbox;
|
||||
btnAccept.setDisable(true);
|
||||
} else {
|
||||
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
|
||||
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
|
||||
@@ -353,7 +353,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private static class AccountDetailsInputPane extends GridPane {
|
||||
|
||||
// ==== authlib-injector hyperlinks ====
|
||||
private static final String[] ALLOWED_LINKS = { "homepage", "register" };
|
||||
private static final String[] ALLOWED_LINKS = {"homepage", "register"};
|
||||
|
||||
private static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
if (server == null) {
|
||||
@@ -459,7 +459,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
linksContainer.setMinWidth(USE_PREF_SIZE);
|
||||
|
||||
JFXButton btnAddServer = new JFXButton();
|
||||
btnAddServer.setGraphic(SVG.plus(Theme.blackFillBinding(), 20, 20));
|
||||
btnAddServer.setGraphic(SVG.PLUS.createIcon(Theme.blackFill(), 20, 20));
|
||||
btnAddServer.getStyleClass().add("toggle-icon4");
|
||||
btnAddServer.setOnAction(e -> {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
@@ -507,29 +507,11 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
if (factory instanceof YggdrasilAccountFactory) {
|
||||
HBox box = new HBox();
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setExternalLink("https://aka.ms/MinecraftMigration");
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setExternalLink("https://help.minecraft.net/hc/articles/4411173197709");
|
||||
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
|
||||
box.getChildren().setAll(migrationLink, migrationHowLink, purchaseLink);
|
||||
add(box, 0, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
if (factory instanceof OfflineAccountFactory) {
|
||||
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
|
||||
runInFX(() -> FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters")));
|
||||
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
HBox linkPane = new HBox(purchaseLink);
|
||||
GridPane.setColumnSpan(linkPane, 2);
|
||||
@@ -600,9 +582,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
private boolean requiresEmailAsUsername() {
|
||||
if (factory instanceof YggdrasilAccountFactory) {
|
||||
return true;
|
||||
} else if ((factory instanceof AuthlibInjectorAccountFactory) && this.server != null) {
|
||||
if ((factory instanceof AuthlibInjectorAccountFactory) && this.server != null) {
|
||||
return !server.isNonEmailLogin();
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -70,7 +70,7 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-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"));
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
@@ -85,7 +85,7 @@ final class ComponentListCell extends StackPane {
|
||||
|
||||
VBox groupNode = new VBox();
|
||||
|
||||
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
|
||||
Node expandIcon = SVG.EXPAND.createIcon(Theme.blackFill(), 20, 20);
|
||||
JFXButton expandButton = new JFXButton();
|
||||
expandButton.setGraphic(expandIcon);
|
||||
expandButton.getStyleClass().add("options-list-item-expand-button");
|
||||
|
||||
@@ -58,7 +58,7 @@ public class FileItem extends BorderPane {
|
||||
setLeft(left);
|
||||
|
||||
JFXButton right = new JFXButton();
|
||||
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
|
||||
right.setGraphic(SVG.PENCIL.createIcon(Theme.blackFill(), 15, 15));
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
right.setOnMouseClicked(e -> onExplore());
|
||||
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||
|
||||
@@ -81,7 +81,7 @@ public class FileSelector extends HBox {
|
||||
FXUtils.bindString(customField, valueProperty());
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
|
||||
selectButton.setGraphic(SVG.FOLDER_OPEN.createIcon(Theme.blackFill(), 15, 15));
|
||||
selectButton.setOnAction(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
|
||||
@@ -45,31 +45,30 @@ public class HintPane extends VBox {
|
||||
public HintPane(MessageDialogPane.MessageType type) {
|
||||
setFillWidth(true);
|
||||
getStyleClass().addAll("hint", type.name().toLowerCase(Locale.ROOT));
|
||||
HBox hbox = new HBox();
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
SVG svg;
|
||||
switch (type) {
|
||||
case INFO:
|
||||
hbox.getChildren().add(SVG.informationOutline(Theme.blackFillBinding(), 16, 16));
|
||||
svg = SVG.INFORMATION_OUTLINE;
|
||||
break;
|
||||
case ERROR:
|
||||
hbox.getChildren().add(SVG.closeCircleOutline(Theme.blackFillBinding(), 16, 16));
|
||||
svg = SVG.CLOSE_CIRCLE_OUTLINE;
|
||||
break;
|
||||
case SUCCESS:
|
||||
hbox.getChildren().add(SVG.checkCircleOutline(Theme.blackFillBinding(), 16, 16));
|
||||
svg = SVG.CHECK_CIRCLE_OUTLINE;
|
||||
break;
|
||||
case WARNING:
|
||||
hbox.getChildren().add(SVG.alertOutline(Theme.blackFillBinding(), 16, 16));
|
||||
svg = SVG.ALERT_OUTLINE;
|
||||
break;
|
||||
case QUESTION:
|
||||
hbox.getChildren().add(SVG.helpCircleOutline(Theme.blackFillBinding(), 16, 16));
|
||||
svg = SVG.HELP_CIRCLE_OUTLINE;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized message box message type " + type);
|
||||
}
|
||||
|
||||
|
||||
hbox.getChildren().add(new Text(type.getDisplayName()));
|
||||
HBox hbox = new HBox(svg.createIcon(Theme.blackFill(), 16, 16), new Text(type.getDisplayName()));
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
flow.getChildren().setAll(label);
|
||||
getChildren().setAll(hbox, flow);
|
||||
label.textProperty().bind(text);
|
||||
|
||||
@@ -17,16 +17,26 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
public class IconedMenuItem extends IconedItem {
|
||||
|
||||
public IconedMenuItem(Node node, String text, Runnable action) {
|
||||
super(node, text);
|
||||
public IconedMenuItem(SVG icon, String text, Runnable action, JFXPopup popup) {
|
||||
super(icon != null ? FXUtils.limitingSize(icon.createIcon(Theme.blackFill(), 14, 14), 14, 14) : null, text);
|
||||
|
||||
getStyleClass().setAll("iconed-menu-item");
|
||||
setOnMouseClicked(e -> action.run());
|
||||
|
||||
if (popup == null) {
|
||||
setOnMouseClicked(e -> action.run());
|
||||
} else {
|
||||
setOnMouseClicked(e -> {
|
||||
action.run();
|
||||
popup.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public IconedMenuItem addTooltip(String tooltip) {
|
||||
|
||||
@@ -32,7 +32,7 @@ public class IconedTwoLineListItem extends HBox {
|
||||
private final Lazy<JFXButton> externalLinkButton = new Lazy<>(() -> {
|
||||
JFXButton button = new JFXButton();
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(SVG.openInNew(Theme.blackFillBinding(), -1, -1));
|
||||
button.setGraphic(SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), -1, -1));
|
||||
button.setOnAction(e -> FXUtils.openLink(externalLink.get()));
|
||||
return button;
|
||||
});
|
||||
|
||||
@@ -54,12 +54,12 @@ public final class ImagePickerItem extends BorderPane {
|
||||
imageView.setPreserveRatio(true);
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.setGraphic(SVG.pencil(Theme.blackFillBinding(), 20, 20));
|
||||
selectButton.setGraphic(SVG.PENCIL.createIcon(Theme.blackFill(), 20, 20));
|
||||
selectButton.onMouseClickedProperty().bind(onSelectButtonClicked);
|
||||
selectButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
JFXButton deleteButton = new JFXButton();
|
||||
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), 20, 20));
|
||||
deleteButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 20, 20));
|
||||
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
|
||||
deleteButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ public class JFXHyperlink extends Hyperlink {
|
||||
public JFXHyperlink() {
|
||||
super();
|
||||
|
||||
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
|
||||
setGraphic(SVG.LAUNCH_OUTLINE.createIcon(Theme.blackFill(), 16, 16));
|
||||
}
|
||||
|
||||
public JFXHyperlink(String text) {
|
||||
super(text);
|
||||
|
||||
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
|
||||
setGraphic(SVG.LAUNCH_OUTLINE.createIcon(Theme.blackFill(), 16, 16));
|
||||
}
|
||||
|
||||
public void setExternalLink(String externalLink) {
|
||||
|
||||
@@ -81,8 +81,8 @@ public class MenuUpDownButton extends Control {
|
||||
label.setStyle("-fx-text-fill: black;");
|
||||
label.textProperty().bind(control.text);
|
||||
|
||||
Node up = SVG.menuUp(Theme.blackFillBinding(), 16, 16);
|
||||
Node down = SVG.menuDown(Theme.blackFillBinding(), 16, 16);
|
||||
Node up = SVG.MENU_UP.createIcon(Theme.blackFill(), 16, 16);
|
||||
Node down = SVG.MENU_DOWN.createIcon(Theme.blackFill(), 16, 16);
|
||||
|
||||
JFXButton button = new JFXButton();
|
||||
button.setGraphic(content);
|
||||
|
||||
@@ -71,25 +71,27 @@ public final class MessageDialogPane extends HBox {
|
||||
graphic.setTranslateY(10);
|
||||
graphic.setMinSize(40, 40);
|
||||
graphic.setMaxSize(40, 40);
|
||||
SVG svg;
|
||||
switch (type) {
|
||||
case INFO:
|
||||
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
|
||||
svg = SVG.INFO_CIRCLE;
|
||||
break;
|
||||
case ERROR:
|
||||
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
|
||||
svg = SVG.CLOSE_CIRCLE;
|
||||
break;
|
||||
case SUCCESS:
|
||||
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
|
||||
svg = SVG.CHECK_CIRCLE;
|
||||
break;
|
||||
case WARNING:
|
||||
graphic.setGraphic(SVG.alert(Theme.blackFillBinding(), 40, 40));
|
||||
svg = SVG.ALERT;
|
||||
break;
|
||||
case QUESTION:
|
||||
graphic.setGraphic(SVG.helpCircle(Theme.blackFillBinding(), 40, 40));
|
||||
svg = SVG.HELP_CIRCLE;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized message box message type " + type);
|
||||
}
|
||||
graphic.setGraphic(svg.createIcon(Theme.blackFill(), 40, 40));
|
||||
|
||||
VBox vbox = new VBox();
|
||||
HBox.setHgrow(vbox, Priority.ALWAYS);
|
||||
|
||||
@@ -40,7 +40,7 @@ import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MultiFileItem<T> extends VBox {
|
||||
public final class MultiFileItem<T> extends VBox {
|
||||
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
|
||||
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
|
||||
|
||||
@@ -176,8 +176,8 @@ public class MultiFileItem<T> extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringOption<T> extends Option<T> {
|
||||
private JFXTextField customField = new JFXTextField();
|
||||
public static final class StringOption<T> extends Option<T> {
|
||||
private final JFXTextField customField = new JFXTextField();
|
||||
|
||||
public StringOption(String title, T data) {
|
||||
super(title, data);
|
||||
@@ -196,7 +196,7 @@ public class MultiFileItem<T> extends VBox {
|
||||
}
|
||||
|
||||
public StringOption<T> bindBidirectional(Property<String> property) {
|
||||
customField.textProperty().bindBidirectional(property);
|
||||
FXUtils.bindString(customField, property);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -230,8 +230,8 @@ public class MultiFileItem<T> extends VBox {
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileOption<T> extends Option<T> {
|
||||
private FileSelector selector = new FileSelector();
|
||||
public static final class FileOption<T> extends Option<T> {
|
||||
private final FileSelector selector = new FileSelector();
|
||||
|
||||
public FileOption(String title, T data) {
|
||||
super(title, data);
|
||||
|
||||
@@ -36,6 +36,8 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
|
||||
@@ -123,6 +125,8 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
|
||||
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.neoforge")));
|
||||
} else if (task instanceof LiteLoaderInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
|
||||
} else if (task instanceof OptiFineInstallTask) {
|
||||
@@ -248,6 +252,7 @@ public final class TaskListPane extends StackPane {
|
||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||
case "hmcl.install.game": message = i18n("install.installer.install", i18n("install.installer.game") + " " + stageValue); break;
|
||||
case "hmcl.install.forge": message = i18n("install.installer.install", i18n("install.installer.forge") + " " + stageValue); break;
|
||||
case "hmcl.install.neoforge": message = i18n("install.installer.install", i18n("install.installer.neoforge") + " " + stageValue); break;
|
||||
case "hmcl.install.liteloader": message = i18n("install.installer.install", i18n("install.installer.liteloader") + " " + stageValue); break;
|
||||
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
|
||||
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
|
||||
@@ -262,21 +267,21 @@ public final class TaskListPane extends StackPane {
|
||||
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
|
||||
setPadding(new Insets(0, 0, 8, 4));
|
||||
setCenter(title);
|
||||
setLeft(FXUtils.limitingSize(SVG.dotsHorizontal(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
setLeft(FXUtils.limitingSize(SVG.DOTS_HORIZONTAL.createIcon(Theme.blackFill(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void begin() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
setLeft(FXUtils.limitingSize(SVG.arrowRight(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
setLeft(FXUtils.limitingSize(SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void fail() {
|
||||
setLeft(FXUtils.limitingSize(SVG.close(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void succeed() {
|
||||
setLeft(FXUtils.limitingSize(SVG.check(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void count() {
|
||||
@@ -301,7 +306,7 @@ public final class TaskListPane extends StackPane {
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
|
||||
getWidth() - getPadding().getLeft() - getPadding().getRight(),
|
||||
getWidth() - getPadding().getLeft() - getPadding().getRight() - getInsets().getLeft() - getInsets().getRight(),
|
||||
paddingProperty(), widthProperty());
|
||||
|
||||
public ProgressListNode(Task<?> task) {
|
||||
|
||||
@@ -50,6 +50,10 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -64,7 +68,7 @@ import java.util.stream.Stream;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
|
||||
@@ -168,11 +172,26 @@ public class DecoratorController {
|
||||
break;
|
||||
case NETWORK:
|
||||
String backgroundImageUrl = config().getBackgroundImageUrl();
|
||||
if (backgroundImageUrl != null && NetworkUtils.isURL(backgroundImageUrl))
|
||||
image = tryLoadImage(backgroundImageUrl).orElse(null);
|
||||
if (backgroundImageUrl != null) {
|
||||
try {
|
||||
URLConnection connection = NetworkUtils.createConnection(new URL(backgroundImageUrl));
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
}
|
||||
|
||||
try (InputStream input = connection.getInputStream()) {
|
||||
image = new Image(input);
|
||||
if (image.isError()) {
|
||||
throw image.getException();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.log(WARNING, "Couldn't load background image", e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CLASSIC:
|
||||
image = newImage("/assets/img/background-classic.jpg");
|
||||
image = newBuiltinImage("/assets/img/background-classic.jpg");
|
||||
break;
|
||||
case TRANSLUCENT:
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
@@ -183,8 +202,6 @@ public class DecoratorController {
|
||||
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
|
||||
}
|
||||
|
||||
private volatile Image defaultBackground;
|
||||
|
||||
/**
|
||||
* Load background image from bg/, background.png, background.jpg, background.gif
|
||||
*/
|
||||
@@ -199,12 +216,7 @@ public class DecoratorController {
|
||||
if (!image.isPresent()) {
|
||||
image = tryLoadImage(Paths.get("background.gif"));
|
||||
}
|
||||
|
||||
return image.orElseGet(() -> {
|
||||
if (defaultBackground == null)
|
||||
defaultBackground = newImage("/assets/img/background.jpg");
|
||||
return defaultBackground;
|
||||
});
|
||||
return image.orElseGet(() -> newBuiltinImage("/assets/img/background.jpg"));
|
||||
}
|
||||
|
||||
private Optional<Image> randomImageIn(Path imageDir) {
|
||||
|
||||
@@ -141,6 +141,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
|
||||
// Maybe, we can automatically identify whether the top part of the picture is light-coloured or dark when the title is transparent,
|
||||
// and decide whether the whole top bar should be rendered in white or black. TODO
|
||||
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
|
||||
if (titleTransparent) {
|
||||
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
@@ -194,6 +196,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
buttonsContainer.setAlignment(Pos.TOP_RIGHT);
|
||||
buttonsContainer.setMaxHeight(40);
|
||||
{
|
||||
JFXButton btnHelp = new JFXButton();
|
||||
btnHelp.setGraphic(SVG.HELP_CIRCLE_OUTLINE.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
btnHelp.getStyleClass().add("jfx-decorator-button");
|
||||
btnHelp.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
|
||||
|
||||
JFXButton btnMin = new JFXButton();
|
||||
StackPane pane = new StackPane(minus);
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
@@ -202,11 +209,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
btnMin.setOnAction(e -> skinnable.minimize());
|
||||
|
||||
JFXButton btnClose = new JFXButton();
|
||||
btnClose.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
btnClose.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
btnClose.getStyleClass().add("jfx-decorator-button");
|
||||
btnClose.setOnAction(e -> skinnable.close());
|
||||
|
||||
buttonsContainer.getChildren().setAll(btnMin, btnClose);
|
||||
buttonsContainer.getChildren().setAll(btnHelp, btnMin, btnClose);
|
||||
}
|
||||
AnchorPane layer = new AnchorPane();
|
||||
layer.setPickOnBounds(false);
|
||||
@@ -229,9 +236,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
if (canBack) {
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
|
||||
backNavButton.setGraphic(SVG.BACK.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
backNavButton.ripplerFillProperty().set(Theme.whiteFill());
|
||||
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
|
||||
backNavButton.visibleProperty().set(canBack);
|
||||
|
||||
@@ -240,14 +247,14 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
if (canClose) {
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
closeNavButton.ripplerFillProperty().set(Theme.whiteFill());
|
||||
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
|
||||
if (showCloseAsHome)
|
||||
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.setGraphic(SVG.HOME.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
else
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
|
||||
navLeft.getChildren().add(closeNavButton);
|
||||
}
|
||||
@@ -286,9 +293,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
HBox navRight = new HBox();
|
||||
navRight.setAlignment(Pos.CENTER_RIGHT);
|
||||
JFXButton refreshNavButton = new JFXButton();
|
||||
refreshNavButton.setGraphic(SVG.refresh(Theme.foregroundFillBinding(), -1, -1));
|
||||
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Theme.foregroundFillBinding(), -1, -1));
|
||||
refreshNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
|
||||
refreshNavButton.ripplerFillProperty().set(Theme.whiteFill());
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
|
||||
@@ -84,37 +84,30 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version.resolvePreservingPatches(repository));
|
||||
Version resolved = version.resolvePreservingPatches(repository);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null));
|
||||
String game = analyzer.getVersion(MINECRAFT).orElse(null);
|
||||
String forge = analyzer.getVersion(FORGE).orElse(null);
|
||||
String liteLoader = analyzer.getVersion(LITELOADER).orElse(null);
|
||||
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
|
||||
String fabric = analyzer.getVersion(FABRIC).orElse(null);
|
||||
String fabricApi = analyzer.getVersion(FABRIC_API).orElse(null);
|
||||
String quilt = analyzer.getVersion(QUILT).orElse(null);
|
||||
String quiltApi = analyzer.getVersion(QUILT_API).orElse(null);
|
||||
|
||||
InstallerItem[] libraries = group.getLibraries();
|
||||
String[] versions = new String[]{game, forge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
|
||||
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
|
||||
|
||||
boolean compatible = true;
|
||||
for (int i = 0; i < libraries.length; ++i) {
|
||||
String libraryId = libraries[i].getLibraryId();
|
||||
String libraryVersion = Lang.nonNull(getVersion(libraryId), versions[i]);
|
||||
boolean alreadyInstalled = versions[i] != null && !(controller.getSettings().get(libraryId) instanceof UpdateInstallerWizardProvider.RemoveVersionAction);
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
String version = analyzer.getVersion(libraryId).orElse(null);
|
||||
String libraryVersion = Lang.requireNonNullElse(getVersion(libraryId), version);
|
||||
boolean alreadyInstalled = version != null && !(controller.getSettings().get(libraryId) instanceof UpdateInstallerWizardProvider.RemoveVersionAction);
|
||||
if (!"game".equals(libraryId) && currentGameVersion != null && !currentGameVersion.equals(game) && getVersion(libraryId) == null && alreadyInstalled) {
|
||||
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
|
||||
// warns the user that we should update the library.
|
||||
libraries[i].setState(libraryVersion, /* incompatibleWithGame */ true, /* removable */ true);
|
||||
library.setState(libraryVersion, /* incompatibleWithGame */ true, /* removable */ true);
|
||||
compatible = false;
|
||||
} else if (alreadyInstalled || getVersion(libraryId) != null) {
|
||||
libraries[i].setState(libraryVersion, /* incompatibleWithGame */ false, /* removable */ true);
|
||||
library.setState(libraryVersion, /* incompatibleWithGame */ false, /* removable */ true);
|
||||
} else {
|
||||
libraries[i].setState(/* libraryVersion */ null, /* incompatibleWithGame */ false, /* removable */ false);
|
||||
library.setState(/* libraryVersion */ null, /* incompatibleWithGame */ false, /* removable */ false);
|
||||
}
|
||||
}
|
||||
|
||||
this.compatible.set(compatible);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
return page;
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.RESOURCE_PACKS, (profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new ResourcePackDownloadListPage((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
customizationTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.CUSTOMIZATIONS)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
|
||||
@@ -106,28 +106,28 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
.startCategory(i18n("download.game"))
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("game"));
|
||||
item.setLeftGraphic(wrap(SVG::gamepad));
|
||||
item.setLeftGraphic(wrap(SVG.GAMEPAD));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(newGameTab));
|
||||
item.setOnAction(e -> tab.select(newGameTab));
|
||||
})
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("modpack"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG::pack));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.PACK));
|
||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modpackTab));
|
||||
settingsItem.setOnAction(e -> tab.select(modpackTab));
|
||||
})
|
||||
.startCategory(i18n("download.content"))
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("mods"));
|
||||
item.setLeftGraphic(wrap(SVG::puzzle));
|
||||
item.setLeftGraphic(wrap(SVG.PUZZLE));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab));
|
||||
item.setOnAction(e -> tab.select(modTab));
|
||||
})
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("resourcepack"));
|
||||
item.setLeftGraphic(wrap(SVG::textureBox));
|
||||
item.setLeftGraphic(wrap(SVG.TEXTURE_BOX));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
|
||||
item.setOnAction(e -> selectTabIfCurseForgeAvailable(resourcePackTab));
|
||||
item.setOnAction(e -> tab.select(resourcePackTab));
|
||||
})
|
||||
// .addNavigationDrawerItem(item -> {
|
||||
// item.setTitle(i18n("download.curseforge.customization"));
|
||||
@@ -137,7 +137,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
// })
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("world"));
|
||||
item.setLeftGraphic(wrap(SVG::earth));
|
||||
item.setLeftGraphic(wrap(SVG.EARTH));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
|
||||
item.setOnAction(e -> selectTabIfCurseForgeAvailable(worldTab));
|
||||
});
|
||||
@@ -232,8 +232,13 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
tab.select(newGameTab);
|
||||
}
|
||||
|
||||
public void showModDownloads() {
|
||||
public void showModpackDownloads() {
|
||||
tab.select(modpackTab);
|
||||
}
|
||||
|
||||
public DownloadListPage showModDownloads() {
|
||||
tab.select(modTab);
|
||||
return modTab.getNode();
|
||||
}
|
||||
|
||||
public void showWorldDownloads() {
|
||||
|
||||
@@ -23,14 +23,12 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -50,12 +48,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class InstallersPage extends Control implements WizardPage {
|
||||
protected final WizardController controller;
|
||||
|
||||
protected InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup();
|
||||
protected InstallerItem.InstallerItemGroup group;
|
||||
protected JFXTextField txtName = new JFXTextField();
|
||||
protected BooleanProperty installable = new SimpleBooleanProperty();
|
||||
|
||||
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion);
|
||||
|
||||
txtName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
@@ -72,9 +71,9 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals("game")) continue;
|
||||
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
|
||||
library.action.set(e -> {
|
||||
if ("fabric-api".equals(libraryId)) {
|
||||
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
|
||||
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
|
||||
@@ -154,11 +153,22 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
{
|
||||
FlowPane libraryPane = new FlowPane(control.group.getLibraries());
|
||||
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
|
||||
InstallerItem[] libraries = control.group.getLibraries();
|
||||
|
||||
FlowPane libraryPane = new FlowPane(libraries);
|
||||
libraryPane.setVgap(16);
|
||||
libraryPane.setHgap(16);
|
||||
root.setCenter(libraryPane);
|
||||
|
||||
if (libraries.length <= 8) {
|
||||
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(libraryPane);
|
||||
} else {
|
||||
ScrollPane scrollPane = new ScrollPane(libraryPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(scrollPane);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
this.getChildren().setAll(
|
||||
title,
|
||||
createButton("local", this::onChooseLocalFile),
|
||||
createButton("remote", this::onChooseRemoteFile)
|
||||
createButton("remote", this::onChooseRemoteFile),
|
||||
createButton("repository", this::onChooseRepository)
|
||||
);
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
@@ -98,7 +99,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
|
||||
|
||||
SVGPath arrow = new SVGPath();
|
||||
arrow.setContent(SVG.ARROW_RIGHT);
|
||||
arrow.setContent(SVG.ARROW_RIGHT.getPath());
|
||||
BorderPane.setAlignment(arrow, Pos.CENTER);
|
||||
graphic.setRight(arrow);
|
||||
|
||||
@@ -168,6 +169,12 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
});
|
||||
}
|
||||
|
||||
public void onChooseRepository() {
|
||||
DownloadPage downloadPage = new DownloadPage();
|
||||
downloadPage.showModpackDownloads();
|
||||
Controllers.navigate(downloadPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
txtModpackName.setText(name.get());
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
txtModpackName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
|
||||
@@ -26,7 +26,6 @@ import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
@@ -36,11 +35,13 @@ import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
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;
|
||||
@@ -51,11 +52,8 @@ 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;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -64,6 +62,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
|
||||
@@ -177,11 +176,10 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
|
||||
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1, -1)));
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
EnumMap<VersionIconType, Image> icons = new EnumMap<>(VersionIconType.class);
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, icons));
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell));
|
||||
|
||||
list.setOnMouseClicked(e -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
@@ -241,7 +239,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/huanghongxun/HMCL/issues/938
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
});
|
||||
}
|
||||
@@ -265,7 +263,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
private void onBack() { navigation.onPrev(true); }
|
||||
|
||||
private void onSponsor() {
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
@@ -274,21 +272,15 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
private final EnumMap<VersionIconType, Image> icons;
|
||||
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, EnumMap<VersionIconType, Image> icons) {
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell) {
|
||||
this.lastCell = lastCell;
|
||||
this.icons = icons;
|
||||
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
private Image getIcon(VersionIconType type) {
|
||||
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
@@ -306,7 +298,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
content.setSubtitle(formatDateTime(remoteVersion.getReleaseDate()));
|
||||
} else {
|
||||
content.setSubtitle(null);
|
||||
}
|
||||
@@ -315,15 +307,15 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(getIcon(VersionIconType.GRASS));
|
||||
content.setImage(VersionIconType.GRASS.getIcon());
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(getIcon(VersionIconType.COMMAND));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(getIcon(VersionIconType.CRAFT_TABLE));
|
||||
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -331,9 +323,11 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.COMMAND;
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof NeoForgeRemoteVersion)
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
@@ -341,7 +335,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
else
|
||||
iconType = null;
|
||||
|
||||
content.setImage(iconType != null ? getIcon(iconType) : null);
|
||||
content.setImage(iconType != null ? iconType.getIcon() : null);
|
||||
if (content.getSubtitle() == null)
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
|
||||
@@ -68,8 +68,8 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
|
||||
Optional<Path> launcherJar = JarUtils.thisJar();
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar.isPresent();
|
||||
Path launcherJar = JarUtils.thisJarPath();
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
|
||||
return new Task<Object>() {
|
||||
File tempModpack;
|
||||
Task<?> exportTask;
|
||||
@@ -141,7 +141,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
if (background_gif.isFile())
|
||||
zip.putFile(background_gif, "background.gif");
|
||||
|
||||
zip.putFile(launcherJar.get(), launcherJar.get().getFileName().toString());
|
||||
zip.putFile(launcherJar, launcherJar.getFileName().toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
launchArguments.set(versionSetting.getMinecraftArgs());
|
||||
javaArguments.set(versionSetting.getJavaArgs());
|
||||
|
||||
canIncludeLauncher = JarUtils.thisJar().isPresent();
|
||||
canIncludeLauncher = JarUtils.thisJarPath() != null;
|
||||
|
||||
next.set(e -> onNext());
|
||||
}
|
||||
@@ -179,7 +179,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER) {
|
||||
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
|
||||
hyperlink.setOnMouseClicked(e -> {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/server-modpack");
|
||||
FXUtils.openLink("https://docs.hmcl.net/modpack/serverpack.html");
|
||||
});
|
||||
borderPane.setTop(hyperlink);
|
||||
} else {
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
graphic.setLeft(new TwoLineListItem(i18n("modpack.type." + type), i18n("modpack.type." + type + ".export")));
|
||||
|
||||
SVGPath arrow = new SVGPath();
|
||||
arrow.setContent(SVG.ARROW_RIGHT);
|
||||
arrow.setContent(SVG.ARROW_RIGHT.getPath());
|
||||
BorderPane.setAlignment(arrow, Pos.CENTER);
|
||||
graphic.setRight(arrow);
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
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;
|
||||
@@ -35,13 +34,13 @@ public class AboutPage extends StackPane {
|
||||
ComponentList about = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem launcher = new IconedTwoLineListItem();
|
||||
launcher.setImage(new Image("/assets/img/craft_table.png"));
|
||||
launcher.setImage(FXUtils.newBuiltinImage("/assets/img/icon.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.png"));
|
||||
author.setImage(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
|
||||
author.setTitle("huanghongxun");
|
||||
author.setSubtitle(i18n("about.author.statement"));
|
||||
author.setExternalLink("https://space.bilibili.com/1445341");
|
||||
@@ -52,72 +51,90 @@ public class AboutPage extends StackPane {
|
||||
ComponentList thanks = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
|
||||
yushijinhun.setImage(new Image("/assets/img/yushijinhun.png"));
|
||||
yushijinhun.setImage(FXUtils.newBuiltinImage("/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.png"));
|
||||
bangbang93.setImage(FXUtils.newBuiltinImage("/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.png"));
|
||||
glavo.setImage(FXUtils.newBuiltinImage("/assets/img/glavo.png"));
|
||||
glavo.setTitle("Glavo");
|
||||
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
|
||||
glavo.setExternalLink("https://github.com/Glavo");
|
||||
|
||||
|
||||
IconedTwoLineListItem zekerzhayard = new IconedTwoLineListItem();
|
||||
zekerzhayard.setImage(FXUtils.newBuiltinImage("/assets/img/zekerzhayard.png"));
|
||||
zekerzhayard.setTitle("ZekerZhayard");
|
||||
zekerzhayard.setSubtitle(i18n("about.thanks_to.zekerzhayard.statement"));
|
||||
zekerzhayard.setExternalLink("https://github.com/ZekerZhayard");
|
||||
|
||||
IconedTwoLineListItem zkitefly = new IconedTwoLineListItem();
|
||||
zkitefly.setImage(FXUtils.newBuiltinImage("/assets/img/zkitefly.png"));
|
||||
zkitefly.setTitle("Zkitefly");
|
||||
zkitefly.setSubtitle(i18n("about.thanks_to.zkitefly.statement"));
|
||||
zkitefly.setExternalLink("https://github.com/zkitefly");
|
||||
|
||||
IconedTwoLineListItem burningtnt = new IconedTwoLineListItem();
|
||||
burningtnt.setImage(FXUtils.newBuiltinImage("/assets/img/burningtnt.png"));
|
||||
burningtnt.setTitle("Burning_TNT");
|
||||
burningtnt.setSubtitle(i18n("about.thanks_to.burningtnt.statement"));
|
||||
burningtnt.setExternalLink("https://github.com/burningtnt");
|
||||
|
||||
IconedTwoLineListItem shulkerSakura = new IconedTwoLineListItem();
|
||||
shulkerSakura.setTitle("ShulkerSakura");
|
||||
shulkerSakura.setImage(FXUtils.newBuiltinImage("/assets/img/ShulkerSakura.png"));
|
||||
shulkerSakura.setSubtitle(i18n("about.thanks_to.shulkersakura.statement"));
|
||||
shulkerSakura.setExternalLink("https://github.com/ShulkerSakura");
|
||||
|
||||
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
|
||||
gamerteam.setTitle("gamerteam");
|
||||
gamerteam.setImage(new Image("/assets/img/gamerteam.png"));
|
||||
gamerteam.setImage(FXUtils.newBuiltinImage("/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.png"));
|
||||
redLnn.setImage(FXUtils.newBuiltinImage("/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"));
|
||||
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"));
|
||||
mcmod.setImage(FXUtils.newBuiltinImage("/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"));
|
||||
contributors.setImage(FXUtils.newBuiltinImage("/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");
|
||||
contributors.setExternalLink("https://github.com/HMCL-dev/HMCL/graphs/contributors");
|
||||
|
||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
||||
users.setImage(new Image("/assets/img/craft_table.png"));
|
||||
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.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");
|
||||
users.setExternalLink("https://docs.hmcl.net/groups.html");
|
||||
|
||||
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, mcbbs, mcmod, gamerteam, redLnn, contributors, users);
|
||||
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, zekerzhayard, zkitefly, burningtnt, mcmod, shulkerSakura, gamerteam, redLnn, contributors, users);
|
||||
}
|
||||
|
||||
ComponentList dep = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
|
||||
javafx.setTitle("JavaFX");
|
||||
javafx.setSubtitle("Copyright © 2013, 2023, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
|
||||
javafx.setSubtitle("Copyright © 2013, 2024, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
|
||||
javafx.setExternalLink("https://openjfx.io/");
|
||||
|
||||
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
|
||||
jfoenix.setTitle("JFoenix");
|
||||
jfoenix.setSubtitle("Copyright © 2016 JFoenix.\nLicensed under the MIT License.");
|
||||
jfoenix.setExternalLink("http://www.jfoenix.com/");
|
||||
jfoenix.setExternalLink("https://github.com/sshahine/JFoenix");
|
||||
|
||||
IconedTwoLineListItem gson = new IconedTwoLineListItem();
|
||||
gson.setTitle("Gson");
|
||||
@@ -127,7 +144,7 @@ public class AboutPage extends StackPane {
|
||||
IconedTwoLineListItem xz = new IconedTwoLineListItem();
|
||||
xz.setTitle("XZ for Java");
|
||||
xz.setSubtitle("Lasse Collin, Igor Pavlov, and/or Brett Okken.\nPublic Domain.");
|
||||
xz.setExternalLink("https://tukaani.org/xz/java.html");
|
||||
xz.setExternalLink("https://xz.tukaani.org/xz-for-java/");
|
||||
|
||||
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
|
||||
fxgson.setTitle("fx-gson");
|
||||
@@ -142,7 +159,7 @@ public class AboutPage extends StackPane {
|
||||
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
|
||||
openNBT.setTitle("OpenNBT");
|
||||
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
|
||||
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
|
||||
openNBT.setExternalLink("https://github.com/GeyserMC/OpenNBT");
|
||||
|
||||
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
|
||||
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
|
||||
@@ -167,7 +184,7 @@ public class AboutPage extends StackPane {
|
||||
IconedTwoLineListItem openSource = new IconedTwoLineListItem();
|
||||
openSource.setTitle(i18n("about.open_source"));
|
||||
openSource.setSubtitle(i18n("about.open_source.statement"));
|
||||
openSource.setExternalLink("https://github.com/huanghongxun/HMCL");
|
||||
openSource.setExternalLink("https://github.com/HMCL-dev/HMCL");
|
||||
|
||||
legal.getContent().setAll(copyright, claim, openSource);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
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.construct.ComponentList;
|
||||
@@ -43,25 +42,25 @@ public class FeedbackPage extends SpinnerPane {
|
||||
ComponentList community = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
||||
users.setImage(new Image("/assets/img/craft_table.png"));
|
||||
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
|
||||
users.setTitle(i18n("feedback.qq_group"));
|
||||
users.setSubtitle(i18n("feedback.qq_group.statement"));
|
||||
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||
users.setExternalLink("https://docs.hmcl.net/groups.html");
|
||||
|
||||
IconedTwoLineListItem github = new IconedTwoLineListItem();
|
||||
github.setImage(new Image("/assets/img/github.png"));
|
||||
github.setImage(FXUtils.newBuiltinImage("/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");
|
||||
github.setExternalLink("https://github.com/HMCL-dev/HMCL/issues/new/choose");
|
||||
|
||||
IconedTwoLineListItem discord = new IconedTwoLineListItem();
|
||||
discord.setImage(new Image("/assets/img/discord.png"));
|
||||
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png"));
|
||||
discord.setTitle(i18n("feedback.discord"));
|
||||
discord.setSubtitle(i18n("feedback.discord.statement"));
|
||||
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
||||
|
||||
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
|
||||
kookapp.setImage(new Image("/assets/img/kookapp.png"));
|
||||
kookapp.setImage(FXUtils.newBuiltinImage("/assets/img/kookapp.png"));
|
||||
kookapp.setTitle(i18n("feedback.kookapp"));
|
||||
kookapp.setSubtitle(i18n("feedback.kookapp.statement"));
|
||||
kookapp.setExternalLink("https://kook.top/Kx7n3t");
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
@@ -45,7 +46,6 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
private final TabHeader.Tab<HelpPage> helpTab = new TabHeader.Tab<>("helpPage");
|
||||
private final TabHeader.Tab<AboutPage> aboutTab = new TabHeader.Tab<>("aboutPage");
|
||||
private final TabHeader.Tab<FeedbackPage> feedbackTab = new TabHeader.Tab<>("feedbackPage");
|
||||
private final TabHeader.Tab<SponsorPage> sponsorTab = new TabHeader.Tab<>("sponsorPage");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
public LauncherSettingsPage() {
|
||||
@@ -55,9 +55,8 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
|
||||
helpTab.setNodeSupplier(HelpPage::new);
|
||||
feedbackTab.setNodeSupplier(FeedbackPage::new);
|
||||
sponsorTab.setNodeSupplier(SponsorPage::new);
|
||||
aboutTab.setNodeSupplier(AboutPage::new);
|
||||
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab);
|
||||
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
|
||||
|
||||
tab.select(gameTab);
|
||||
gameTab.initializeIfNeeded();
|
||||
@@ -70,51 +69,46 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("settings.type.global.manage"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG::gamepad));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.GAMEPAD));
|
||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
|
||||
runInFX(() -> FXUtils.installFastTooltip(settingsItem, i18n("settings.type.global.manage")));
|
||||
settingsItem.setOnAction(e -> tab.select(gameTab));
|
||||
})
|
||||
.startCategory(i18n("launcher"))
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("settings.launcher.general"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG::applicationOutline));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.APPLICATION_OUTLINE));
|
||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab));
|
||||
settingsItem.setOnAction(e -> tab.select(settingsTab));
|
||||
})
|
||||
.addNavigationDrawerItem(personalizationItem -> {
|
||||
personalizationItem.setTitle(i18n("settings.launcher.appearance"));
|
||||
personalizationItem.setLeftGraphic(wrap(SVG::styleOutline));
|
||||
personalizationItem.setLeftGraphic(wrap(SVG.STYLE_OUTLINE));
|
||||
personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab));
|
||||
personalizationItem.setOnAction(e -> tab.select(personalizationTab));
|
||||
})
|
||||
.addNavigationDrawerItem(downloadItem -> {
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
|
||||
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD_OUTLINE));
|
||||
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
|
||||
downloadItem.setOnAction(e -> tab.select(downloadTab));
|
||||
})
|
||||
.startCategory(i18n("help"))
|
||||
.addNavigationDrawerItem(helpItem -> {
|
||||
helpItem.setTitle(i18n("help"));
|
||||
helpItem.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
helpItem.setLeftGraphic(wrap(SVG.HELP_CIRCLE_OUTLINE));
|
||||
helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab));
|
||||
helpItem.setOnAction(e -> tab.select(helpTab));
|
||||
})
|
||||
.addNavigationDrawerItem(feedbackItem -> {
|
||||
feedbackItem.setTitle(i18n("feedback"));
|
||||
feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
||||
feedbackItem.setLeftGraphic(wrap(SVG.MESSAGE_ALERT_OUTLINE));
|
||||
feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab));
|
||||
feedbackItem.setOnAction(e -> tab.select(feedbackTab));
|
||||
})
|
||||
.addNavigationDrawerItem(sponsorItem -> {
|
||||
sponsorItem.setTitle(i18n("sponsor"));
|
||||
sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline));
|
||||
sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab));
|
||||
sponsorItem.setOnAction(e -> tab.select(sponsorTab));
|
||||
})
|
||||
.addNavigationDrawerItem(aboutItem -> {
|
||||
aboutItem.setTitle(i18n("about"));
|
||||
aboutItem.setLeftGraphic(wrap(SVG::informationOutline));
|
||||
aboutItem.setLeftGraphic(wrap(SVG.INFORMATION_OUTLINE));
|
||||
aboutItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(aboutTab));
|
||||
aboutItem.setOnAction(e -> tab.select(aboutTab));
|
||||
});
|
||||
|
||||
@@ -30,7 +30,6 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
@@ -59,16 +58,14 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
@@ -95,18 +92,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
titleNode.setPadding(new Insets(0, 0, 0, 2));
|
||||
titleNode.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
ImageView titleIcon = new ImageView();
|
||||
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) {
|
||||
// JavaFX 8 has some problems with @2x images
|
||||
// Force load the original icon
|
||||
try (InputStream is = MainPage.class.getResourceAsStream("/assets/img/icon.png")) {
|
||||
titleIcon.setImage(new Image(is, 20, 20, false, false));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
} else {
|
||||
titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false));
|
||||
}
|
||||
|
||||
ImageView titleIcon = new ImageView(FXUtils.newBuiltinImage("/assets/img/icon-title.png"));
|
||||
Label titleLabel = new Label(Metadata.FULL_TITLE);
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleNode.getChildren().setAll(titleIcon, titleLabel);
|
||||
@@ -142,7 +128,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
StackPane.setMargin(hBox, new Insets(9, 12, 9, 16));
|
||||
{
|
||||
Label lblIcon = new Label();
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
lblIcon.setGraphic(SVG.UPDATE.createIcon(Theme.whiteFill(), 20, 20));
|
||||
|
||||
TwoLineListItem prompt = new TwoLineListItem();
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
@@ -154,7 +140,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
JFXButton closeUpdateButton = new JFXButton();
|
||||
closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10));
|
||||
closeUpdateButton.setGraphic(SVG.CLOSE.createIcon(Theme.whiteFill(), 10, 10));
|
||||
StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT);
|
||||
closeUpdateButton.getStyleClass().add("toggle-icon-tiny");
|
||||
StackPane.setMargin(closeUpdateButton, new Insets(5));
|
||||
@@ -223,10 +209,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
menuButton.setOnMouseClicked(e -> onMenu());
|
||||
menuButton.setClip(new Rectangle(211, -100, 100, 200));
|
||||
StackPane graphic = new StackPane();
|
||||
Node svg = SVG.triangle(Theme.foregroundFillBinding(), 10, 10);
|
||||
Node svg = SVG.TRIANGLE.createIcon(Theme.foregroundFillBinding(), 10, 10);
|
||||
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
|
||||
graphic.getChildren().setAll(svg);
|
||||
graphic.setTranslateX(12);
|
||||
runInFX(() -> FXUtils.installFastTooltip(menuButton, i18n("version.switch")));
|
||||
menuButton.setGraphic(graphic);
|
||||
|
||||
launchPane.getChildren().setAll(launchButton, separator, menuButton);
|
||||
|
||||
@@ -202,7 +202,7 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
JFXButton clearButton = new JFXButton();
|
||||
clearButton.getStyleClass().add("toggle-icon4");
|
||||
clearButton.setGraphic(SVG.restore(Theme.blackFillBinding(), -1, -1));
|
||||
clearButton.setGraphic(SVG.RESTORE.createIcon(Theme.blackFill(), -1, -1));
|
||||
clearButton.setOnAction(e -> config().setLauncherFontFamily(null));
|
||||
|
||||
hBox.getChildren().setAll(cboFont, clearButton);
|
||||
|
||||
@@ -34,27 +34,32 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTHelper;
|
||||
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
@@ -85,12 +90,24 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
public MainPage getMainPage() {
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
FXUtils.applyDragListener(mainPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack),
|
||||
i18n("install.modpack"));
|
||||
});
|
||||
FXUtils.applyDragListener(mainPage,
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTHelper.isNBTFileByExtension(file),
|
||||
modpacks -> {
|
||||
File file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file),
|
||||
i18n("install.modpack"));
|
||||
} else if (NBTHelper.isNBTFileByExtension(file)) {
|
||||
try {
|
||||
Controllers.navigate(new NBTEditorPage(file));
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Fail to open nbt file", e);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
|
||||
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame);
|
||||
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
|
||||
@@ -101,9 +118,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
List<Version> children = repository.getVersions().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator
|
||||
.comparing((Version version) -> version.getReleaseTime() == null ? new Date(0L)
|
||||
: version.getReleaseTime())
|
||||
.thenComparing(a -> VersionNumber.asVersion(a.getId())))
|
||||
.comparing((Version version) -> Lang.requireNonNullElse(version.getReleaseTime(), Instant.EPOCH))
|
||||
.thenComparing(version -> VersionNumber.asVersion(repository.getGameVersion(version).orElse(version.getId()))))
|
||||
.collect(Collectors.toList());
|
||||
runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile())
|
||||
@@ -139,38 +155,33 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
gameItem.setLeftGraphic(wrap(SVG::viewList));
|
||||
gameItem.setLeftGraphic(wrap(SVG.VIEW_LIST));
|
||||
gameItem.setActionButtonVisible(false);
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
|
||||
|
||||
// forth item in left sidebar
|
||||
AdvancedListItem downloadItem = new AdvancedListItem();
|
||||
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
|
||||
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD_OUTLINE));
|
||||
downloadItem.setActionButtonVisible(false);
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
runInFX(() -> FXUtils.installFastTooltip(downloadItem, i18n("download.hint")));
|
||||
|
||||
// fifth item in left sidebar
|
||||
AdvancedListItem multiplayerItem = new AdvancedListItem();
|
||||
multiplayerItem.setLeftGraphic(wrap(SVG::lan));
|
||||
multiplayerItem.setActionButtonVisible(false);
|
||||
multiplayerItem.setTitle(i18n("multiplayer"));
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("multiplayer.hint.details"));
|
||||
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)
|
||||
.ok(null)
|
||||
.build()));
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setLeftGraphic(wrap(SVG::gearOutline));
|
||||
launcherSettingsItem.setLeftGraphic(wrap(SVG.GEAR_OUTLINE));
|
||||
launcherSettingsItem.setActionButtonVisible(false);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem chatItem = new AdvancedListItem();
|
||||
chatItem.setLeftGraphic(wrap(SVG.CHAT));
|
||||
chatItem.setActionButtonVisible(false);
|
||||
chatItem.setTitle(i18n("chat"));
|
||||
chatItem.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/groups.html"));
|
||||
|
||||
// the left sidebar
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.startCategory(i18n("account").toUpperCase(Locale.ROOT))
|
||||
@@ -180,8 +191,9 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
.add(gameItem)
|
||||
.add(downloadItem)
|
||||
.startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT))
|
||||
.add(multiplayerItem)
|
||||
.add(launcherSettingsItem);
|
||||
.add(launcherSettingsItem)
|
||||
.add(chatItem)
|
||||
;
|
||||
|
||||
// the root page, with the sidebar in left, navigator in center.
|
||||
setLeft(sideBar);
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
@@ -132,7 +133,7 @@ public final class SettingsPage extends SettingsView {
|
||||
try {
|
||||
Files.write(logFile, Logging.getRawLogs());
|
||||
} catch (IOException e) {
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + e, null, MessageType.ERROR));
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
LOG.log(Level.WARNING, "Failed to export logs", e);
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +145,7 @@ public final class SettingsPage extends SettingsView {
|
||||
|
||||
@Override
|
||||
protected void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||
FXUtils.openLink("https://github.com/HMCL-dev/HMCL");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -121,7 +122,7 @@ public abstract class SettingsView extends StackPane {
|
||||
btnUpdate = new JFXButton();
|
||||
btnUpdate.setOnMouseClicked(e -> onUpdate());
|
||||
btnUpdate.getStyleClass().add("toggle-icon4");
|
||||
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 20, 20));
|
||||
btnUpdate.setGraphic(SVG.UPDATE.createIcon(Theme.blackFill(), 20, 20));
|
||||
|
||||
updatePane.setHeaderRight(btnUpdate);
|
||||
}
|
||||
@@ -176,8 +177,8 @@ public abstract class SettingsView extends StackPane {
|
||||
languagePane.setLeft(left);
|
||||
|
||||
cboLanguage = new JFXComboBox<>();
|
||||
cboLanguage.setConverter(stringConverter(locale -> locale.getName(config().getLocalization().getResourceBundle())));
|
||||
FXUtils.setLimitWidth(cboLanguage, 400);
|
||||
cboLanguage.setConverter(stringConverter(locale -> locale.getName(I18n.getCurrentLocale().getResourceBundle())));
|
||||
FXUtils.setLimitWidth(cboLanguage, 300);
|
||||
languagePane.setRight(cboLanguage);
|
||||
|
||||
settingsPane.getContent().add(languagePane);
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.jfoenix.controls.JFXListCell;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
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;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class SponsorPage extends StackPane {
|
||||
private final JFXListView<Sponsor> listView;
|
||||
|
||||
public SponsorPage() {
|
||||
VBox content = new VBox();
|
||||
content.setPadding(new Insets(10));
|
||||
content.setSpacing(10);
|
||||
content.setFillWidth(true);
|
||||
|
||||
{
|
||||
StackPane sponsorPane = new StackPane();
|
||||
sponsorPane.getStyleClass().add("card");
|
||||
sponsorPane.setCursor(Cursor.HAND);
|
||||
sponsorPane.setOnMouseClicked(e -> onSponsor());
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
|
||||
ColumnConstraints col = new ColumnConstraints();
|
||||
col.setHgrow(Priority.SOMETIMES);
|
||||
col.setMaxWidth(Double.POSITIVE_INFINITY);
|
||||
|
||||
gridPane.getColumnConstraints().setAll(col);
|
||||
|
||||
RowConstraints row = new RowConstraints();
|
||||
row.setMinHeight(Double.NEGATIVE_INFINITY);
|
||||
row.setValignment(VPos.TOP);
|
||||
row.setVgrow(Priority.SOMETIMES);
|
||||
gridPane.getRowConstraints().setAll(row);
|
||||
|
||||
{
|
||||
Label label = new Label(i18n("sponsor.hmcl"));
|
||||
label.setWrapText(true);
|
||||
label.setTextAlignment(TextAlignment.JUSTIFY);
|
||||
GridPane.setRowIndex(label, 0);
|
||||
GridPane.setColumnIndex(label, 0);
|
||||
gridPane.getChildren().add(label);
|
||||
}
|
||||
|
||||
sponsorPane.getChildren().setAll(gridPane);
|
||||
content.getChildren().add(sponsorPane);
|
||||
}
|
||||
|
||||
{
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("card");
|
||||
listView = new JFXListView<>();
|
||||
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.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (!empty) {
|
||||
setText(item.getName());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
VBox.setVgrow(pane, Priority.ALWAYS);
|
||||
pane.getChildren().setAll(listView);
|
||||
content.getChildren().add(pane);
|
||||
}
|
||||
|
||||
loadSponsorList();
|
||||
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||
}
|
||||
|
||||
private void loadSponsorList() {
|
||||
Task.<List<Sponsor>>supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/sponsor").getJson(new TypeToken<List<Sponsor>>() {
|
||||
}.getType())).thenAcceptAsync(Schedulers.javafx(), sponsors -> {
|
||||
listView.getItems().setAll(sponsors);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static class Sponsor {
|
||||
@SerializedName("name")
|
||||
private final String name;
|
||||
|
||||
@SerializedName("create_time")
|
||||
private final Date createTime;
|
||||
|
||||
@SerializedName("money")
|
||||
private final BigDecimal money;
|
||||
|
||||
@SerializedName("contact")
|
||||
private final String contact;
|
||||
|
||||
@SerializedName("afdian_id")
|
||||
private final String afdianId;
|
||||
|
||||
public Sponsor() {
|
||||
this("", new Date(), BigDecimal.ZERO, "", "");
|
||||
}
|
||||
|
||||
public Sponsor(String name, Date createTime, BigDecimal money, String contact, String afdianId) {
|
||||
this.name = name;
|
||||
this.createTime = createTime;
|
||||
this.money = money;
|
||||
this.contact = contact;
|
||||
this.afdianId = afdianId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Date getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public BigDecimal getMoney() {
|
||||
return money;
|
||||
}
|
||||
|
||||
public String getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public String getAfdianId() {
|
||||
return afdianId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
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.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
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 NBTEditorPage extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state;
|
||||
private final File file;
|
||||
private final NBTFileType type;
|
||||
|
||||
public NBTEditorPage(File file) throws IOException {
|
||||
getStyleClass().add("gray-background");
|
||||
|
||||
this.state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("nbt.title", file.getAbsolutePath())));
|
||||
this.file = file;
|
||||
this.type = NBTFileType.ofFile(file);
|
||||
|
||||
if (type == null) {
|
||||
throw new IOException("Unknown type of file " + file);
|
||||
}
|
||||
|
||||
setCenter(new ProgressIndicator());
|
||||
|
||||
HBox actions = new HBox(8);
|
||||
actions.setPadding(new Insets(8));
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton saveButton = new JFXButton(i18n("button.save"));
|
||||
saveButton.getStyleClass().add("jfx-button-raised");
|
||||
saveButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
saveButton.setOnAction(e -> {
|
||||
try {
|
||||
save();
|
||||
} catch (IOException ex) {
|
||||
LOG.log(Level.WARNING, "Failed to save NBT file", ex);
|
||||
Controllers.dialog(i18n("nbt.save.failed") + "\n\n" + StringUtils.getStackTrace(ex));
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("jfx-button-raised");
|
||||
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
actions.getChildren().setAll(saveButton, cancelButton);
|
||||
|
||||
CompletableFuture.supplyAsync(Lang.wrap(() -> type.readAsTree(file)))
|
||||
.thenAcceptAsync(tree -> {
|
||||
setCenter(new NBTTreeView(tree));
|
||||
// setBottom(actions);
|
||||
}, Schedulers.javafx())
|
||||
.handleAsync((result, e) -> {
|
||||
if (e != null) {
|
||||
LOG.log(Level.WARNING, "Fail to open nbt file", e);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
|
||||
}
|
||||
return null;
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.IntTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import org.apache.commons.compress.utils.BoundedInputStream;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("dat", "dat_old") {
|
||||
@Override
|
||||
public Tag read(File file) throws IOException {
|
||||
try (BufferedInputStream fileInputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
fileInputStream.mark(3);
|
||||
byte[] header = new byte[3];
|
||||
if (fileInputStream.read(header) < 3) {
|
||||
throw new IOException("File is too small");
|
||||
}
|
||||
fileInputStream.reset();
|
||||
|
||||
InputStream input;
|
||||
if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) {
|
||||
input = new GZIPInputStream(fileInputStream);
|
||||
} else {
|
||||
input = fileInputStream;
|
||||
}
|
||||
|
||||
Tag tag = NBTIO.readTag(input);
|
||||
if (!(tag instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + tag);
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
},
|
||||
ANVIL("mca") {
|
||||
@Override
|
||||
public Tag read(File file) throws IOException {
|
||||
return REGION.read(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(File file) throws IOException {
|
||||
return REGION.readAsTree(file);
|
||||
}
|
||||
},
|
||||
REGION("mcr") {
|
||||
@Override
|
||||
public Tag read(File file) throws IOException {
|
||||
try (RandomAccessFile r = new RandomAccessFile(file, "r")) {
|
||||
byte[] header = new byte[4096];
|
||||
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
|
||||
Inflater inflater = new Inflater();
|
||||
|
||||
ListTag tag = new ListTag(file.getName(), CompoundTag.class);
|
||||
|
||||
r.readFully(header);
|
||||
for (int i = 0; i < 4096; i += 4) {
|
||||
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
|
||||
int length = header[i + 3] & 0xff;
|
||||
|
||||
if (offset == 0 || length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
r.seek(offset * 4096L);
|
||||
r.readFully(buffer, 0, length * 4096);
|
||||
|
||||
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
|
||||
|
||||
InputStream input = new ByteArrayInputStream(buffer);
|
||||
input.skip(5);
|
||||
input = new BoundedInputStream(input, chunkLength - 1);
|
||||
|
||||
switch (buffer[4]) {
|
||||
case 0x01:
|
||||
// GZip
|
||||
input = new GZIPInputStream(input);
|
||||
break;
|
||||
case 0x02:
|
||||
// Zlib
|
||||
inflater.reset();
|
||||
input = new InflaterInputStream(input, inflater);
|
||||
break;
|
||||
case 0x03:
|
||||
// Uncompressed
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
|
||||
}
|
||||
|
||||
try (InputStream in = input) {
|
||||
Tag chunk = NBTIO.readTag(in);
|
||||
if (!(chunk instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + chunk);
|
||||
|
||||
tag.add(chunk);
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(File file) throws IOException {
|
||||
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
|
||||
|
||||
for (Tag tag : ((ListTag) item.getValue())) {
|
||||
CompoundTag chunk = (CompoundTag) tag;
|
||||
|
||||
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
|
||||
|
||||
Tag xPos = chunk.get("xPos");
|
||||
Tag zPos = chunk.get("zPos");
|
||||
|
||||
if (xPos instanceof IntTag && zPos instanceof IntTag) {
|
||||
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
|
||||
} else {
|
||||
tree.setText("Chunk: Unknown");
|
||||
}
|
||||
|
||||
item.getChildren().add(tree);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
static final NBTFileType[] types = values();
|
||||
|
||||
public static NBTFileType ofFile(File file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
for (NBTFileType type : types) {
|
||||
for (String extension : type.extensions) {
|
||||
if (extension.equals(ext))
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private final String[] extensions;
|
||||
|
||||
NBTFileType(String... extensions) {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
public abstract Tag read(File file) throws IOException;
|
||||
|
||||
public NBTTreeView.Item readAsTree(File file) throws IOException {
|
||||
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
|
||||
root.setName(file.getName());
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class NBTHelper {
|
||||
private NBTHelper() {
|
||||
}
|
||||
|
||||
public static boolean isNBTFileByExtension(File file) {
|
||||
return NBTFileType.ofFile(file) != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public enum NBTTagType {
|
||||
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
|
||||
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
|
||||
STRING,
|
||||
LIST, COMPOUND;
|
||||
|
||||
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (NBTTagType type : values()) {
|
||||
lookupTable.put(type.getTagClassName(), type);
|
||||
}
|
||||
}
|
||||
|
||||
public static NBTTagType typeOf(Tag tag) {
|
||||
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Unknown tag: " + type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private final String iconUrl;
|
||||
private final String tagClassName;
|
||||
|
||||
NBTTagType() {
|
||||
String tagName;
|
||||
String className;
|
||||
|
||||
int idx = name().indexOf('_');
|
||||
if (idx < 0) {
|
||||
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
|
||||
className = tagName + "Tag";
|
||||
} else {
|
||||
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
|
||||
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
|
||||
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
|
||||
}
|
||||
|
||||
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
|
||||
this.tagClassName = className;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public String getTagClassName() {
|
||||
return tagClassName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
return view -> new TreeCell<Tag>() {
|
||||
private void setTagText(String text) {
|
||||
String name = ((Item) getTreeItem()).getName();
|
||||
|
||||
if (name == null) {
|
||||
setText(text);
|
||||
} else if (text == null) {
|
||||
setText(name);
|
||||
} else {
|
||||
setText(name + ": " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagText(int nEntries) {
|
||||
setTagText(i18n("nbt.entries", nEntries));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(Tag item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
ImageView imageView = (ImageView) this.getGraphic();
|
||||
if (imageView == null) {
|
||||
imageView = new ImageView();
|
||||
this.setGraphic(imageView);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
imageView.setImage(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
NBTTagType tagType = NBTTagType.typeOf(item);
|
||||
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
|
||||
|
||||
if (((Item) getTreeItem()).getText() != null) {
|
||||
setText(((Item) getTreeItem()).getText());
|
||||
} else {
|
||||
switch (tagType) {
|
||||
case BYTE:
|
||||
case SHORT:
|
||||
case INT:
|
||||
case LONG:
|
||||
case FLOAT:
|
||||
case DOUBLE:
|
||||
case STRING:
|
||||
setTagText(item.getValue().toString());
|
||||
break;
|
||||
case BYTE_ARRAY:
|
||||
case INT_ARRAY:
|
||||
case LONG_ARRAY:
|
||||
setTagText(Array.getLength(item.getValue()));
|
||||
break;
|
||||
case LIST:
|
||||
setTagText(((ListTag) item).size());
|
||||
break;
|
||||
case COMPOUND:
|
||||
setTagText(((CompoundTag) item).size());
|
||||
break;
|
||||
default:
|
||||
setTagText(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Item buildTree(Tag tag) {
|
||||
Item item = new Item(tag);
|
||||
|
||||
if (tag instanceof CompoundTag) {
|
||||
for (Tag subTag : ((CompoundTag) tag)) {
|
||||
item.getChildren().add(buildTree(subTag));
|
||||
}
|
||||
} else if (tag instanceof ListTag) {
|
||||
int idx = 0;
|
||||
for (Tag subTag : ((ListTag) tag)) {
|
||||
Item subTree = buildTree(subTag);
|
||||
subTree.setName(String.valueOf(idx++));
|
||||
item.getChildren().add(subTree);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public CompoundTag getRootTag() {
|
||||
return ((CompoundTag) getRoot().getValue());
|
||||
}
|
||||
|
||||
public static class Item extends TreeItem<Tag> {
|
||||
|
||||
private String text;
|
||||
private String name;
|
||||
|
||||
public Item() {
|
||||
}
|
||||
|
||||
public Item(Tag value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name == null ? getValue().getName() : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
getSkinnable().setSelected(true);
|
||||
});
|
||||
|
||||
Node left = VersionPage.wrap(SVG::folderOutline);
|
||||
Node left = VersionPage.wrap(SVG.FOLDER_OUTLINE);
|
||||
root.setLeft(left);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
|
||||
btnRemove.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 14, 14));
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.bindString(txtEnvironmentVariables, versionSetting.environmentVariablesProperty());
|
||||
FXUtils.bindString(txtWrapper, versionSetting.wrapperProperty());
|
||||
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
FXUtils.bindEnum(cboRenderer, versionSetting.rendererProperty());
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
|
||||
@@ -86,7 +86,7 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
|
||||
.withRunAsync(Schedulers.javafx(), () -> {
|
||||
setLoading(false);
|
||||
|
||||
// https://github.com/huanghongxun/HMCL/issues/938
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
})
|
||||
.start();
|
||||
|
||||
@@ -56,16 +56,16 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setPickOnBounds(false);
|
||||
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG::refresh, skinnable::refresh));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG::plus, skinnable::add));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("button.remove"), SVG::delete, () -> {
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("datapack.add"), SVG.PLUS, skinnable::add));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG::check, () ->
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())));
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG::close, () ->
|
||||
toolbar.getChildren().add(createToolbarButton(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())));
|
||||
root.setTop(toolbar);
|
||||
}
|
||||
|
||||
@@ -35,10 +35,8 @@ import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -58,8 +56,8 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -69,6 +67,7 @@ import java.util.stream.Collectors;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
|
||||
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
|
||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
@@ -76,6 +75,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
private final BooleanProperty failed = new SimpleBooleanProperty(false);
|
||||
private final boolean versionSelection;
|
||||
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
|
||||
private final IntegerProperty pageOffset = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty pageCount = new SimpleIntegerProperty(-1);
|
||||
private final ListProperty<RemoteMod> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
private final ObservableList<String> versions = FXCollections.observableArrayList();
|
||||
private final StringProperty selectedVersion = new SimpleStringProperty();
|
||||
@@ -153,34 +154,39 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.loading.set(loading);
|
||||
}
|
||||
|
||||
public void selectVersion(String versionID) {
|
||||
FXUtils.runInFX(() -> selectedVersion.set(versionID));
|
||||
}
|
||||
|
||||
public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, RemoteModRepository.SortType sort) {
|
||||
retrySearch = null;
|
||||
setLoading(true);
|
||||
setFailed(false);
|
||||
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
|
||||
? version.get().getProfile().getRepository().getVersionJar(version.get().getVersion())
|
||||
: null;
|
||||
|
||||
if (executor != null && !executor.isCancelled()) {
|
||||
executor.cancel();
|
||||
}
|
||||
|
||||
executor = Task.supplyAsync(() -> {
|
||||
String gameVersion;
|
||||
if (StringUtils.isBlank(version.get().getVersion())) {
|
||||
gameVersion = userGameVersion;
|
||||
Profile.ProfileVersion version = this.version.get();
|
||||
if (StringUtils.isBlank(version.getVersion())) {
|
||||
return userGameVersion;
|
||||
} else {
|
||||
gameVersion = GameVersion.minecraftVersion(versionJar).orElse("");
|
||||
return StringUtils.isNotBlank(version.getVersion())
|
||||
? version.getProfile().getRepository().getGameVersion(version.getVersion()).orElse("")
|
||||
: "";
|
||||
}
|
||||
return gameVersion;
|
||||
}).thenApplyAsync(gameVersion -> {
|
||||
return repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
}).thenApplyAsync(gameVersion ->
|
||||
repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
items.setAll(result.collect(Collectors.toList()));
|
||||
items.setAll(result.getResults().collect(Collectors.toList()));
|
||||
pageCount.set(result.getTotalPages());
|
||||
failed.set(false);
|
||||
} else {
|
||||
failed.set(true);
|
||||
pageCount.set(-1);
|
||||
retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort);
|
||||
}
|
||||
}).executor(true);
|
||||
@@ -220,8 +226,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
}
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final AggregatedObservableList<Node> actions = new AggregatedObservableList<>();
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
@@ -280,11 +284,16 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
JFXTextField nameField = new JFXTextField();
|
||||
nameField.setPromptText(getSkinnable().supportChinese.get() ? i18n("search.hint.chinese") : i18n("search.hint.english"));
|
||||
if (getSkinnable().supportChinese.get()) {
|
||||
FXUtils.installFastTooltip(nameField, i18n("search.hint.chinese"));
|
||||
} else {
|
||||
FXUtils.installFastTooltip(nameField, i18n("search.hint.english"));
|
||||
}
|
||||
|
||||
JFXComboBox<String> gameVersionField = new JFXComboBox<>();
|
||||
gameVersionField.setMaxWidth(Double.MAX_VALUE);
|
||||
gameVersionField.setEditable(true);
|
||||
gameVersionField.getItems().setAll(RemoteModRepository.DEFAULT_GAME_VERSIONS);
|
||||
gameVersionField.getItems().setAll(GameVersionNumber.getDefaultGameVersions());
|
||||
Label lblGameVersion = new Label(i18n("world.game_version"));
|
||||
searchPane.addRow(rowIndex++, new Label(i18n("mods.name")), nameField, lblGameVersion, gameVersionField);
|
||||
|
||||
@@ -294,6 +303,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
lblGameVersion.visibleProperty().bind(hasVersion);
|
||||
gameVersionField.managedProperty().bind(hasVersion);
|
||||
gameVersionField.visibleProperty().bind(hasVersion);
|
||||
runInFX(() -> FXUtils.installFastTooltip(gameVersionField, i18n("search.enter")));
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().version, version -> {
|
||||
if (StringUtils.isNotBlank(version.getVersion())) {
|
||||
@@ -334,25 +344,90 @@ 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 = FXUtils.newRaisedButton(i18n("search"));
|
||||
ObservableList<Node> last = FXCollections.observableArrayList(searchButton);
|
||||
HBox searchBox = new HBox(8);
|
||||
actions.appendList(control.actions);
|
||||
actions.appendList(last);
|
||||
Bindings.bindContent(searchBox.getChildren(), actions.getAggregatedList());
|
||||
GridPane.setColumnSpan(searchBox, 4);
|
||||
searchBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
searchPane.addRow(rowIndex++, searchBox);
|
||||
StringProperty previousSearchFilter = new SimpleStringProperty(this, "Previous Seach Filter", "");
|
||||
EventHandler<ActionEvent> searchAction = e -> {
|
||||
if (!previousSearchFilter.get().equals(nameField.getText())) {
|
||||
control.pageOffset.set(0);
|
||||
}
|
||||
|
||||
previousSearchFilter.set(nameField.getText());
|
||||
getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(),
|
||||
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
|
||||
.map(CategoryIndented::getCategory)
|
||||
.orElse(null),
|
||||
control.pageOffset.get(),
|
||||
nameField.getText(),
|
||||
sortComboBox.getSelectionModel().getSelectedItem());
|
||||
};
|
||||
|
||||
HBox actionsBox = new HBox(8);
|
||||
GridPane.setColumnSpan(actionsBox, 4);
|
||||
actionsBox.setAlignment(Pos.CENTER);
|
||||
{
|
||||
AggregatedObservableList<Node> actions = new AggregatedObservableList<>();
|
||||
|
||||
JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page"));
|
||||
firstPageButton.setOnAction(event -> {
|
||||
control.pageOffset.set(0);
|
||||
searchAction.handle(event);
|
||||
});
|
||||
firstPageButton.setDisable(true);
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> firstPageButton.setDisable(control.pageCount.get() == -1));
|
||||
|
||||
JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page"));
|
||||
previousPageButton.setOnAction(event -> {
|
||||
if (control.pageOffset.get() > 0) {
|
||||
control.pageOffset.set(control.pageOffset.get() - 1);
|
||||
searchAction.handle(event);
|
||||
}
|
||||
});
|
||||
previousPageButton.setDisable(true);
|
||||
control.pageOffset.addListener((observable, oldValue, newValue) -> previousPageButton.setDisable(
|
||||
control.pageCount.get() == -1 || control.pageOffset.get() == 0
|
||||
));
|
||||
|
||||
Label pageOffset = new Label(i18n("search.page_n", 0, "-"));
|
||||
control.pageOffset.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n(
|
||||
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
|
||||
)));
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n(
|
||||
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
|
||||
)));
|
||||
|
||||
JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page"));
|
||||
nextPageButton.setOnAction(event -> {
|
||||
control.pageOffset.set(control.pageOffset.get() + 1);
|
||||
searchAction.handle(event);
|
||||
});
|
||||
nextPageButton.setDisable(true);
|
||||
control.pageOffset.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
|
||||
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
|
||||
));
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
|
||||
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
|
||||
));
|
||||
|
||||
JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page"));
|
||||
lastPageButton.setOnAction(event -> {
|
||||
control.pageOffset.set(control.pageCount.get() - 1);
|
||||
searchAction.handle(event);
|
||||
});
|
||||
lastPageButton.setDisable(true);
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> lastPageButton.setDisable(control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1));
|
||||
|
||||
Pane placeholder = new Pane();
|
||||
HBox.setHgrow(placeholder, Priority.SOMETIMES);
|
||||
|
||||
JFXButton searchButton = FXUtils.newRaisedButton(i18n("search"));
|
||||
searchButton.setOnAction(searchAction);
|
||||
|
||||
actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageOffset, nextPageButton, lastPageButton, placeholder, searchButton));
|
||||
actions.appendList(control.actions);
|
||||
Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList());
|
||||
}
|
||||
|
||||
searchPane.addRow(rowIndex++, actionsBox);
|
||||
|
||||
EventHandler<ActionEvent> searchAction = e -> getSkinnable()
|
||||
.search(gameVersionField.getSelectionModel().getSelectedItem(),
|
||||
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
|
||||
.map(CategoryIndented::getCategory)
|
||||
.orElse(null),
|
||||
0,
|
||||
nameField.getText(),
|
||||
sortComboBox.getSelectionModel().getSelectedItem());
|
||||
searchButton.setOnAction(searchAction);
|
||||
nameField.setOnAction(searchAction);
|
||||
gameVersionField.setOnAction(searchAction);
|
||||
categoryComboBox.setOnAction(searchAction);
|
||||
@@ -395,6 +470,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
pane.getChildren().add(container);
|
||||
|
||||
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -408,7 +484,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
|
||||
imageView.setImage(new Image(dataItem.getIconUrl(), 40, 40, true, true, true));
|
||||
imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
@@ -27,15 +27,14 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -48,12 +47,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
@@ -64,6 +62,7 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class DownloadPage extends Control implements DecoratorPage {
|
||||
@@ -79,8 +78,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
private final DownloadCallback callback;
|
||||
private final DownloadListPage page;
|
||||
|
||||
private List<RemoteMod> dependencies;
|
||||
private SimpleMultimap<String, RemoteMod.Version> versions;
|
||||
private SimpleMultimap<String, RemoteMod.Version, List<RemoteMod.Version>> versions;
|
||||
|
||||
public DownloadPage(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
|
||||
this.page = page;
|
||||
@@ -103,10 +101,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
setLoading(true);
|
||||
setFailed(false);
|
||||
|
||||
Task.allOf(
|
||||
Task.supplyAsync(() -> addon.getData().loadDependencies(repository)),
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
||||
// if (StringUtils.isNotBlank(version.getVersion())) {
|
||||
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||
// if (gameVersion.isPresent()) {
|
||||
@@ -114,30 +110,23 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
|
||||
// }
|
||||
// }
|
||||
return sortVersions(versions);
|
||||
}))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RemoteMod> dependencies = (List<RemoteMod>) result.get(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
SimpleMultimap<String, RemoteMod.Version> versions = (SimpleMultimap<String, RemoteMod.Version>) result.get(1);
|
||||
return sortVersions(versions);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
this.versions = result;
|
||||
|
||||
this.dependencies = dependencies;
|
||||
this.versions = versions;
|
||||
|
||||
loaded.set(true);
|
||||
setFailed(false);
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
setLoading(false);
|
||||
}).start();
|
||||
loaded.set(true);
|
||||
setFailed(false);
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
setLoading(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private SimpleMultimap<String, RemoteMod.Version> sortVersions(Stream<RemoteMod.Version> versions) {
|
||||
SimpleMultimap<String, RemoteMod.Version> classifiedVersions
|
||||
= new SimpleMultimap<String, RemoteMod.Version>(HashMap::new, ArrayList::new);
|
||||
private SimpleMultimap<String, RemoteMod.Version, List<RemoteMod.Version>> sortVersions(Stream<RemoteMod.Version> versions) {
|
||||
SimpleMultimap<String, RemoteMod.Version, List<RemoteMod.Version>> classifiedVersions
|
||||
= new SimpleMultimap<>(HashMap::new, ArrayList::new);
|
||||
versions.forEach(version -> {
|
||||
for (String gameVersion : version.getGameVersions()) {
|
||||
classifiedVersions.put(gameVersion, version);
|
||||
@@ -145,7 +134,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
});
|
||||
|
||||
for (String gameVersion : classifiedVersions.keys()) {
|
||||
List<RemoteMod.Version> versionList = (List<RemoteMod.Version>) classifiedVersions.get(gameVersion);
|
||||
List<RemoteMod.Version> versionList = classifiedVersions.get(gameVersion);
|
||||
versionList.sort(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed());
|
||||
}
|
||||
return classifiedVersions;
|
||||
@@ -244,7 +233,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
{
|
||||
ImageView imageView = new ImageView();
|
||||
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
|
||||
imageView.setImage(new Image(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
|
||||
imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
|
||||
}
|
||||
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
|
||||
|
||||
@@ -262,40 +251,23 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod"));
|
||||
openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod));
|
||||
descriptionPane.getChildren().add(openMcmodButton);
|
||||
openMcmodButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod")));
|
||||
|
||||
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
|
||||
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
|
||||
openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs()));
|
||||
descriptionPane.getChildren().add(openMcbbsButton);
|
||||
}
|
||||
// if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
|
||||
// JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
|
||||
// openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs()));
|
||||
// descriptionPane.getChildren().add(openMcbbsButton);
|
||||
// openMcbbsButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
// runInFX(() -> FXUtils.installFastTooltip(openMcbbsButton, i18n("mods.mcbbs")));
|
||||
// }
|
||||
}
|
||||
|
||||
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
|
||||
openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl());
|
||||
descriptionPane.getChildren().add(openUrlButton);
|
||||
}
|
||||
|
||||
{
|
||||
ComponentList dependencyPane = new ComponentList();
|
||||
dependencyPane.getStyleClass().add("no-padding");
|
||||
|
||||
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
||||
if (loaded) {
|
||||
dependencyPane.getContent().setAll(control.dependencies.stream()
|
||||
.map(dependency -> new DependencyModItem(getSkinnable().page, dependency, control.version, control.callback))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
});
|
||||
|
||||
Node title = ComponentList.createComponentListTitle(i18n("mods.dependencies"));
|
||||
|
||||
BooleanBinding show = Bindings.createBooleanBinding(() -> control.loaded.get() && !control.dependencies.isEmpty(), control.loaded);
|
||||
title.managedProperty().bind(show);
|
||||
title.visibleProperty().bind(show);
|
||||
dependencyPane.managedProperty().bind(show);
|
||||
dependencyPane.visibleProperty().bind(show);
|
||||
|
||||
pane.getChildren().addAll(title, dependencyPane);
|
||||
openUrlButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
runInFX(() -> FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage()));
|
||||
}
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
@@ -319,15 +291,33 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
||||
if (control.versions == null) return;
|
||||
|
||||
if (control.version.getProfile() != null && control.version.getVersion() != null) {
|
||||
HMCLGameRepository repository = control.version.getProfile().getRepository();
|
||||
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(game, repository.getGameVersion(game).orElse(null));
|
||||
libraryAnalyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT).ifPresent(currentGameVersion -> {
|
||||
Set<ModLoaderType> currentGameModLoaders = libraryAnalyzer.getModLoaders();
|
||||
if (control.versions.containsKey(currentGameVersion)) {
|
||||
control.versions.get(currentGameVersion).stream()
|
||||
.filter(version1 -> version1.getLoaders().isEmpty() || version1.getLoaders().stream().anyMatch(currentGameModLoaders::contains))
|
||||
.findFirst()
|
||||
.ifPresent(value -> list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("mods.download.recommend", currentGameVersion)),
|
||||
new ModItem(value, control)
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (String gameVersion : control.versions.keys().stream()
|
||||
.sorted(VersionNumber.VERSION_COMPARATOR.reversed())
|
||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||
.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);
|
||||
sublist.setTitle("Minecraft " + gameVersion);
|
||||
|
||||
list.getContent().add(sublist);
|
||||
}
|
||||
@@ -339,10 +329,19 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
private static final class DependencyModItem extends StackPane {
|
||||
public static final EnumMap<RemoteMod.DependencyType, String> I18N_KEY = new EnumMap<>(Lang.mapOf(
|
||||
Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"),
|
||||
Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"),
|
||||
Pair.pair(RemoteMod.DependencyType.REQUIRED, "mods.dependency.required"),
|
||||
Pair.pair(RemoteMod.DependencyType.TOOL, "mods.dependency.tool"),
|
||||
Pair.pair(RemoteMod.DependencyType.INCLUDE, "mods.dependency.include"),
|
||||
Pair.pair(RemoteMod.DependencyType.INCOMPATIBLE, "mods.dependency.incompatible"),
|
||||
Pair.pair(RemoteMod.DependencyType.BROKEN, "mods.dependency.broken")
|
||||
));
|
||||
|
||||
DependencyModItem(DownloadListPage page, RemoteMod addon, Profile.ProfileVersion version, DownloadCallback callback) {
|
||||
HBox pane = new HBox(8);
|
||||
pane.setPadding(new Insets(8));
|
||||
pane.setPadding(new Insets(0, 8, 0, 8));
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
@@ -361,7 +360,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.setImage(new Image(addon.getIconUrl(), 40, 40, true, true, true));
|
||||
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,47 +368,155 @@ 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));
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
StackPane graphicPane = new StackPane();
|
||||
JFXButton saveAsButton = new JFXButton();
|
||||
VBox pane = new VBox(8);
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
container.setOnMouseClicked(e -> selfPage.download(dataItem));
|
||||
getChildren().setAll(container);
|
||||
{
|
||||
HBox descPane = new HBox(8);
|
||||
descPane.setPadding(new Insets(0, 8, 0, 8));
|
||||
descPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
saveAsButton.getStyleClass().add("toggle-icon4");
|
||||
saveAsButton.setGraphic(SVG.contentSaveMoveOutline(Theme.blackFillBinding(), -1, -1));
|
||||
{
|
||||
StackPane graphicPane = new StackPane();
|
||||
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
|
||||
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
pane.getChildren().setAll(graphicPane, content, saveAsButton);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setTitle(dataItem.getName());
|
||||
content.setSubtitle(FORMATTER.format(dataItem.getDatePublished()));
|
||||
|
||||
content.setTitle(dataItem.getName());
|
||||
content.setSubtitle(FORMATTER.format(dataItem.getDatePublished().toInstant()));
|
||||
saveAsButton.setOnAction(e -> selfPage.saveAs(dataItem));
|
||||
switch (dataItem.getVersionType()) {
|
||||
case Alpha:
|
||||
case Beta:
|
||||
content.getTags().add(i18n("version.game.snapshot"));
|
||||
break;
|
||||
case Release:
|
||||
content.getTags().add(i18n("version.game.release"));
|
||||
break;
|
||||
}
|
||||
|
||||
switch (dataItem.getVersionType()) {
|
||||
case Release:
|
||||
graphicPane.getChildren().setAll(SVG.releaseCircleOutline(Theme.blackFillBinding(), 24, 24));
|
||||
content.getTags().add(i18n("version.game.release"));
|
||||
break;
|
||||
case Beta:
|
||||
graphicPane.getChildren().setAll(SVG.betaCircleOutline(Theme.blackFillBinding(), 24, 24));
|
||||
content.getTags().add(i18n("version.game.snapshot"));
|
||||
break;
|
||||
case Alpha:
|
||||
graphicPane.getChildren().setAll(SVG.alphaCircleOutline(Theme.blackFillBinding(), 24, 24));
|
||||
content.getTags().add(i18n("version.game.snapshot"));
|
||||
break;
|
||||
for (ModLoaderType modLoaderType : dataItem.getLoaders()) {
|
||||
switch (modLoaderType) {
|
||||
case FORGE:
|
||||
content.getTags().add(i18n("install.installer.forge"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.getTags().add(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.getTags().add(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.getTags().add(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.getTags().add(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
descPane.getChildren().setAll(graphicPane, content);
|
||||
}
|
||||
|
||||
pane.getChildren().add(descPane);
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/huanghongxun/HMCL/issues/2129
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
container.setOnMouseClicked(e -> Controllers.dialog(new ModVersion(dataItem, selfPage)));
|
||||
getChildren().setAll(container);
|
||||
|
||||
// Workaround for https://github.com/HMCL-dev/HMCL/issues/2129
|
||||
this.setMinHeight(50);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ModVersion extends JFXDialogLayout {
|
||||
public ModVersion(RemoteMod.Version version, DownloadPage selfPage) {
|
||||
this.setHeading(new HBox(new Label(i18n("mods.download.title", version.getName()))));
|
||||
|
||||
VBox box = new VBox(8);
|
||||
box.setPadding(new Insets(8));
|
||||
ModItem modItem = new ModItem(version, selfPage);
|
||||
modItem.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
box.getChildren().setAll(modItem);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList dependenciesList = new ComponentList(Lang::immutableListOf);
|
||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
spinnerPane.setContent(scrollPane);
|
||||
box.getChildren().add(spinnerPane);
|
||||
VBox.setVgrow(spinnerPane, Priority.SOMETIMES);
|
||||
|
||||
this.setBody(box);
|
||||
|
||||
JFXButton downloadButton = new JFXButton(i18n("download"));
|
||||
downloadButton.getStyleClass().add("dialog-accept");
|
||||
downloadButton.setOnAction(e -> {
|
||||
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
selfPage.download(version);
|
||||
});
|
||||
|
||||
JFXButton saveAsButton = new JFXButton(i18n("button.save_as"));
|
||||
saveAsButton.getStyleClass().add("dialog-accept");
|
||||
saveAsButton.setOnAction(e -> {
|
||||
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
selfPage.saveAs(version);
|
||||
});
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
|
||||
this.setActions(downloadButton, saveAsButton, cancelButton);
|
||||
|
||||
this.prefWidthProperty().bind(BindingMapping.of(Controllers.getStage().widthProperty()).map(w -> w.doubleValue() * 0.7));
|
||||
this.prefHeightProperty().bind(BindingMapping.of(Controllers.getStage().heightProperty()).map(w -> w.doubleValue() * 0.7));
|
||||
}
|
||||
|
||||
private void loadDependencies(RemoteMod.Version version, DownloadPage selfPage, SpinnerPane spinnerPane, ComponentList dependenciesList) {
|
||||
spinnerPane.setLoading(true);
|
||||
Task.supplyAsync(() -> {
|
||||
EnumMap<RemoteMod.DependencyType, List<Node>> dependencies = new EnumMap<>(RemoteMod.DependencyType.class);
|
||||
for (RemoteMod.Dependency dependency : version.getDependencies()) {
|
||||
if (dependency.getType() == RemoteMod.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteMod.DependencyType.BROKEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dependencies.containsKey(dependency.getType())) {
|
||||
List<Node> list = new ArrayList<>();
|
||||
Label title = new Label(i18n(DependencyModItem.I18N_KEY.get(dependency.getType())));
|
||||
title.setPadding(new Insets(0, 8, 0, 8));
|
||||
list.add(title);
|
||||
dependencies.put(dependency.getType(), list);
|
||||
}
|
||||
DependencyModItem dependencyModItem = new DependencyModItem(selfPage.page, dependency.load(), selfPage.version, selfPage.callback);
|
||||
dependencyModItem.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
dependencies.get(dependency.getType()).add(dependencyModItem);
|
||||
}
|
||||
|
||||
return dependencies.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
spinnerPane.setLoading(false);
|
||||
if (exception == null) {
|
||||
dependenciesList.getContent().setAll(result);
|
||||
spinnerPane.setFailedReason(null);
|
||||
} else {
|
||||
dependenciesList.getContent().setAll();
|
||||
spinnerPane.setFailedReason(i18n("download.failed.refresh"));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
|
||||
|
||||
public interface DownloadCallback {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.scene.image.ImageView;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
@@ -30,7 +31,6 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameAdvancedListItem extends AdvancedListItem {
|
||||
@@ -72,7 +72,7 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
Tooltip.uninstall(this,tooltip);
|
||||
setTitle(i18n("version.empty"));
|
||||
setSubtitle(i18n("version.empty.add"));
|
||||
imageView.setImage(newImage("/assets/img/grass.png"));
|
||||
imageView.setImage(VersionIconType.DEFAULT.getIcon());
|
||||
tooltip.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 374 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user