Compare commits
311
Commits
release/3.6
...
v3.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
341c60b0de | ||
|
|
69f19115ea | ||
|
|
ecb4eedd12 | ||
|
|
df6f35bc8e | ||
|
|
58d3780553 | ||
|
|
58b357d4b8 | ||
|
|
d69d27af57 | ||
|
|
f9a39f1c90 | ||
|
|
f8a4261480 | ||
|
|
2c428faa95 | ||
|
|
da91fe15df | ||
|
|
1df962884c | ||
|
|
8ad5d83946 | ||
|
|
74878bda0c | ||
|
|
b32d3304b0 | ||
|
|
60375c9632 | ||
|
|
33877691f8 | ||
|
|
109d6874ab | ||
|
|
a225e7fac4 | ||
|
|
7dad43570e | ||
|
|
8a166266d8 | ||
|
|
c53fbd0efb | ||
|
|
1e74567a22 | ||
|
|
0b9616f460 | ||
|
|
0b89612ec5 | ||
|
|
642893d2e9 | ||
|
|
2e878e43cb | ||
|
|
c8edf4be62 | ||
|
|
96f16382c0 | ||
|
|
11c45e0ab8 | ||
|
|
08f6f973c0 | ||
|
|
35e63fc8c8 | ||
|
|
23037b8afc | ||
|
|
25323dc385 | ||
|
|
496b403eeb | ||
|
|
7be00f2408 | ||
|
|
08d7974e27 | ||
|
|
f16cbed5b4 | ||
|
|
0988dc53cd | ||
|
|
f1c8791961 | ||
|
|
07e5ac9e0e | ||
|
|
c98dbe6694 | ||
|
|
460134cc1e | ||
|
|
9dd117c454 | ||
|
|
c60af8675f | ||
|
|
411efbe774 | ||
|
|
2e09a40b69 | ||
|
|
ba47c2d10d | ||
|
|
fd90ca8afe | ||
|
|
dda30274a4 | ||
|
|
2b43ee24a1 | ||
|
|
66f1e60433 | ||
|
|
3c3e2b8cfc | ||
|
|
365bb089c5 | ||
|
|
87af44328b | ||
|
|
8731faaac7 | ||
|
|
5abc1f1f9d | ||
|
|
40b4ecd42a | ||
|
|
e0f5c4d91e | ||
|
|
35ecd58460 | ||
|
|
f29c2bc202 | ||
|
|
361e928e73 | ||
|
|
484b65b513 | ||
|
|
19f02e3967 | ||
|
|
495da6b6b0 | ||
|
|
168c4ac708 | ||
|
|
7e8a6679cc | ||
|
|
e90d5185f6 | ||
|
|
8a89baee09 | ||
|
|
7f4c5fc4a0 | ||
|
|
75789c774d | ||
|
|
f0a16b3d14 | ||
|
|
710e69751f | ||
|
|
938d8647ce | ||
|
|
4b4dd592c3 | ||
|
|
225dba7448 | ||
|
|
5b1414428e | ||
|
|
cb467ae240 | ||
|
|
9416c0cfb3 | ||
|
|
16ea98c95a | ||
|
|
a8552a92d1 | ||
|
|
36d71bd14e | ||
|
|
cbe6554390 | ||
|
|
1b65e0b6d7 | ||
|
|
bd9ae189f8 | ||
|
|
867a04dfba | ||
|
|
dd7703c7a9 | ||
|
|
756b95ca8c | ||
|
|
ac0a0c7604 | ||
|
|
9ee611c0f6 | ||
|
|
c38371f454 | ||
|
|
eb3be8c465 | ||
|
|
e6ac07f62b | ||
|
|
a24fea4a95 | ||
|
|
8bd104c207 | ||
|
|
a14a265986 | ||
|
|
66524613b4 | ||
|
|
a0568e34a8 | ||
|
|
45f7719096 | ||
|
|
77fadf5d28 | ||
|
|
e9d7d0a33c | ||
|
|
94994ffdb6 | ||
|
|
26e274a89a | ||
|
|
6a497df0d1 | ||
|
|
fc1cce5e5a | ||
|
|
b328ed2cc9 | ||
|
|
5a8d567bd7 | ||
|
|
5005343d00 | ||
|
|
e4bc8f48c5 | ||
|
|
831a8a9bb8 | ||
|
|
7c024473d6 | ||
|
|
f594abc37d | ||
|
|
b66e249591 | ||
|
|
15e490f313 | ||
|
|
7ce59cac2c | ||
|
|
a7178802f8 | ||
|
|
51954163d4 | ||
|
|
19072bd617 | ||
|
|
a39a23f938 | ||
|
|
a10e9a04b1 | ||
|
|
c2d5a07053 | ||
|
|
e7526e39bf | ||
|
|
811b1fb5f4 | ||
|
|
72ae46c93d | ||
|
|
6b6276e6e2 | ||
|
|
60ff34a587 | ||
|
|
356960cc82 | ||
|
|
8708e7f8b1 | ||
|
|
962ee0d108 | ||
|
|
cf5957ffd1 | ||
|
|
f41b008b42 | ||
|
|
228a1e4887 | ||
|
|
913ce55d43 | ||
|
|
9ded2e489d | ||
|
|
c65e4d83a2 | ||
|
|
3184f91227 | ||
|
|
8eb17b6664 | ||
|
|
36ea1dac94 | ||
|
|
3fbe199aab | ||
|
|
8db8ade9c7 | ||
|
|
41e004f4d5 | ||
|
|
37ad445620 | ||
|
|
480f8b6890 | ||
|
|
24fd2cf521 | ||
|
|
68dbe6945b | ||
|
|
c3352d2b73 | ||
|
|
55a7169e21 | ||
|
|
50080ab875 | ||
|
|
5697cbd251 | ||
|
|
86ad055f8f | ||
|
+1 |
4883c3897b | ||
|
|
09c17fc85b | ||
|
|
fe59f495f6 | ||
|
|
9a475c2e92 | ||
|
|
8cd8a389b9 | ||
|
|
9cc45bc66a | ||
|
|
ea0b65be2e | ||
|
|
f4a8e3d1a6 | ||
|
|
7576bf6a01 | ||
|
|
3d63523f25 | ||
|
|
dbfd6c7599 | ||
|
|
ed6c7387bc | ||
|
|
6ab216dcb6 | ||
|
|
b38076f847 | ||
|
|
16816d8490 | ||
|
|
e8813fe153 | ||
|
|
ce24e4e622 | ||
|
|
10e5e7bc9c | ||
|
|
28b222e3dd | ||
|
|
86085031f6 | ||
|
|
30f47704f1 | ||
|
|
82e0cd1ca1 | ||
|
|
5374127241 | ||
|
|
df489c0cfe | ||
|
|
d83f6106f5 | ||
|
|
b7361c8da4 | ||
|
|
73531dbf60 | ||
|
|
5af6858d4c | ||
|
|
1a3b473356 | ||
|
|
67e4dddda1 | ||
|
|
55816032b7 | ||
|
|
b9fd0e50a7 | ||
|
|
71d54955ad | ||
|
|
d34dc82db6 | ||
|
|
73214ad599 | ||
|
|
1ddb73595d | ||
|
|
f9ee9c8592 | ||
|
|
0fdfc945c3 | ||
|
|
97219c951e | ||
|
|
64cc64095d | ||
|
|
91a613d966 | ||
|
|
06b7b1e574 | ||
|
|
933b7cab2f | ||
|
|
885e25d07a | ||
|
|
421f9a3b2b | ||
|
|
e52cfb036e | ||
|
|
87a3a4ece3 | ||
|
|
b152e2bfab | ||
|
|
c9c97a45b7 | ||
|
|
cba4ba2799 | ||
|
|
ab9eea3620 | ||
|
|
96f2d011ca | ||
|
|
9ee55cd17d | ||
|
|
07ee7867da | ||
|
|
940d4f722c | ||
|
|
af56096162 | ||
|
|
0e12c1a132 | ||
|
|
ca20509cb1 | ||
|
|
58c93ff349 | ||
|
|
aab3b22290 | ||
|
|
6cf933741f | ||
|
|
9d17834227 | ||
|
|
7805508d4c | ||
|
|
72f6c2faf3 | ||
|
|
7035767828 | ||
|
|
13a2777c87 | ||
|
|
a4f9438d9e | ||
|
|
4445021d66 | ||
|
|
f6a3b76a0b | ||
|
|
42bb602ff5 | ||
|
|
4547448718 | ||
|
|
10bda7e6f4 | ||
|
|
f239140055 | ||
|
|
6f4b557f63 | ||
|
|
5118ce4e62 | ||
|
|
a705b7cf95 | ||
|
|
0d5d1ae4e6 | ||
|
|
b57547415b | ||
|
|
b60d889bf7 | ||
|
|
2f463c1eda | ||
|
|
d1e006ed29 | ||
|
|
ba58905c9c | ||
|
|
de221b9dae | ||
|
|
0913c5e710 | ||
|
|
41fd38d8d1 | ||
|
|
7ec880852f | ||
|
|
9213b7fb2e | ||
|
|
80f1367d9a | ||
|
|
f521a041a9 | ||
|
|
be9efbe6ee | ||
|
|
8f3c716ca7 | ||
|
|
3cdddfa86d | ||
|
|
b1d8edc72a | ||
|
|
bbc546e2a0 | ||
|
|
f621776cdd | ||
|
|
7ba9d9eadd | ||
|
|
ac4c3a7501 | ||
|
|
f95ebe2224 | ||
|
|
7ee2c51b81 | ||
|
|
192c90816b | ||
|
|
1a772e8c65 | ||
|
|
95bed4712c | ||
|
|
2c6ceb3cb0 | ||
|
|
3118c87c65 | ||
|
|
f945a85441 | ||
|
|
72ffbb4176 | ||
|
|
77dc5dbe06 | ||
|
|
0a1d6be7b7 | ||
|
|
bb3d03fc72 | ||
|
|
a0ddaf6766 | ||
|
|
0bd2adc3c6 | ||
|
|
77c3ef6ced | ||
|
|
a66ddb9a20 | ||
|
|
eef365b37e | ||
|
|
5cab603b6e | ||
|
|
6c5d07adad | ||
|
|
1935a7021d | ||
|
|
31c46c963d | ||
|
|
4bc6ffa0f0 | ||
|
|
b62659de62 | ||
|
|
9a27c27b9a | ||
|
|
74c647cc97 | ||
|
|
e2df8e25b1 | ||
|
|
9969dc60c5 | ||
|
|
6230b436ad | ||
|
|
3873459aaa | ||
|
|
4ee3857427 | ||
|
|
2617dd3630 | ||
|
|
e0425102c8 | ||
|
|
a470475c21 | ||
|
|
bcd73e6d15 | ||
|
|
7d6f21dc3a | ||
|
|
3eddfa23b7 | ||
|
|
d5fa232704 | ||
|
|
c84afb3155 | ||
|
|
401fef5774 | ||
|
|
9298fe48e1 | ||
|
|
7f51e0395c | ||
|
|
4f410fb8ad | ||
|
|
9bcf95287a | ||
|
|
5703377626 | ||
|
|
3adb3a67e9 | ||
|
|
7918b333e6 | ||
|
|
329b0e8f3a | ||
|
|
3b17e0a897 | ||
|
|
e10f1dac20 | ||
|
|
86c171545e | ||
|
|
c3a3881cfc | ||
|
|
7f04cdd6b1 | ||
|
|
27361c65fe | ||
|
|
ffa7afcbab | ||
|
|
9aa1367823 | ||
|
|
acc96b95c8 | ||
|
|
46502875c5 | ||
|
|
43b769923b | ||
|
|
fe3fc0413c | ||
|
|
fd13088726 | ||
|
|
195db2ee6d | ||
|
|
96f68e1017 | ||
|
|
24d3bbf4fe | ||
|
|
d60a923841 |
No files matched your search
@@ -1071,6 +1071,7 @@ ij_jsp_keep_indents_on_empty_lines = false
|
||||
ij_jspx_keep_indents_on_empty_lines = false
|
||||
|
||||
[{*.markdown,*.md}]
|
||||
max_line_length = 200
|
||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||
ij_markdown_force_one_space_after_header_symbol = true
|
||||
ij_markdown_force_one_space_after_list_bullet = true
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
name: Check Translations
|
||||
name: Check Codes
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
|
||||
jobs:
|
||||
@@ -13,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations --no-daemon
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Check Style
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain --no-daemon
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest --no-daemon
|
||||
@@ -2,8 +2,8 @@ name: Check Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 * * * *'
|
||||
# schedule:
|
||||
# - cron: '30 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -13,39 +13,27 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
- 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/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=v$HMCL_VERSION" >> $GITHUB_ENV
|
||||
run: ./gradlew checkUpdateDev --no-daemon --info --stacktrace
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$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/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
@@ -78,39 +66,28 @@ jobs:
|
||||
needs: dev-check-update
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
- 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
|
||||
run: ./gradlew checkUpdateStable --no-daemon --info --stacktrace
|
||||
- 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
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
@@ -122,9 +99,9 @@ jobs:
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Create release
|
||||
@@ -139,3 +116,4 @@ jobs:
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
prerelease: true
|
||||
@@ -13,11 +13,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
||||
@@ -27,8 +27,24 @@ jobs:
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.jar
|
||||
HMCL/build/libs/HMCL-*.jar.sha256
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.exe
|
||||
HMCL/build/libs/HMCL-*.exe.sha256
|
||||
- name: Upload SH
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
+9
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
*.hprof
|
||||
|
||||
.gradle
|
||||
|
||||
@@ -13,13 +14,18 @@ hs_err_pid*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
/.java/
|
||||
/.local/
|
||||
/.cache/
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/HMCLBoot/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/build/
|
||||
/buildSrc/build/
|
||||
|
||||
# idea
|
||||
@@ -28,12 +34,14 @@ minecraft-exported-crash-info*
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
+160
-62
@@ -1,4 +1,11 @@
|
||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
@@ -12,40 +19,51 @@ plugins {
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
||||
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
|
||||
.isNullOrEmpty())
|
||||
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
|
||||
|
||||
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||
if (number != null) {
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
||||
val prefix = if (isOfficial) "dev" else "unofficial"
|
||||
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
}
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
|
||||
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
||||
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
|
||||
if (buildNumber != null) {
|
||||
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
|
||||
"$versionRoot.0.$buildNumber"
|
||||
} else {
|
||||
"$versionRoot.$buildNumber"
|
||||
}
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
||||
version = if (shortCommit.isNullOrBlank()) {
|
||||
"$versionRoot.SNAPSHOT"
|
||||
} else if (isOfficial) {
|
||||
"$versionRoot.dev-$shortCommit"
|
||||
} else {
|
||||
"$versionRoot.unofficial-$shortCommit"
|
||||
}
|
||||
}
|
||||
|
||||
val embedResources by configurations.registering
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
implementation("libs:JFoenix")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
|
||||
if (launcherExe == null) {
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
embedResources(libs.authlib.injector)
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
@@ -89,16 +107,63 @@ fun attachSignature(jar: File) {
|
||||
}
|
||||
}
|
||||
|
||||
val java11 = sourceSets.create("java11") {
|
||||
java {
|
||||
srcDir("src/main/java11")
|
||||
}
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "17"
|
||||
targetCompatibility = "17"
|
||||
}
|
||||
|
||||
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
|
||||
tasks.checkstyleMain {
|
||||
// Third-party code is not checked
|
||||
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
}
|
||||
|
||||
val addOpens = listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
)
|
||||
|
||||
val hmclProperties = buildList {
|
||||
add("hmcl.version" to project.version.toString())
|
||||
add("hmcl.add-opens" to addOpens.joinToString(" "))
|
||||
System.getenv("GITHUB_SHA")?.let {
|
||||
add("hmcl.version.hash" to it)
|
||||
}
|
||||
add("hmcl.version.type" to versionType)
|
||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||
}
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
val createPropertiesFile by tasks.registering {
|
||||
outputs.file(hmclPropertiesFile)
|
||||
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
||||
|
||||
doLast {
|
||||
val targetFile = hmclPropertiesFile.get().asFile
|
||||
targetFile.parentFile.mkdir()
|
||||
targetFile.bufferedWriter().use {
|
||||
for ((k, v) in hmclProperties) {
|
||||
it.write("$k=$v\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
@@ -109,6 +174,8 @@ tasks.jar {
|
||||
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
|
||||
tasks.shadowJar {
|
||||
dependsOn(createPropertiesFile)
|
||||
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
exclude("**/package-info.class")
|
||||
@@ -118,7 +185,7 @@ tasks.shadowJar {
|
||||
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
||||
|
||||
listOf(
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
||||
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
||||
).forEach { exclude("com/sun/jna/$it/**") }
|
||||
|
||||
@@ -126,43 +193,19 @@ tasks.shadowJar {
|
||||
exclude(dependency("com.google.code.gson:.*:.*"))
|
||||
exclude(dependency("net.java.dev.jna:jna:.*"))
|
||||
exclude(dependency("libs:JFoenix:.*"))
|
||||
exclude(project(":HMCLBoot"))
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Implementation-Version" to project.version,
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Build-Channel" to versionType,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"jdk.attach/sun.tools.attach",
|
||||
).joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
manifest.attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
||||
"Implementation-Version" to project.version.toString(),
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Add-Opens" to addOpens.joinToString(" "),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
System.getenv("GITHUB_SHA")?.also {
|
||||
attributes("GitHub-SHA" to it)
|
||||
}
|
||||
}
|
||||
|
||||
if (launcherExe != null) {
|
||||
if (launcherExe.isNotBlank()) {
|
||||
into("assets") {
|
||||
from(file(launcherExe))
|
||||
}
|
||||
@@ -175,10 +218,21 @@ tasks.shadowJar {
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
into("META-INF/versions/11") {
|
||||
from(sourceSets["java11"].output)
|
||||
dependsOn(createPropertiesFile)
|
||||
dependsOn(upsideDownTranslate)
|
||||
dependsOn(createLocaleNamesResourceBundle)
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
into("assets/") {
|
||||
from(hmclPropertiesFile)
|
||||
from(embedResources)
|
||||
}
|
||||
|
||||
into("assets/lang") {
|
||||
from(createLanguageList.map { it.outputFile })
|
||||
from(upsideDownTranslate.map { it.outputFile })
|
||||
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||
}
|
||||
dependsOn(tasks["java11Classes"])
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
@@ -263,6 +317,16 @@ fun parseToolOptions(options: String?): MutableList<String> {
|
||||
return result
|
||||
}
|
||||
|
||||
// For IntelliJ IDEA
|
||||
tasks.withType<JavaExec> {
|
||||
if (name != "run") {
|
||||
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
|
||||
// if (javaVersion >= JavaVersion.VERSION_24) {
|
||||
// jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<JavaExec>("run") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
@@ -291,6 +355,40 @@ tasks.register<JavaExec>("run") {
|
||||
}
|
||||
}
|
||||
|
||||
// Check Translations
|
||||
|
||||
tasks.register<CheckTranslations>("checkTranslations") {
|
||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||
|
||||
englishFile.set(dir.file("I18N.properties"))
|
||||
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
|
||||
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
|
||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
||||
}
|
||||
|
||||
// l10n
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
||||
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
||||
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
||||
}
|
||||
|
||||
val createLanguageList by tasks.registering(CreateLanguageList::class) {
|
||||
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
||||
resourceBundleBaseName.set("I18N")
|
||||
additionalLanguages.set(listOf("en-Qabs"))
|
||||
outputFile.set(generatedDir.map { it.file("languages.json") })
|
||||
}
|
||||
|
||||
val createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
||||
}
|
||||
|
||||
// mcmod data
|
||||
|
||||
tasks.register<ParseModDataTask>("parseModData") {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
+107
-103
@@ -17,41 +17,25 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.ModuleHelper;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Main {
|
||||
public final class EntryPoint {
|
||||
|
||||
private Main() {
|
||||
private EntryPoint() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -62,12 +46,9 @@ public final class Main {
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
checkDirectoryPath();
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION < 9)
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
initIcon();
|
||||
|
||||
@@ -85,6 +66,71 @@ public final class Main {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void setupJavaFXVMOptions() {
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||
LOG.info("HMCL_FORCE_GPU: true");
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
}
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
||||
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
if (uiScale != null) {
|
||||
uiScale = uiScale.trim();
|
||||
|
||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||
|
||||
try {
|
||||
float scaleValue;
|
||||
if (uiScale.endsWith("%")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
||||
} else {
|
||||
scaleValue = Float.parseFloat(uiScale);
|
||||
}
|
||||
|
||||
float lowerBound;
|
||||
float upperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
lowerBound = 0.25f;
|
||||
upperBound = 4f;
|
||||
} else {
|
||||
lowerBound = 0.01f;
|
||||
upperBound = 10f;
|
||||
}
|
||||
|
||||
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Invalid UI scale: " + uiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createHMCLDirectories() {
|
||||
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
|
||||
try {
|
||||
@@ -97,6 +143,8 @@ public final class Main {
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Logger has not been started yet, so print directly to System.err
|
||||
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
e.printStackTrace(System.err);
|
||||
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
|
||||
}
|
||||
@@ -112,16 +160,23 @@ public final class Main {
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
try {
|
||||
if (java.awt.Taskbar.isTaskbarSupported()) {
|
||||
var image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
|
||||
java.awt.Taskbar.getTaskbar().setIconImage(image);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set application icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDirectory = new File("").getAbsolutePath();
|
||||
if (currentDirectory.contains("!")) {
|
||||
String currentDir = System.getProperty("user.dir", "");
|
||||
if (currentDir.contains("!")) {
|
||||
LOG.error("The current working path contains an exclamation mark: " + currentDir);
|
||||
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||
+ "The path is " + currentDirectory);
|
||||
+ "The path is " + currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +184,10 @@ public final class Main {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.error("unable to patch JVM", e);
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.error("unable to patch JVM", e);
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.error("User cancels downloading JavaFX", e);
|
||||
@@ -149,7 +204,7 @@ public final class Main {
|
||||
Class.forName("javafx.stage.Stage"); // javafx.graphics
|
||||
Class.forName("javafx.scene.control.Skin"); // javafx.controls
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
LOG.warning("JavaFX is incomplete or not found", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
@@ -157,9 +212,20 @@ public final class Main {
|
||||
private static void addEnableNativeAccess() {
|
||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||
try {
|
||||
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
|
||||
// javafx.graphics
|
||||
Module module = Class.forName("javafx.stage.Stage").getModule();
|
||||
if (module.isNamed()) {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
|
||||
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
|
||||
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
|
||||
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace(System.err);
|
||||
LOG.error("Failed to add enable native access for JavaFX", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
@@ -170,11 +236,11 @@ public final class Main {
|
||||
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("sun.misc.Unsafe");
|
||||
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
|
||||
trySetMemoryAccessWarned.setAccessible(true);
|
||||
trySetMemoryAccessWarned.invoke(null);
|
||||
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
|
||||
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
|
||||
.invokeExact();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
LOG.warning("Failed to enable unsafe memory access", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,70 +248,8 @@ public final class Main {
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
static void showErrorAndExit(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("A fatal error has occurred, forcibly exiting.");
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.ERROR, message).showAndWait();
|
||||
exit(1);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
private static void showErrorAndExit(String message) {
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
|
||||
*/
|
||||
static void showWarningAndContinue(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("Potential issues have been detected.");
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.WARNING, message).showAndWait();
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
private static void fixLetsEncrypt() {
|
||||
try {
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
|
||||
try (InputStream ksStream = Files.newInputStream(ksPath)) {
|
||||
defaultKeyStore.load(ksStream, "changeit".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
|
||||
for (String alias : Collections.list(defaultKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
|
||||
|
||||
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
instance.init(merged);
|
||||
SSLContext tls = SSLContext.getInstance("TLS");
|
||||
tls.init(null, instance.getTrustManagers(), null);
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
|
||||
LOG.info("Added Lets Encrypt root certificates as additional trust");
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
|
||||
KeyManagementException e) {
|
||||
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
@@ -27,6 +28,7 @@ import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -54,6 +56,8 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -71,23 +75,21 @@ public final class Launcher extends Application {
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get prism pipeline", e);
|
||||
}
|
||||
LOG.info("Prism Pipeline: " + FXUtils.GRAPHICS_PIPELINE);
|
||||
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
|
||||
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
|
||||
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (SambaException e) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load config", e);
|
||||
checkConfigInTempDir();
|
||||
checkConfigOwner();
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
@@ -105,8 +107,12 @@ public final class Launcher extends Application {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConfigHolder.isUnsupportedVersion()) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
|
||||
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
@@ -169,7 +175,7 @@ public final class Launcher extends Application {
|
||||
private static void checkConfigInTempDir() {
|
||||
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||
Main.exit(0);
|
||||
EntryPoint.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +215,7 @@ public final class Launcher extends Application {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
Main.exit(1);
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -231,8 +237,8 @@ public final class Launcher extends Application {
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
|
||||
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
|
||||
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
|
||||
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion()
|
||||
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + ')'));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
|
||||
}
|
||||
|
||||
@@ -36,14 +36,15 @@ public final class Metadata {
|
||||
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
|
||||
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 8;
|
||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 11;
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||
@@ -55,8 +56,8 @@ public final class Metadata {
|
||||
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
||||
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
|
||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||
|
||||
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
@@ -117,7 +118,6 @@ public final class Metadata {
|
||||
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
|
||||
else
|
||||
supportedArchitectures = EnumSet.noneOf(Architecture.class);
|
||||
|
||||
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
|
||||
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
|
||||
OperatingSystem.CURRENT_OS.getCheckedName(),
|
||||
|
||||
@@ -42,7 +42,7 @@ public class CrashReport {
|
||||
"\n Content: \n " +
|
||||
stackTrace + "\n\n" +
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
|
||||
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
|
||||
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
|
||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
|
||||
|
||||
@@ -21,15 +21,17 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
@@ -58,61 +60,82 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
private void generateOptionsTxt() {
|
||||
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
|
||||
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
|
||||
|
||||
if (optionsFile.exists()) {
|
||||
if (config().isDisableAutoGameOptions())
|
||||
return;
|
||||
}
|
||||
|
||||
if (configFolder.isDirectory()) {
|
||||
if (findFiles(configFolder, "options.txt")) {
|
||||
return;
|
||||
Path runDir = repository.getRunDirectory(version.getId());
|
||||
Path optionsFile = runDir.resolve("options.txt");
|
||||
Path configFolder = runDir.resolve("config");
|
||||
|
||||
if (Files.exists(optionsFile))
|
||||
return;
|
||||
|
||||
if (Files.isDirectory(configFolder)) {
|
||||
try (Stream<Path> stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) {
|
||||
if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file))))
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to visit config folder", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!I18n.isUseChinese()) {
|
||||
return;
|
||||
}
|
||||
Locale locale = Locale.getDefault();
|
||||
|
||||
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 时自动切换为英文
|
||||
* 1.0 : No language option, do not set for these versions
|
||||
* 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash)
|
||||
* 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English
|
||||
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
||||
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
||||
*/
|
||||
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 (gameVersion.compareTo("1.1") < 0)
|
||||
return;
|
||||
|
||||
if (lang != null) {
|
||||
try {
|
||||
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
String lang = normalizedLanguageTag(locale, gameVersion);
|
||||
if (lang.isEmpty())
|
||||
return;
|
||||
|
||||
if (gameVersion.compareTo("1.11") >= 0)
|
||||
lang = lang.toLowerCase(Locale.ROOT);
|
||||
|
||||
try {
|
||||
Files.createDirectories(optionsFile.getParent());
|
||||
Files.writeString(optionsFile, String.format("lang:%s\n", lang));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to generate options.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean findFiles(File folder, String fileName) {
|
||||
File[] fs = folder.listFiles();
|
||||
if (fs != null) {
|
||||
for (File f : fs) {
|
||||
if (f.isDirectory())
|
||||
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
|
||||
return true;
|
||||
if (f.getName().equals(fileName))
|
||||
return true;
|
||||
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
||||
String region = locale.getCountry();
|
||||
|
||||
return switch (LocaleUtils.getRootLanguage(locale)) {
|
||||
case "es" -> "es_ES";
|
||||
case "ja" -> "ja_JP";
|
||||
case "ru" -> "ru_RU";
|
||||
case "uk" -> "uk_UA";
|
||||
case "zh" -> {
|
||||
if ("lzh".equals(locale.getLanguage()) && gameVersion.compareTo("1.16") >= 0)
|
||||
yield "lzh";
|
||||
|
||||
String script = LocaleUtils.getScript(locale);
|
||||
if ("Hant".equals(script)) {
|
||||
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
|
||||
yield "zh_HK";
|
||||
yield "zh_TW";
|
||||
}
|
||||
yield "zh_CN";
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case "en" -> {
|
||||
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
|
||||
yield "en_UD";
|
||||
}
|
||||
|
||||
yield "";
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,7 +145,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeLaunchScript(File scriptFile) throws IOException {
|
||||
public void makeLaunchScript(Path scriptFile) throws IOException {
|
||||
generateOptionsTxt();
|
||||
super.makeLaunchScript(scriptFile);
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
@@ -57,7 +57,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
|
||||
// local version settings
|
||||
@@ -66,7 +66,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
|
||||
|
||||
public HMCLGameRepository(Profile profile, File baseDirectory) {
|
||||
public HMCLGameRepository(Profile profile, Path baseDirectory) {
|
||||
super(baseDirectory);
|
||||
this.profile = profile;
|
||||
}
|
||||
@@ -85,18 +85,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
public Path getRunDirectory(String id) {
|
||||
switch (getGameDirectoryType(id)) {
|
||||
case VERSION_FOLDER:
|
||||
return getVersionRoot(id);
|
||||
case ROOT_FOLDER:
|
||||
return super.getRunDirectory(id);
|
||||
case CUSTOM:
|
||||
File dir = new File(getVersionSetting(id).getGameDir());
|
||||
if (!FileUtils.isValidPath(dir)) return getVersionRoot(id);
|
||||
return dir;
|
||||
try {
|
||||
return Path.of(getVersionSetting(id).getGameDir());
|
||||
} catch (InvalidPathException ignored) {
|
||||
return getVersionRoot(id);
|
||||
}
|
||||
default:
|
||||
throw new Error();
|
||||
throw new AssertionError("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,25 +121,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
});
|
||||
|
||||
try {
|
||||
File file = new File(getBaseDirectory(), "launcher_profiles.json");
|
||||
if (!file.exists() && !versions.isEmpty())
|
||||
FileUtils.writeText(file, PROFILE);
|
||||
Path file = getBaseDirectory().resolve("launcher_profiles.json");
|
||||
if (!Files.exists(file) && !versions.isEmpty()) {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, PROFILE);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
public void changeDirectory(Path newDirectory) {
|
||||
setBaseDirectory(newDirectory);
|
||||
refreshVersionsAsync().start();
|
||||
}
|
||||
|
||||
private void clean(File directory) throws IOException {
|
||||
FileUtils.deleteDirectory(new File(directory, "crash-reports"));
|
||||
FileUtils.deleteDirectory(new File(directory, "logs"));
|
||||
private void clean(Path directory) throws IOException {
|
||||
FileUtils.deleteDirectory(directory.resolve("crash-reports"));
|
||||
FileUtils.deleteDirectory(directory.resolve("logs"));
|
||||
}
|
||||
|
||||
public void clean(String id) throws IOException {
|
||||
@@ -146,8 +147,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
Path srcDir = getVersionRoot(srcId).toPath();
|
||||
Path dstDir = getVersionRoot(dstId).toPath();
|
||||
Path srcDir = getVersionRoot(srcId);
|
||||
Path dstDir = getVersionRoot(dstId);
|
||||
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
@@ -158,6 +159,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
blackList.add("saves");
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
|
||||
Files.createDirectories(dstDir);
|
||||
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
|
||||
Path fromJson = srcDir.resolve(srcId + ".json");
|
||||
@@ -170,7 +173,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
Files.copy(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
@@ -179,22 +182,22 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
|
||||
saveVersionSetting(dstId);
|
||||
|
||||
File srcGameDir = getRunDirectory(srcId);
|
||||
File dstGameDir = getRunDirectory(dstId);
|
||||
Path srcGameDir = getRunDirectory(srcId);
|
||||
Path dstGameDir = getRunDirectory(dstId);
|
||||
|
||||
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
|
||||
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
|
||||
FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
}
|
||||
|
||||
private File getLocalVersionSettingFile(String id) {
|
||||
return new File(getVersionRoot(id), "hmclversion.cfg");
|
||||
private Path getLocalVersionSettingFile(String id) {
|
||||
return getVersionRoot(id).resolve("hmclversion.cfg");
|
||||
}
|
||||
|
||||
private void loadLocalVersionSetting(String id) {
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (file.exists())
|
||||
Path file = getLocalVersionSettingFile(id);
|
||||
if (Files.exists(file))
|
||||
try {
|
||||
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
|
||||
VersionSetting versionSetting = JsonUtils.fromJsonFile(file, VersionSetting.class);
|
||||
initLocalVersionSetting(id, versionSetting);
|
||||
} catch (Exception ex) {
|
||||
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
|
||||
@@ -257,12 +260,12 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return vs;
|
||||
}
|
||||
|
||||
public Optional<File> getVersionIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
public Optional<Path> getVersionIconFile(String id) {
|
||||
Path root = getVersionRoot(id);
|
||||
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
File file = new File(root, "icon." + extension);
|
||||
if (file.exists()) {
|
||||
Path file = root.resolve("icon." + extension);
|
||||
if (Files.exists(file)) {
|
||||
return Optional.of(file);
|
||||
}
|
||||
}
|
||||
@@ -270,7 +273,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void setVersionIconFile(String id, File iconFile) throws IOException {
|
||||
public void setVersionIconFile(String id, Path iconFile) throws IOException {
|
||||
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
||||
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
|
||||
throw new IllegalArgumentException("Unsupported icon file: " + ext);
|
||||
@@ -278,13 +281,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
deleteIconFile(id);
|
||||
|
||||
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
|
||||
FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext));
|
||||
}
|
||||
|
||||
public void deleteIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
Path root = getVersionRoot(id);
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
new File(root, "icon." + extension).delete();
|
||||
Path file = root.resolve("icon." + extension);
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete icon file: " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,10 +305,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
if (iconType == VersionIconType.DEFAULT) {
|
||||
Version version = getVersion(id).resolve(this);
|
||||
Optional<File> iconFile = getVersionIconFile(id);
|
||||
Optional<Path> iconFile = getVersionIconFile(id);
|
||||
if (iconFile.isPresent()) {
|
||||
try {
|
||||
return FXUtils.loadImage(iconFile.get().toPath());
|
||||
return FXUtils.loadImage(iconFile.get());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load version icon of " + id, e);
|
||||
}
|
||||
@@ -312,6 +320,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
return VersionIconType.FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
|
||||
return VersionIconType.CLEANROOM.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
@@ -333,7 +343,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
public void saveVersionSetting(String id) {
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
return;
|
||||
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
|
||||
Path file = getLocalVersionSettingFile(id).toAbsolutePath().normalize();
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
} catch (IOException e) {
|
||||
@@ -367,7 +377,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
@@ -378,7 +388,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int) (getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
@@ -403,6 +413,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
.setNoGeneratedOptimizingJVMArgs(vs.isNoOptimizingJVMArgs())
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
@@ -424,15 +435,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
File json = getModpackConfiguration(version);
|
||||
if (json.exists()) {
|
||||
Path json = getModpackConfiguration(version);
|
||||
if (Files.exists(json)) {
|
||||
try {
|
||||
String jsonText = FileUtils.readText(json);
|
||||
String jsonText = Files.readString(json);
|
||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
|
||||
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
e.printStackTrace();
|
||||
LOG.warning("Failed to parse modpack configuration file " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,8 +454,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getModpackConfiguration(String version) {
|
||||
return new File(getVersionRoot(version), "modpack.cfg");
|
||||
public Path getModpackConfiguration(String version) {
|
||||
return getVersionRoot(version).resolve("modpack.cfg");
|
||||
}
|
||||
|
||||
public void markVersionAsModpack(String id) {
|
||||
@@ -457,16 +468,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public void markVersionLaunchedAbnormally(String id) {
|
||||
try {
|
||||
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
|
||||
Files.createFile(getVersionRoot(id).resolve(".abnormal"));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unmarkVersionLaunchedAbnormally(String id) {
|
||||
File file = new File(getVersionRoot(id), ".abnormal");
|
||||
boolean result = file.isFile();
|
||||
file.delete();
|
||||
return result;
|
||||
Path file = getVersionRoot(id).resolve(".abnormal");
|
||||
if (Files.isRegularFile(file)) {
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete abnormal mark file: " + file, e);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
@@ -488,7 +507,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return OperatingSystem.isNameValid(id);
|
||||
return FileUtils.isNameValid(id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,16 +28,16 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final File zipFile;
|
||||
private final Path zipFile;
|
||||
private final String name;
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
@@ -45,16 +45,16 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
|
||||
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
|
||||
public HMCLModpackInstallTask(Profile profile, Path zipFile, Modpack modpack, String name) {
|
||||
dependency = profile.getDependency();
|
||||
repository = profile.getRepository();
|
||||
this.zipFile = zipFile;
|
||||
this.name = name;
|
||||
this.modpack = modpack;
|
||||
|
||||
File run = repository.getRunDirectory(name);
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && !json.exists())
|
||||
Path run = repository.getRunDirectory(name);
|
||||
Path json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && Files.notExists(json))
|
||||
throw new IllegalArgumentException("Version " + name + " already exists");
|
||||
|
||||
dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync());
|
||||
@@ -65,8 +65,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
|
||||
if (Files.exists(json)) {
|
||||
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
|
||||
|
||||
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
@@ -49,15 +48,14 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
|
||||
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository repository)) {
|
||||
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
|
||||
Profile profile = repository.getProfile();
|
||||
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
|
||||
@@ -79,9 +77,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private static class HMCLModpack extends Modpack {
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name) {
|
||||
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,16 +49,18 @@ import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static javafx.application.Platform.runLater;
|
||||
import static javafx.application.Platform.setImplicitExit;
|
||||
@@ -75,7 +77,7 @@ public final class LauncherHelper {
|
||||
private final Profile profile;
|
||||
private Account account;
|
||||
private final String selectedVersion;
|
||||
private File scriptFile;
|
||||
private Path scriptFile;
|
||||
private final VersionSetting setting;
|
||||
private LauncherVisibility launcherVisibility;
|
||||
private boolean showLogs;
|
||||
@@ -118,13 +120,15 @@ public final class LauncherHelper {
|
||||
launch0();
|
||||
}
|
||||
|
||||
public void makeLaunchScript(File scriptFile) {
|
||||
public void makeLaunchScript(Path scriptFile) {
|
||||
this.scriptFile = Objects.requireNonNull(scriptFile);
|
||||
|
||||
launch();
|
||||
}
|
||||
|
||||
private void launch0() {
|
||||
// https://github.com/HMCL-dev/HMCL/pull/4121
|
||||
PROCESSES.removeIf(it -> it.get() == null);
|
||||
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
|
||||
@@ -157,16 +161,16 @@ public final class LauncherHelper {
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getMesaLoader(java);
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.getAbsolutePath().indexOf('=') >= 0) {
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
|
||||
String agent = file.getAbsolutePath() + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
@@ -187,7 +191,7 @@ public final class LauncherHelper {
|
||||
LaunchOptions launchOptions = repository.getLaunchOptions(
|
||||
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
@@ -209,7 +213,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
}).thenAcceptAsync(process -> { // process is LaunchTask's result
|
||||
if (scriptFile == null) {
|
||||
PROCESSES.add(process);
|
||||
PROCESSES.add(new WeakReference<>(process));
|
||||
if (launcherVisibility == LauncherVisibility.CLOSE)
|
||||
Launcher.stopApplication();
|
||||
else
|
||||
@@ -220,7 +224,7 @@ public final class LauncherHelper {
|
||||
} else {
|
||||
runLater(() -> {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
||||
Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile)));
|
||||
});
|
||||
}
|
||||
}).withFakeProgress(
|
||||
@@ -263,27 +267,27 @@ public final class LauncherHelper {
|
||||
if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", url);
|
||||
message += i18n("download.code.404", uri);
|
||||
else
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof DownloadException) {
|
||||
URL url = ((DownloadException) ex).getUrl();
|
||||
URI uri = ((DownloadException) ex).getUri();
|
||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||
message = i18n("install.failed.downloading.timeout", url);
|
||||
message = i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||
message = i18n("assets.index.malformed");
|
||||
@@ -294,11 +298,11 @@ public final class LauncherHelper {
|
||||
} else if (ex instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex;
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message = i18n("download.code.404", url);
|
||||
message = i18n("download.code.404", uri);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
message = i18n("download.failed", uri, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
@@ -446,7 +450,9 @@ public final class LauncherHelper {
|
||||
return result;
|
||||
} else {
|
||||
GameJavaVersion gameJavaVersion;
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
|
||||
gameJavaVersion = GameJavaVersion.JAVA_21;
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
gameJavaVersion = version.getJavaVersion();
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
@@ -536,6 +542,9 @@ public final class LauncherHelper {
|
||||
case MODDED_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
||||
break;
|
||||
case CLEANROOM_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.cleanroom"));
|
||||
break;
|
||||
case VANILLA_JAVA_8_51:
|
||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||
break;
|
||||
@@ -698,6 +707,7 @@ public final class LauncherHelper {
|
||||
*/
|
||||
private final class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final HMCLGameRepository repository;
|
||||
private final Version version;
|
||||
private final LaunchOptions launchOptions;
|
||||
@@ -841,21 +851,27 @@ public final class LauncherHelper {
|
||||
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||
logBuffer.add(new Log(log, level));
|
||||
} else {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
logs.addLast(new Log(log, level));
|
||||
if (logs.size() > Log.getLogLines())
|
||||
logs.removeFirst();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (!lwjgl) {
|
||||
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
|
||||
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!lwjgl) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -880,9 +896,12 @@ public final class LauncherHelper {
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!lwjgl)
|
||||
finishLaunch();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,10 +915,18 @@ public final class LauncherHelper {
|
||||
|
||||
}
|
||||
|
||||
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static int countMangedProcesses() {
|
||||
PROCESSES.removeIf(it -> {
|
||||
ManagedProcess process = it.get();
|
||||
return process == null || !process.isRunning();
|
||||
});
|
||||
return PROCESSES.size();
|
||||
}
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
while (!PROCESSES.isEmpty())
|
||||
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
|
||||
Optional.ofNullable(PROCESSES.poll()).map(WeakReference::get).ifPresent(ManagedProcess::stop);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, 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(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
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);
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
|
||||
@@ -44,8 +44,8 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
|
||||
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
|
||||
Path runDirectory = gameRepository.getRunDirectory(versionId);
|
||||
Path baseDirectory = gameRepository.getBaseDirectory();
|
||||
List<String> versions = new ArrayList<>();
|
||||
|
||||
String currentVersionId = versionId;
|
||||
|
||||
@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCComponents;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
@@ -42,7 +44,6 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -50,10 +51,7 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -61,7 +59,8 @@ import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
private ModpackHelper() {
|
||||
}
|
||||
|
||||
private static final Map<String, ModpackProvider> providers = mapOf(
|
||||
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
|
||||
@@ -72,12 +71,16 @@ public final class ModpackHelper {
|
||||
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
|
||||
);
|
||||
|
||||
static {
|
||||
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ModpackProvider getProviderByType(String type) {
|
||||
return providers.get(type);
|
||||
}
|
||||
|
||||
public static boolean isFileModpackByExtension(File file) {
|
||||
public static boolean isFileModpackByExtension(Path file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
return "zip".equals(ext) || "mrpack".equals(ext);
|
||||
}
|
||||
@@ -135,15 +138,12 @@ public final class ModpackHelper {
|
||||
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
|
||||
}
|
||||
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
|
||||
if (!file.exists())
|
||||
throw new FileNotFoundException(file.getPath());
|
||||
else
|
||||
try {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
|
||||
try {
|
||||
return JsonUtils.fromJsonFile(file, ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
@@ -174,21 +174,21 @@ public final class ModpackHelper {
|
||||
return Files.exists(Paths.get("externalgames").resolve(name));
|
||||
}
|
||||
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
|
||||
if (isExternalGameNameConflicts(name)) {
|
||||
throw new IllegalArgumentException("name existing");
|
||||
}
|
||||
|
||||
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
|
||||
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
|
||||
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
||||
Profile newProfile = new Profile(name, location.toFile());
|
||||
Profile newProfile = new Profile(name, location);
|
||||
newProfile.setUseRelativePath(true);
|
||||
Profiles.getProfiles().add(newProfile);
|
||||
Profiles.setSelectedProfile(newProfile);
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -210,14 +210,17 @@ public final class ModpackHelper {
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.javafx(), success, failure);
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHint(List.of("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
@@ -230,13 +233,17 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
|
||||
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
}
|
||||
|
||||
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
@@ -276,6 +283,24 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
if (c.isOverrideCommands()) {
|
||||
vs.setWrapper(Lang.nonNull(c.getWrapperCommand(), ""));
|
||||
vs.setPreLaunchCommand(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
||||
}
|
||||
|
||||
if (c.isOverrideJavaArgs()) {
|
||||
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostUpdateTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.applyCommandAndJvmSettings(manifest, vs);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -103,6 +104,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load html", e);
|
||||
@@ -158,13 +160,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
|
||||
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,8 +26,6 @@ import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
@@ -45,7 +43,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -110,7 +107,7 @@ public final class TexturesLoader {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
|
||||
new FileDownloadTask(texture.getUrl(), file).run();
|
||||
LOG.info("Texture downloaded: " + texture.getUrl());
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
@@ -265,15 +262,8 @@ public final class TexturesLoader {
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
|
||||
// In order to prevent the blurring caused by bilinear interpolation,
|
||||
// we use the self-implemented nearest neighbor interpolation to scale
|
||||
drawAvatarSlow(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
|
||||
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
@@ -285,37 +275,6 @@ public final class TexturesLoader {
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
PixelReader reader = skin.getPixelReader();
|
||||
PixelWriter writer = g.getPixelWriter();
|
||||
drawImage(writer, reader,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
drawImage(writer, reader,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Scale and draw image using nearest-neighbor interpolation
|
||||
*/
|
||||
private static void drawImage(PixelWriter writer, PixelReader reader,
|
||||
int sx, int sy, int sw, int sh,
|
||||
int dx, int dy, int dw, int dh) {
|
||||
double xScale = ((double) sw) / dw;
|
||||
double yScale = ((double) sh) / dh;
|
||||
|
||||
for (int xOffset = 0; xOffset < dw; xOffset++) {
|
||||
for (int yOffset = 0; yOffset < dh; yOffset++) {
|
||||
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
|
||||
|
||||
// Draw only non-transparent pixels
|
||||
if ((color >>> 24) != 0)
|
||||
writer.setArgb(dx + xOffset, dy + yOffset, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -116,11 +115,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
if (Files.isDirectory(javaDir)) {
|
||||
JavaManifest manifest;
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
|
||||
}
|
||||
|
||||
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
|
||||
}
|
||||
}
|
||||
@@ -191,7 +186,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
});
|
||||
|
||||
JavaManifest manifest = new JavaManifest(info, update, files);
|
||||
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
|
||||
return JavaRuntime.of(executable, info, true);
|
||||
});
|
||||
}
|
||||
@@ -203,7 +198,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
|
||||
|
||||
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
|
||||
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
|
||||
return JavaRuntime.of(executable, result.getInfo(), true);
|
||||
});
|
||||
}
|
||||
@@ -212,7 +207,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
|
||||
return Task.runAsync(() -> {
|
||||
Files.deleteIfExists(getManifestFile(platform, name));
|
||||
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
|
||||
FileUtils.deleteDirectory(getJavaDir(platform, name));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,7 +220,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
if (relativized.getNameCount() > 1) {
|
||||
String name = relativized.getName(0).toString();
|
||||
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
|
||||
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
|
||||
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,17 +20,20 @@ package org.jackhuang.hmcl.java;
|
||||
import kala.compress.archivers.ArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -98,10 +101,9 @@ public final class JavaInstallTask extends Task<JavaManifest> {
|
||||
}
|
||||
|
||||
if (tree.isExecutable(entry))
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
path.toFile().setExecutable(true);
|
||||
FileUtils.setExecutable(path);
|
||||
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(HexFormat.of().formatHex(messageDigest.digest()), size));
|
||||
}
|
||||
nameStack.remove(nameStack.size() - 1);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
|
||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
|
||||
|
||||
public static String getMojangJavaPlatform(Platform platform) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||
@@ -208,9 +209,18 @@ public final class JavaManager {
|
||||
|
||||
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
|
||||
assert java.isManaged();
|
||||
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
|
||||
Path relativized = root.relativize(java.getBinary());
|
||||
|
||||
Path platformRoot;
|
||||
try {
|
||||
platformRoot = REPOSITORY.getPlatformRoot(java.getPlatform()).toRealPath();
|
||||
} catch (Throwable ignored) {
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
if (!java.getBinary().startsWith(platformRoot))
|
||||
return Task.completed(null);
|
||||
|
||||
Path relativized = platformRoot.relativize(java.getBinary());
|
||||
if (relativized.getNameCount() > 1) {
|
||||
FXUtils.runInFX(() -> {
|
||||
try {
|
||||
@@ -254,16 +264,15 @@ public final class JavaManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
|
||||
if (suggestedJavaVersion != null) {
|
||||
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
|
||||
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
|
||||
private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
|
||||
if (java1 == null)
|
||||
return java2;
|
||||
|
||||
if (b1 != b2)
|
||||
return b1 ? 1 : -1;
|
||||
}
|
||||
|
||||
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
|
||||
if (java1.getParsedVersion() != java2.getParsedVersion())
|
||||
// Prefer the Java version that is closer to the game's recommended Java version
|
||||
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
|
||||
else
|
||||
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -279,9 +288,6 @@ public final class JavaManager {
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
|
||||
|
||||
GameJavaVersion suggestedJavaVersion =
|
||||
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
|
||||
|
||||
JavaRuntime mandatory = null;
|
||||
JavaRuntime suggested = null;
|
||||
for (JavaRuntime java : javaRuntimes) {
|
||||
@@ -309,15 +315,10 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
if (!violationMandatory) {
|
||||
if (mandatory == null) mandatory = java;
|
||||
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
|
||||
mandatory = java;
|
||||
mandatory = chooseJava(mandatory, java);
|
||||
|
||||
if (!violationSuggested) {
|
||||
if (suggested == null) suggested = java;
|
||||
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
|
||||
suggested = java;
|
||||
}
|
||||
if (!violationSuggested)
|
||||
suggested = chooseJava(suggested, java);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,8 +412,15 @@ public final class JavaManager {
|
||||
if (System.getenv("PATH") != null) {
|
||||
String[] paths = System.getenv("PATH").split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4079
|
||||
// https://github.com/Meloong-Git/PCL/issues/4261
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
|
||||
.contains("\\common files\\oracle\\java\\")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
@@ -480,12 +488,15 @@ public final class JavaManager {
|
||||
try {
|
||||
info = JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e2);
|
||||
}
|
||||
LOG.warning("Failed to read release file " + releaseFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,6 +572,10 @@ public final class JavaManager {
|
||||
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
|
||||
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
@@ -68,18 +69,6 @@ public final class Accounts {
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
@@ -337,8 +326,6 @@ public final class Accounts {
|
||||
});
|
||||
}
|
||||
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
@@ -346,7 +333,7 @@ public final class Accounts {
|
||||
try {
|
||||
server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
LOG.warning("Failed to fetch authlib-injector server metadata: " + server, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -371,24 +358,18 @@ public final class Accounts {
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
|
||||
DownloadProviders::getDownloadProvider) {
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
|
||||
if (local.isPresent()) {
|
||||
return local;
|
||||
}
|
||||
// search authlib-injector.jar in current directory, it's used as a fallback
|
||||
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (authlibinjectorLocation != null) {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
|
||||
String authlibInjectorVersion = JarUtils.getAttribute("hmcl.authlib-injector.version", null);
|
||||
if (authlibInjectorVersion == null)
|
||||
throw new AssertionError("Missing hmcl.authlib-injector.version");
|
||||
|
||||
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
|
||||
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
|
||||
@@ -21,10 +21,10 @@ import com.google.gson.JsonParseException;
|
||||
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.JsonSerializable;
|
||||
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;
|
||||
@@ -38,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@JsonSerializable
|
||||
public final class AuthlibInjectorServers implements Validation {
|
||||
|
||||
public static final String CONFIG_FILENAME = "authlib-injectors.json";
|
||||
@@ -73,8 +74,7 @@ public final class AuthlibInjectorServers implements Validation {
|
||||
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
|
||||
AuthlibInjectorServers configInstance;
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
|
||||
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Malformed authlib-injectors.json", e);
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -45,6 +45,7 @@ public final class ConfigHolder {
|
||||
private static GlobalConfig globalConfigInstance;
|
||||
private static boolean newlyCreated;
|
||||
private static boolean ownerChanged = false;
|
||||
private static boolean unsupportedVersion = false;
|
||||
|
||||
public static Config config() {
|
||||
if (configInstance == null) {
|
||||
@@ -72,6 +73,10 @@ public final class ConfigHolder {
|
||||
return ownerChanged;
|
||||
}
|
||||
|
||||
public static boolean isUnsupportedVersion() {
|
||||
return unsupportedVersion;
|
||||
}
|
||||
|
||||
public static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
@@ -82,7 +87,8 @@ public final class ConfigHolder {
|
||||
LOG.info("Config location: " + configLocation);
|
||||
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
|
||||
if (!unsupportedVersion)
|
||||
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
|
||||
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
|
||||
@@ -101,6 +107,7 @@ public final class ConfigHolder {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
@@ -157,12 +164,19 @@ public final class ConfigHolder {
|
||||
LOG.warning("Failed to get owner");
|
||||
}
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
String content = Files.readString(configLocation);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
ConfigUpgrader.upgradeConfig(deserialized, content);
|
||||
int configVersion = deserialized.getConfigVersion();
|
||||
if (configVersion < Config.CURRENT_VERSION) {
|
||||
ConfigUpgrader.upgradeConfig(deserialized, content);
|
||||
} else if (configVersion > Config.CURRENT_VERSION) {
|
||||
unsupportedVersion = true;
|
||||
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
|
||||
}
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
@@ -179,7 +193,7 @@ public final class ConfigHolder {
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
String content = Files.readString(GLOBAL_CONFIG_PATH);
|
||||
GlobalConfig deserialized = GlobalConfig.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
|
||||
@@ -31,8 +31,6 @@ final class ConfigUpgrader {
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
/**
|
||||
* This method is for the compatibility with old HMCL versions.
|
||||
*
|
||||
@@ -42,17 +40,10 @@ final class ConfigUpgrader {
|
||||
static void upgradeConfig(Config deserialized, String rawContent) {
|
||||
int configVersion = deserialized.getConfigVersion();
|
||||
|
||||
if (configVersion == CURRENT_VERSION) {
|
||||
if (configVersion >= Config.CURRENT_VERSION)
|
||||
return;
|
||||
}
|
||||
|
||||
if (configVersion > CURRENT_VERSION) {
|
||||
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
|
||||
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
|
||||
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
|
||||
|
||||
if (configVersion < 1) {
|
||||
@@ -100,6 +91,6 @@ final class ConfigUpgrader {
|
||||
}
|
||||
}
|
||||
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
deserialized.setConfigVersion(Config.CURRENT_VERSION);
|
||||
}
|
||||
}
|
||||
@@ -29,25 +29,23 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {}
|
||||
private DownloadProviders() {
|
||||
}
|
||||
|
||||
private static DownloadProvider currentDownloadProvider;
|
||||
private static final DownloadProviderWrapper provider;
|
||||
|
||||
public static final Map<String, DownloadProvider> providersById;
|
||||
public static final Map<String, DownloadProvider> rawProviders;
|
||||
@@ -59,6 +57,7 @@ public final class DownloadProviders {
|
||||
public static final String DEFAULT_PROVIDER_ID = "balanced";
|
||||
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final InvalidationListener observer;
|
||||
|
||||
static {
|
||||
@@ -68,95 +67,92 @@ public final class DownloadProviders {
|
||||
|
||||
MOJANG = new MojangDownloadProvider();
|
||||
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
|
||||
rawProviders = mapOf(
|
||||
pair("mojang", MOJANG),
|
||||
pair("bmclapi", BMCLAPI)
|
||||
rawProviders = Map.of(
|
||||
"mojang", MOJANG,
|
||||
"bmclapi", BMCLAPI
|
||||
);
|
||||
|
||||
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
|
||||
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
|
||||
fileProvider.setDownloadProviderCandidates(List.of(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(BMCLAPI, fileProvider)));
|
||||
providersById = Map.of(
|
||||
"official", new AutoDownloadProvider(MOJANG, fileProvider),
|
||||
"balanced", new AutoDownloadProvider(balanced, fileProvider),
|
||||
"mirror", new AutoDownloadProvider(BMCLAPI, fileProvider));
|
||||
|
||||
observer = FXUtils.observeWeak(() -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(
|
||||
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
|
||||
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
|
||||
|
||||
provider = new DownloadProviderWrapper(MOJANG);
|
||||
}
|
||||
|
||||
static void init() {
|
||||
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
|
||||
if (!providersById.containsKey(versionListSource)) {
|
||||
config().setVersionListSource(DEFAULT_PROVIDER_ID);
|
||||
return;
|
||||
InvalidationListener onChangeDownloadSource = observable -> {
|
||||
String versionListSource = Objects.requireNonNullElse(config().getVersionListSource(), "");
|
||||
if (config().isAutoChooseDownloadType()) {
|
||||
DownloadProvider currentDownloadProvider = providersById.get(versionListSource);
|
||||
if (currentDownloadProvider == null)
|
||||
currentDownloadProvider = Objects.requireNonNull(providersById.get(DEFAULT_PROVIDER_ID),
|
||||
"default provider is null");
|
||||
|
||||
provider.setProvider(currentDownloadProvider);
|
||||
} else {
|
||||
provider.setProvider(fileDownloadProvider);
|
||||
}
|
||||
};
|
||||
config().versionListSourceProperty().addListener(onChangeDownloadSource);
|
||||
config().autoChooseDownloadTypeProperty().addListener(onChangeDownloadSource);
|
||||
|
||||
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
});
|
||||
|
||||
if (!rawProviders.containsKey(config().getDownloadType())) {
|
||||
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
|
||||
}
|
||||
onChangeDownloadSource.invalidated(null);
|
||||
|
||||
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
|
||||
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
|
||||
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
fileDownloadProvider.setDownloadProviderCandidates(
|
||||
Stream.concat(
|
||||
Stream.of(primary),
|
||||
rawProviders.values().stream().filter(x -> x != primary)
|
||||
).collect(Collectors.toList())
|
||||
);
|
||||
DownloadProvider primary = Objects.requireNonNullElseGet(
|
||||
rawProviders.get(Objects.requireNonNullElse(downloadType, "")),
|
||||
() -> rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
|
||||
|
||||
List<DownloadProvider> providers = new ArrayList<>(rawProviders.size());
|
||||
providers.add(primary);
|
||||
for (DownloadProvider provider : rawProviders.values()) {
|
||||
if (provider != primary)
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
fileDownloadProvider.setDownloadProviderCandidates(providers);
|
||||
});
|
||||
}
|
||||
|
||||
public static String getPrimaryDownloadProviderId() {
|
||||
String downloadType = config().getDownloadType();
|
||||
if (providersById.containsKey(downloadType))
|
||||
return downloadType;
|
||||
else
|
||||
return DEFAULT_PROVIDER_ID;
|
||||
}
|
||||
|
||||
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
|
||||
return Optional.ofNullable(providersById.get(primaryId))
|
||||
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current primary preferred download provider
|
||||
*/
|
||||
public static DownloadProvider getDownloadProvider() {
|
||||
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
return i18n("install.failed.downloading.timeout", url);
|
||||
return i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
return i18n("download.code.404", uri);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
|
||||
@@ -22,5 +22,6 @@ public enum EnumBackgroundImage {
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
TRANSLUCENT, // Deprecated
|
||||
PAINT
|
||||
}
|
||||
@@ -21,23 +21,20 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -54,7 +51,21 @@ public final class FontManager {
|
||||
public static final double DEFAULT_FONT_SIZE = 12.0f;
|
||||
|
||||
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
|
||||
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
Font font;
|
||||
|
||||
// Recommended
|
||||
|
||||
font = tryLoadLocalizedFont(Metadata.HMCL_CURRENT_DIRECTORY.resolve("font"));
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
font = tryLoadLocalizedFont(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("font"));
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
// Legacy
|
||||
|
||||
font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
@@ -73,10 +84,12 @@ public final class FontManager {
|
||||
return font;
|
||||
}
|
||||
|
||||
// Default
|
||||
|
||||
String fcMatchPattern;
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
|
||||
&& Locale.getDefault() != Locale.ROOT
|
||||
&& !"en".equals(Locale.getDefault().getLanguage()))
|
||||
return findByFcMatch();
|
||||
&& !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty())
|
||||
return findByFcMatch(fcMatchPattern);
|
||||
else
|
||||
return null;
|
||||
});
|
||||
@@ -112,6 +125,7 @@ public final class FontManager {
|
||||
for (String extension : FONT_EXTENSIONS) {
|
||||
Path path = dir.resolve("font." + extension);
|
||||
if (Files.isRegularFile(path)) {
|
||||
LOG.info("Load font file: " + path);
|
||||
try {
|
||||
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (font != null) {
|
||||
@@ -127,16 +141,47 @@ public final class FontManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Font findByFcMatch() {
|
||||
private static Font tryLoadLocalizedFont(Path dir) {
|
||||
Map<String, Map<String, Path>> fontFiles = LocaleUtils.findAllLocalizedFiles(dir, "font", Set.of(FONT_EXTENSIONS));
|
||||
if (fontFiles.isEmpty())
|
||||
return null;
|
||||
|
||||
List<Locale> candidateLocales = I18n.getLocale().getCandidateLocales();
|
||||
|
||||
for (Locale locale : candidateLocales) {
|
||||
Map<String, Path> extToFiles = fontFiles.get(LocaleUtils.toLanguageKey(locale));
|
||||
if (extToFiles != null) {
|
||||
for (String ext : FONT_EXTENSIONS) {
|
||||
Path fontFile = extToFiles.get(ext);
|
||||
if (fontFile != null) {
|
||||
LOG.info("Load font file: " + fontFile);
|
||||
try {
|
||||
Font font = Font.loadFont(
|
||||
fontFile.toAbsolutePath().normalize().toUri().toURL().toExternalForm(),
|
||||
DEFAULT_FONT_SIZE);
|
||||
if (font != null)
|
||||
return font;
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
|
||||
LOG.warning("Failed to load font " + fontFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Font findByFcMatch(String pattern) {
|
||||
Path fcMatch = SystemUtils.which("fc-match");
|
||||
if (fcMatch == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
String result = SystemUtils.run(fcMatch.toString(),
|
||||
":lang=" + Locale.getDefault().toLanguageTag(),
|
||||
pattern,
|
||||
"--format", "%{family}\\n%{file}").trim();
|
||||
|
||||
String[] results = result.split("\\n");
|
||||
if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) {
|
||||
LOG.warning("Unexpected output from fc-match: " + result);
|
||||
@@ -152,46 +197,34 @@ public final class FontManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION >= 9) {
|
||||
try {
|
||||
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
|
||||
MethodType.methodType(Font[].class, String.class, double.class));
|
||||
LOG.info("Load font file: " + path);
|
||||
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (fonts == null) {
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return null;
|
||||
} else if (fonts.length == 0) {
|
||||
LOG.warning("No fonts loaded from " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (fonts == null) {
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return null;
|
||||
} else if (fonts.length == 0) {
|
||||
LOG.warning("No fonts loaded from " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(family)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
if (family.indexOf(',') >= 0) {
|
||||
for (String candidateFamily : family.split(",")) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
|
||||
return fonts[0];
|
||||
} catch (NoSuchMethodException | IllegalAccessException ignored) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(family)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (font == null)
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return font;
|
||||
if (family.indexOf(',') >= 0) {
|
||||
for (String candidateFamily : family.split(",")) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
|
||||
return fonts[0];
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get default font with fc-match", e);
|
||||
return null;
|
||||
|
||||
@@ -21,10 +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 javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
@@ -51,12 +48,16 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty();
|
||||
|
||||
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
|
||||
|
||||
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
|
||||
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
@@ -95,6 +96,18 @@ public final class GlobalConfig implements Observable {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty terracottaAgreementVersionProperty() {
|
||||
return terracottaAgreementVersion;
|
||||
}
|
||||
|
||||
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
|
||||
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
@@ -131,6 +144,18 @@ public final class GlobalConfig implements Observable {
|
||||
enableOfflineAccount.set(value);
|
||||
}
|
||||
|
||||
public StringProperty fontAntiAliasingProperty() {
|
||||
return fontAntiAliasing;
|
||||
}
|
||||
|
||||
public String getFontAntiAliasing() {
|
||||
return fontAntiAliasing.get();
|
||||
}
|
||||
|
||||
public void setFontAntiAliasing(String value) {
|
||||
this.fontAntiAliasing.set(value);
|
||||
}
|
||||
|
||||
public ObservableSet<String> getUserJava() {
|
||||
return userJava;
|
||||
}
|
||||
@@ -142,11 +167,13 @@ public final class GlobalConfig implements Observable {
|
||||
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"terracottaAgreementVersion",
|
||||
"platformPromptVersion",
|
||||
"logRetention",
|
||||
"userJava",
|
||||
"disabledJava",
|
||||
"enableOfflineAccount"
|
||||
"enableOfflineAccount",
|
||||
"fontAntiAliasing"
|
||||
));
|
||||
|
||||
@Override
|
||||
@@ -157,8 +184,10 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("terracottaAgreementVersion", context.serialize(src.getTerracottaAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
|
||||
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
|
||||
if (src.enableOfflineAccount.get())
|
||||
jsonObject.addProperty("enableOfflineAccount", true);
|
||||
|
||||
@@ -183,9 +212,11 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setTerracottaAgreementVersion(Optional.ofNullable(obj.get("terracottaAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
|
||||
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
|
||||
|
||||
JsonElement userJava = obj.get("userJava");
|
||||
if (userJava != null && userJava.isJsonArray()) {
|
||||
|
||||
@@ -35,8 +35,8 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
@@ -65,17 +65,17 @@ public final class Profile implements Observable {
|
||||
this.selectedVersion.set(selectedVersion);
|
||||
}
|
||||
|
||||
private final ObjectProperty<File> gameDir;
|
||||
private final ObjectProperty<Path> gameDir;
|
||||
|
||||
public ObjectProperty<File> gameDirProperty() {
|
||||
public ObjectProperty<Path> gameDirProperty() {
|
||||
return gameDir;
|
||||
}
|
||||
|
||||
public File getGameDir() {
|
||||
public Path getGameDir() {
|
||||
return gameDir.get();
|
||||
}
|
||||
|
||||
public void setGameDir(File gameDir) {
|
||||
public void setGameDir(Path gameDir) {
|
||||
this.gameDir.set(gameDir);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public final class Profile implements Observable {
|
||||
this.name.set(name);
|
||||
}
|
||||
|
||||
private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
|
||||
private final BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
|
||||
|
||||
public BooleanProperty useRelativePathProperty() {
|
||||
return useRelativePath;
|
||||
@@ -118,18 +118,18 @@ public final class Profile implements Observable {
|
||||
}
|
||||
|
||||
public Profile(String name) {
|
||||
this(name, new File(".minecraft"));
|
||||
this(name, Path.of(".minecraft"));
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir) {
|
||||
public Profile(String name, Path initialGameDir) {
|
||||
this(name, initialGameDir, new VersionSetting());
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir, VersionSetting global) {
|
||||
public Profile(String name, Path initialGameDir, VersionSetting global) {
|
||||
this(name, initialGameDir, global, null, false);
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
|
||||
public Profile(String name, Path initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) {
|
||||
this.name = new SimpleStringProperty(this, "name", name);
|
||||
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
|
||||
repository = new HMCLGameRepository(this, initialGameDir);
|
||||
@@ -234,7 +234,7 @@ public final class Profile implements Observable {
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("global", context.serialize(src.getGlobal()));
|
||||
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
|
||||
jsonObject.addProperty("gameDir", src.getGameDir().toString());
|
||||
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
|
||||
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
|
||||
|
||||
@@ -243,12 +243,11 @@ public final class Profile implements Observable {
|
||||
|
||||
@Override
|
||||
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
if (!(json instanceof JsonObject obj)) return null;
|
||||
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
|
||||
|
||||
return new Profile("Default",
|
||||
new File(gameDir),
|
||||
Path.of(gameDir),
|
||||
context.deserialize(obj.get("global"), VersionSetting.class),
|
||||
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
|
||||
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -48,20 +48,17 @@ public final class Profiles {
|
||||
}
|
||||
|
||||
public static String getProfileDisplayName(Profile profile) {
|
||||
switch (profile.getName()) {
|
||||
case Profiles.DEFAULT_PROFILE:
|
||||
return i18n("profile.default");
|
||||
case Profiles.HOME_PROFILE:
|
||||
return i18n("profile.home");
|
||||
default:
|
||||
return profile.getName();
|
||||
}
|
||||
return switch (profile.getName()) {
|
||||
case Profiles.DEFAULT_PROFILE -> i18n("profile.default");
|
||||
case Profiles.HOME_PROFILE -> i18n("profile.home");
|
||||
default -> profile.getName();
|
||||
};
|
||||
}
|
||||
|
||||
private static final ObservableList<Profile> profiles = observableArrayList(profile -> new Observable[] { profile });
|
||||
private static final ReadOnlyListWrapper<Profile> profilesWrapper = new ReadOnlyListWrapper<>(profiles);
|
||||
|
||||
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
|
||||
private static final ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
|
||||
{
|
||||
profiles.addListener(onInvalidating(this::invalidated));
|
||||
}
|
||||
@@ -104,8 +101,8 @@ public final class Profiles {
|
||||
|
||||
private static void checkProfiles() {
|
||||
if (profiles.isEmpty()) {
|
||||
Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile());
|
||||
Profile current = new Profile(Profiles.DEFAULT_PROFILE, Path.of(".minecraft"), new VersionSetting(), null, true);
|
||||
Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY);
|
||||
Platform.runLater(() -> profiles.addAll(current, home));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,21 +18,33 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ProxyManager {
|
||||
|
||||
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
|
||||
private static final SimpleProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT;
|
||||
|
||||
static {
|
||||
ProxySelector systemProxySelector = ProxySelector.getDefault();
|
||||
SYSTEM_DEFAULT = systemProxySelector != null
|
||||
? new ProxySelectorWrapper(systemProxySelector)
|
||||
: NO_PROXY;
|
||||
}
|
||||
|
||||
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
|
||||
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
|
||||
|
||||
private static ProxySelector getProxySelector() {
|
||||
if (config().hasProxy()) {
|
||||
@@ -46,20 +58,23 @@ public final class ProxyManager {
|
||||
LOG.warning("Illegal proxy port: " + port);
|
||||
return NO_PROXY;
|
||||
} else {
|
||||
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
|
||||
return new ProxySelectorWrapper(new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port))));
|
||||
}
|
||||
} else {
|
||||
return ProxyManager.SYSTEM_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static Authenticator getAuthenticator() {
|
||||
private static SimpleAuthenticator getAuthenticator() {
|
||||
if (config().hasProxy() && config().hasProxyAuth()) {
|
||||
String username = config().getProxyUser();
|
||||
String password = config().getProxyPass();
|
||||
|
||||
if (username != null || password != null)
|
||||
return new SimpleAuthenticator(username, password.toCharArray());
|
||||
return new SimpleAuthenticator(
|
||||
Objects.requireNonNullElse(username, ""),
|
||||
Objects.requireNonNullElse(password, "").toCharArray()
|
||||
);
|
||||
else
|
||||
return null;
|
||||
} else
|
||||
@@ -67,30 +82,75 @@ public final class ProxyManager {
|
||||
}
|
||||
|
||||
static void init() {
|
||||
ProxySelector.setDefault(getProxySelector());
|
||||
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
|
||||
ProxySelector.setDefault(new ProxySelector() {
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
return defaultProxySelector.select(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
defaultProxySelector.connectFailed(uri, sa, ioe);
|
||||
}
|
||||
});
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
|
||||
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
|
||||
}
|
||||
});
|
||||
|
||||
defaultProxySelector = getProxySelector();
|
||||
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
|
||||
config().proxyTypeProperty().addListener(updateProxySelector);
|
||||
config().proxyHostProperty().addListener(updateProxySelector);
|
||||
config().proxyPortProperty().addListener(updateProxySelector);
|
||||
config().hasProxyProperty().addListener(updateProxySelector);
|
||||
|
||||
Authenticator.setDefault(getAuthenticator());
|
||||
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
|
||||
defaultAuthenticator = getAuthenticator();
|
||||
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
|
||||
config().hasProxyProperty().addListener(updateAuthenticator);
|
||||
config().hasProxyAuthProperty().addListener(updateAuthenticator);
|
||||
config().proxyUserProperty().addListener(updateAuthenticator);
|
||||
config().proxyPassProperty().addListener(updateAuthenticator);
|
||||
}
|
||||
|
||||
private static final class SimpleProxySelector extends ProxySelector {
|
||||
private static abstract class AbstractProxySelector extends ProxySelector {
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SimpleProxySelector extends AbstractProxySelector {
|
||||
private final List<Proxy> proxies;
|
||||
|
||||
SimpleProxySelector(Proxy proxy) {
|
||||
this(Collections.singletonList(proxy));
|
||||
this.proxies = List.of(proxy);
|
||||
}
|
||||
|
||||
SimpleProxySelector(List<Proxy> proxies) {
|
||||
this.proxies = proxies;
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
return proxies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps another ProxySelector to avoid using proxy for loopback addresses.
|
||||
private static final class ProxySelectorWrapper extends AbstractProxySelector {
|
||||
private final ProxySelector source;
|
||||
|
||||
ProxySelectorWrapper(ProxySelector source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,19 +158,10 @@ public final class ProxyManager {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
|
||||
return proxies;
|
||||
}
|
||||
if (NetworkUtils.isLoopbackAddress(uri))
|
||||
return NO_PROXY.proxies;
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
return source.select(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +175,7 @@ public final class ProxyManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
public PasswordAuthentication getPasswordAuthentication() {
|
||||
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,20 +22,13 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
@@ -58,23 +51,8 @@ public final class StyleSheets {
|
||||
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
|
||||
}
|
||||
|
||||
private static String toStyleSheetUri(String styleSheet, String fallback) {
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
|
||||
return fallback;
|
||||
}
|
||||
private static String toStyleSheetUri(String styleSheet) {
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
@@ -124,7 +102,7 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString(), fontFamily);
|
||||
return toStyleSheetUri(builder.toString());
|
||||
}
|
||||
|
||||
private static String rgba(Color color, double opacity) {
|
||||
@@ -151,7 +129,7 @@ public final class StyleSheets {
|
||||
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
|
||||
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
|
||||
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
|
||||
'}', blueCss);
|
||||
'}');
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
@@ -33,10 +34,24 @@ public enum VersionIconType {
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
QUILT("/assets/img/quilt.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
||||
CLEANROOM("/assets/img/cleanroom.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGED -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
case CLEANROOM -> VersionIconType.CLEANROOM;
|
||||
default -> VersionIconType.COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
private final String resourceUrl;
|
||||
|
||||
VersionIconType(String resourceUrl) {
|
||||
|
||||
@@ -380,6 +380,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
noJVMArgsProperty.set(noJVMArgs);
|
||||
}
|
||||
|
||||
private final BooleanProperty noOptimizingJVMArgsProperty = new SimpleBooleanProperty(this, "noOptimizingJVMArgs", false);
|
||||
|
||||
public BooleanProperty noOptimizingJVMArgsProperty() {
|
||||
return noOptimizingJVMArgsProperty;
|
||||
}
|
||||
|
||||
public boolean isNoOptimizingJVMArgs() {
|
||||
return noOptimizingJVMArgsProperty.get();
|
||||
}
|
||||
|
||||
public void setNoOptimizingJVMArgs(boolean noOptimizingJVMArgs) {
|
||||
noOptimizingJVMArgsProperty.set(noOptimizingJVMArgs);
|
||||
}
|
||||
|
||||
private final BooleanProperty notCheckJVMProperty = new SimpleBooleanProperty(this, "notCheckJVM", false);
|
||||
|
||||
public BooleanProperty notCheckJVMProperty() {
|
||||
@@ -757,6 +771,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
obj.addProperty("wrapper", src.getWrapper());
|
||||
obj.addProperty("fullscreen", src.isFullscreen());
|
||||
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
|
||||
obj.addProperty("noOptimizingJVMArgs", src.isNoOptimizingJVMArgs());
|
||||
obj.addProperty("notCheckGame", src.isNotCheckGame());
|
||||
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
|
||||
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
|
||||
@@ -797,14 +812,6 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
|
||||
if (index == null)
|
||||
return defaultValue;
|
||||
|
||||
int idx = index.getAsInt();
|
||||
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject))
|
||||
@@ -835,18 +842,19 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoOptimizingJVMArgs(Optional.ofNullable(obj.get("noOptimizingJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
|
||||
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
|
||||
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setGameDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("gameDirType"), GameDirectoryType.class, GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
|
||||
vs.setNativesDirType(parseJsonPrimitive(obj.getAsJsonPrimitive("nativesDirType"), NativesDirectoryType.class, NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(parseJsonPrimitive(obj.getAsJsonPrimitive("versionIcon"), VersionIconType.class, VersionIconType.DEFAULT));
|
||||
|
||||
if (obj.get("javaVersionType") != null) {
|
||||
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class TerracottaManager {
|
||||
private TerracottaManager() {
|
||||
}
|
||||
|
||||
private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>(TerracottaState.Bootstrap.INSTANCE);
|
||||
private static final ReadOnlyObjectWrapper<TerracottaState> STATE = new ReadOnlyObjectWrapper<>(STATE_V.getPlain());
|
||||
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);
|
||||
|
||||
static {
|
||||
Task.runAsync(() -> {
|
||||
if (TerracottaMetadata.PROVIDER == null) {
|
||||
setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.OS));
|
||||
LOG.warning("Terracotta hasn't support your OS: " + org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM);
|
||||
} else {
|
||||
switch (TerracottaMetadata.PROVIDER.status()) {
|
||||
case NOT_EXIST -> setState(new TerracottaState.Uninitialized(false));
|
||||
case LEGACY_VERSION -> setState(new TerracottaState.Uninitialized(true));
|
||||
case READY -> launch(setState(new TerracottaState.Launching()));
|
||||
}
|
||||
}
|
||||
}).whenComplete(exception -> {
|
||||
if (exception != null) {
|
||||
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static ReadOnlyObjectProperty<TerracottaState> stateProperty() {
|
||||
return STATE.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
static {
|
||||
Lang.thread(() -> {
|
||||
while (true) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.PortSpecific portSpecific)) {
|
||||
LockSupport.parkNanos(500_000);
|
||||
continue;
|
||||
}
|
||||
|
||||
int port = portSpecific.port;
|
||||
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
|
||||
|
||||
TerracottaState next;
|
||||
try {
|
||||
next = new GetTask(URI.create(String.format("http://127.0.0.1:%d/state", port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(jsonString -> {
|
||||
TerracottaState.Ready object = JsonUtils.fromNonNullJson(jsonString, TypeToken.get(TerracottaState.Ready.class));
|
||||
if (object.index <= index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
object.port = port;
|
||||
return object;
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.run();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot fetch state from Terracotta.", e);
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
|
||||
if (next != null) {
|
||||
compareAndSet(state, next);
|
||||
}
|
||||
|
||||
LockSupport.parkNanos(500_000);
|
||||
}
|
||||
}, "Terracotta Background Daemon", true);
|
||||
}
|
||||
|
||||
public static boolean validate(Path file) {
|
||||
return FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
public static TerracottaState.Preparing install(@Nullable Path file) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Uninitialized ||
|
||||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
|
||||
state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (file != null && !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState.Preparing preparing;
|
||||
if (state instanceof TerracottaState.Preparing it) {
|
||||
preparing = it;
|
||||
} else {
|
||||
preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1));
|
||||
}
|
||||
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
return file != null ? TarFileTree.open(file) : null;
|
||||
}).thenComposeAsync(Schedulers.javafx(), tree -> {
|
||||
return getProvider().install(preparing, tree).whenComplete(exception -> {
|
||||
if (tree != null) {
|
||||
tree.close();
|
||||
}
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}).whenComplete(exception -> {
|
||||
if (exception == null) {
|
||||
try {
|
||||
TerracottaMetadata.removeLegacyVersionFiles();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to remove legacy terracotta files.", e);
|
||||
}
|
||||
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching);
|
||||
}
|
||||
} else if (exception instanceof ITerracottaProvider.ArchiveFileMissingException) {
|
||||
LOG.warning("Cannot install terracotta from local package.", exception);
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
} else if (exception instanceof DownloadException) {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
|
||||
} else {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}
|
||||
}).start();
|
||||
|
||||
return setState(preparing);
|
||||
}
|
||||
|
||||
private static ITerracottaProvider getProvider() {
|
||||
ITerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
if (provider == null) {
|
||||
throw new AssertionError("Terracotta Provider must NOT be null.");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static TerracottaState recover(@Nullable Path file) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (getProvider().status()) {
|
||||
case NOT_EXIST, LEGACY_VERSION -> install(file);
|
||||
case READY -> {
|
||||
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
|
||||
launch(launching);
|
||||
yield launching;
|
||||
}
|
||||
};
|
||||
} catch (NullPointerException | IOException e) {
|
||||
LOG.warning("Cannot determine Terracotta state.", e);
|
||||
return setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
}
|
||||
|
||||
private static void launch(TerracottaState.Launching state) {
|
||||
Task.supplyAsync(() -> {
|
||||
Path path = Files.createTempDirectory(String.format("hmcl-terracotta-%d", ThreadLocalRandom.current().nextLong())).resolve("http").toAbsolutePath();
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(getProvider().ofCommandLine(path)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
long exitTime = -1;
|
||||
while (true) {
|
||||
if (Files.exists(path)) {
|
||||
JsonObject object = JsonUtils.fromNonNullJson(Files.readString(path), JsonObject.class);
|
||||
return object.get("port").getAsInt();
|
||||
}
|
||||
|
||||
if (!process.isRunning()) {
|
||||
if (exitTime == -1) {
|
||||
exitTime = System.currentTimeMillis();
|
||||
} else if (System.currentTimeMillis() - exitTime >= 10000) {
|
||||
throw new IllegalStateException("Process has exited for 10s.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (port, exception) -> {
|
||||
TerracottaState next;
|
||||
if (exception == null) {
|
||||
next = new TerracottaState.Unknown(port);
|
||||
} else {
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
compareAndSet(state, next);
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static Task<String> exportLogs() {
|
||||
if (STATE_V.get() instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(URI.create(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
public static TerracottaState.Waiting setWaiting() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(URI.create(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.start();
|
||||
return new TerracottaState.Waiting(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getPlayerName() {
|
||||
Account account = Accounts.getSelectedAccount();
|
||||
return account != null ? account.getCharacter() : i18n("terracotta.player_anonymous");
|
||||
}
|
||||
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(NetworkUtils.toURI(String.format(
|
||||
"http://127.0.0.1:%d/state/scanning?player=%s", portSpecific.port, getPlayerName()))
|
||||
).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Task<TerracottaState.GuestStarting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(NetworkUtils.toURI(String.format(
|
||||
"http://127.0.0.1:%d/state/guesting?room=%s&player=%s", portSpecific.port, room, getPlayerName()
|
||||
)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestStarting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends TerracottaState> T setState(T value) {
|
||||
if (value == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
STATE_V.set(value);
|
||||
STATE_D.accept(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean compareAndSet(TerracottaState previous, TerracottaState next) {
|
||||
if (next == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (STATE_V.compareAndSet(previous, next)) {
|
||||
STATE_D.accept(next);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.DirectoryStream;
|
||||
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.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class TerracottaMetadata {
|
||||
private TerracottaMetadata() {
|
||||
}
|
||||
|
||||
public record Link(@SerializedName("desc") LocalizedText description, String link) {
|
||||
}
|
||||
|
||||
private record Config(
|
||||
@SerializedName("version_legacy") String legacy,
|
||||
@SerializedName("version_recent") List<String> recent,
|
||||
@SerializedName("version_latest") String latest,
|
||||
|
||||
@SerializedName("classifiers") Map<String, String> classifiers,
|
||||
@SerializedName("downloads") List<String> downloads,
|
||||
@SerializedName("downloads_CN") List<String> downloadsCN,
|
||||
@SerializedName("links") List<Link> links
|
||||
) {
|
||||
private TerracottaNative of(String classifier) {
|
||||
List<URI> links = new ArrayList<>(this.downloads.size() + this.downloadsCN.size());
|
||||
for (String download : LocaleUtils.IS_CHINA_MAINLAND
|
||||
? Lang.merge(this.downloadsCN, this.downloads)
|
||||
: Lang.merge(this.downloads, this.downloadsCN)) {
|
||||
links.add(URI.create(download.replace("${version}", this.latest).replace("${classifier}", classifier)));
|
||||
}
|
||||
|
||||
String hash = Objects.requireNonNull(this.classifiers.get(classifier), String.format("Classifier %s doesn't exist.", classifier));
|
||||
if (!hash.startsWith("sha256:")) {
|
||||
throw new IllegalArgumentException(String.format("Invalid hash value %s for classifier %s.", hash, classifier));
|
||||
}
|
||||
hash = hash.substring("sha256:".length());
|
||||
|
||||
return new TerracottaNative(
|
||||
Collections.unmodifiableList(links),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve(
|
||||
String.format("terracotta/%s/terracotta-%s-%s", this.latest, this.latest, classifier)
|
||||
).toAbsolutePath(),
|
||||
new FileDownloadTask.IntegrityCheck("SHA-256", hash)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static final ITerracottaProvider PROVIDER;
|
||||
public static final String PACKAGE_NAME;
|
||||
public static final List<Link> PACKAGE_LINKS;
|
||||
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
|
||||
"v", "v1",
|
||||
"launcher_version", Metadata.VERSION
|
||||
));
|
||||
|
||||
private static final Pattern LEGACY;
|
||||
private static final List<String> RECENT;
|
||||
private static final String LATEST;
|
||||
|
||||
static {
|
||||
Config config;
|
||||
try (InputStream is = TerracottaMetadata.class.getResourceAsStream("/assets/terracotta.json")) {
|
||||
config = JsonUtils.fromNonNullJsonFully(is, Config.class);
|
||||
} catch (IOException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
|
||||
LEGACY = Pattern.compile(config.legacy);
|
||||
RECENT = config.recent;
|
||||
LATEST = config.latest;
|
||||
|
||||
ProviderContext context = locateProvider(config);
|
||||
PROVIDER = context != null ? context.provider() : null;
|
||||
PACKAGE_NAME = context != null ? String.format("terracotta-%s-%s-pkg.tar.gz", config.latest, context.branch) : null;
|
||||
|
||||
if (context != null) {
|
||||
List<Link> packageLinks = new ArrayList<>(config.links.size());
|
||||
for (Link link : config.links) {
|
||||
packageLinks.add(new Link(
|
||||
link.description,
|
||||
link.link.replace("${version}", LATEST)
|
||||
.replace("${classifier}", context.branch)
|
||||
));
|
||||
}
|
||||
|
||||
Collections.shuffle(packageLinks);
|
||||
PACKAGE_LINKS = Collections.unmodifiableList(packageLinks);
|
||||
} else {
|
||||
PACKAGE_LINKS = null;
|
||||
}
|
||||
}
|
||||
|
||||
private record ProviderContext(ITerracottaProvider provider, String branch) {
|
||||
ProviderContext(ITerracottaProvider provider, String system, String arch) {
|
||||
this(provider, system + "-" + arch);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProviderContext locateProvider(Config config) {
|
||||
String architecture = switch (Architecture.SYSTEM_ARCH) {
|
||||
case X86_64 -> "x86_64";
|
||||
case ARM64 -> "arm64";
|
||||
default -> null;
|
||||
};
|
||||
if (architecture == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_8_1)) {
|
||||
yield new ProviderContext(
|
||||
new GeneralProvider(config.of(String.format("windows-%s.exe", architecture))),
|
||||
"windows", architecture
|
||||
);
|
||||
}
|
||||
yield null;
|
||||
}
|
||||
case LINUX -> new ProviderContext(
|
||||
new GeneralProvider(config.of(String.format("linux-%s", architecture))),
|
||||
"linux", architecture
|
||||
);
|
||||
case MACOS -> new ProviderContext(
|
||||
new MacOSProvider(
|
||||
config.of(String.format("macos-%s.pkg", architecture)),
|
||||
config.of(String.format("macos-%s", architecture))
|
||||
),
|
||||
"macos", architecture
|
||||
);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static void removeLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
|
||||
for (Path path : terracotta) {
|
||||
String name = FileUtils.getName(path);
|
||||
if (LATEST.equals(name) || RECENT.contains(name) || !LEGACY.matcher(name).matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.deleteDirectory(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Unable to remove legacy terracotta files: %s", path), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = Files.newDirectoryStream(Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta").toAbsolutePath())) {
|
||||
for (Path path : terracotta) {
|
||||
String name = FileUtils.getName(path);
|
||||
if (!LATEST.equals(name) && (RECENT.contains(name) || LEGACY.matcher(name).matches())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public final class TerracottaNative {
|
||||
private final List<URI> links;
|
||||
private final FileDownloadTask.IntegrityCheck checking;
|
||||
private final Path path;
|
||||
|
||||
public TerracottaNative(List<URI> links, Path path, FileDownloadTask.IntegrityCheck checking) {
|
||||
this.links = links;
|
||||
this.path = path;
|
||||
this.checking = checking;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Task<?> install(ITerracottaProvider.Context context, @Nullable TarFileTree tree) {
|
||||
if (tree == null) {
|
||||
return new FileDownloadTask(links, path, checking) {
|
||||
@Override
|
||||
protected Context getContext(HttpResponse<?> response, boolean checkETag, String bmclapiHash) throws IOException {
|
||||
Context delegate = super.getContext(response, checkETag, bmclapiHash);
|
||||
return new Context() {
|
||||
@Override
|
||||
public void withResult(boolean success) {
|
||||
delegate.withResult(success);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
||||
if (!context.hasInstallFence()) {
|
||||
throw new CancellationException("User has installed terracotta from local archives.");
|
||||
}
|
||||
delegate.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (isSuccess() && !context.requestInstallFence()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
delegate.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return Task.runAsync(() -> {
|
||||
String name = FileUtils.getName(path);
|
||||
TarArchiveEntry entry = tree.getRoot().getFiles().get(name);
|
||||
if (entry == null) {
|
||||
throw new ITerracottaProvider.ArchiveFileMissingException("Cannot exact entry: " + name);
|
||||
}
|
||||
|
||||
if (!context.requestInstallFence()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
Files.createDirectories(path.toAbsolutePath().getParent());
|
||||
|
||||
MessageDigest digest = DigestUtils.getDigest(checking.getAlgorithm());
|
||||
try (
|
||||
InputStream stream = tree.getInputStream(entry);
|
||||
OutputStream os = Files.newOutputStream(path)
|
||||
) {
|
||||
stream.transferTo(new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
os.write(b);
|
||||
digest.update((byte) b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte @NotNull [] buffer, int offset, int len) throws IOException {
|
||||
os.write(buffer, offset, len);
|
||||
digest.update(buffer, offset, len);
|
||||
}
|
||||
});
|
||||
}
|
||||
String checksum = HexFormat.of().formatHex(digest.digest());
|
||||
if (!checksum.equalsIgnoreCase(checking.getChecksum())) {
|
||||
Files.delete(path);
|
||||
throw new ITerracottaProvider.ArchiveFileMissingException("Incorrect checksum (" + checking.getAlgorithm() + "), expected: " + checking.getChecksum() + ", actual: " + checksum);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ITerracottaProvider.Status status() throws IOException {
|
||||
if (Files.exists(path)) {
|
||||
if (DigestUtils.digestToString(checking.getAlgorithm(), path).equalsIgnoreCase(checking.getChecksum())) {
|
||||
return ITerracottaProvider.Status.READY;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (TerracottaMetadata.hasLegacyVersionFiles()) {
|
||||
return ITerracottaProvider.Status.LEGACY_VERSION;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.LOG.warning("Cannot determine whether legacy versions exist.");
|
||||
}
|
||||
return ITerracottaProvider.Status.NOT_EXIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
|
||||
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
||||
import org.jackhuang.hmcl.util.gson.JsonType;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract sealed class TerracottaState {
|
||||
protected TerracottaState() {
|
||||
}
|
||||
|
||||
public boolean isUIFakeState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final class Bootstrap extends TerracottaState {
|
||||
static final Bootstrap INSTANCE = new Bootstrap();
|
||||
|
||||
private Bootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Uninitialized extends TerracottaState {
|
||||
private final boolean hasLegacy;
|
||||
|
||||
Uninitialized(boolean hasLegacy) {
|
||||
this.hasLegacy = hasLegacy;
|
||||
}
|
||||
|
||||
public boolean hasLegacy() {
|
||||
return hasLegacy;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Preparing extends TerracottaState implements ITerracottaProvider.Context {
|
||||
private final ReadOnlyDoubleWrapper progress;
|
||||
|
||||
private final AtomicBoolean installFence = new AtomicBoolean(false);
|
||||
|
||||
Preparing(ReadOnlyDoubleWrapper progress) {
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindProgress(ObservableValue<? extends Number> value) {
|
||||
progress.bind(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestInstallFence() {
|
||||
return installFence.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasInstallFence() {
|
||||
return !installFence.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Launching extends TerracottaState {
|
||||
Launching() {
|
||||
}
|
||||
}
|
||||
|
||||
static abstract sealed class PortSpecific extends TerracottaState {
|
||||
transient int port;
|
||||
|
||||
protected PortSpecific(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonType(
|
||||
property = "state",
|
||||
subtypes = {
|
||||
@JsonSubtype(clazz = Waiting.class, name = "waiting"),
|
||||
@JsonSubtype(clazz = HostScanning.class, name = "host-scanning"),
|
||||
@JsonSubtype(clazz = HostStarting.class, name = "host-starting"),
|
||||
@JsonSubtype(clazz = HostOK.class, name = "host-ok"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-connecting"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-starting"),
|
||||
@JsonSubtype(clazz = GuestOK.class, name = "guest-ok"),
|
||||
@JsonSubtype(clazz = Exception.class, name = "exception"),
|
||||
}
|
||||
)
|
||||
static abstract sealed class Ready extends PortSpecific {
|
||||
@SerializedName("index")
|
||||
final int index;
|
||||
|
||||
@SerializedName("state")
|
||||
private final String state;
|
||||
|
||||
Ready(int port, int index, String state) {
|
||||
super(port);
|
||||
this.index = index;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUIFakeState() {
|
||||
return this.index == -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Unknown extends PortSpecific {
|
||||
Unknown(int port) {
|
||||
super(port);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Waiting extends Ready {
|
||||
Waiting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostScanning extends Ready {
|
||||
HostScanning(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostStarting extends Ready {
|
||||
HostStarting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostOK extends Ready implements Validation {
|
||||
@SerializedName("room")
|
||||
private final String code;
|
||||
|
||||
@SerializedName("profile_index")
|
||||
private final int profileIndex;
|
||||
|
||||
@SerializedName("profiles")
|
||||
private final List<TerracottaProfile> profiles;
|
||||
|
||||
HostOK(int port, int index, String state, String code, int profileIndex, List<TerracottaProfile> profiles) {
|
||||
super(port, index, state);
|
||||
this.code = code;
|
||||
this.profileIndex = profileIndex;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (code == null) {
|
||||
throw new JsonParseException("code is null");
|
||||
}
|
||||
if (profiles == null) {
|
||||
throw new JsonParseException("profiles is null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public List<TerracottaProfile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return state instanceof HostOK hostOK && this.index - hostOK.index <= profileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
GuestStarting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestOK extends Ready implements Validation {
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
@SerializedName("profile_index")
|
||||
private final int profileIndex;
|
||||
|
||||
@SerializedName("profiles")
|
||||
private final List<TerracottaProfile> profiles;
|
||||
|
||||
GuestOK(int port, int index, String state, String url, int profileIndex, List<TerracottaProfile> profiles) {
|
||||
super(port, index, state);
|
||||
this.url = url;
|
||||
this.profileIndex = profileIndex;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (profiles == null) {
|
||||
throw new JsonParseException("profiles is null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public List<TerracottaProfile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return state instanceof GuestOK guestOK && this.index - guestOK.index <= profileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Exception extends Ready implements Validation {
|
||||
public enum Type {
|
||||
PING_HOST_FAIL,
|
||||
PING_HOST_RST,
|
||||
GUEST_ET_CRASH,
|
||||
HOST_ET_CRASH,
|
||||
PING_SERVER_RST,
|
||||
SCAFFOLDING_INVALID_RESPONSE
|
||||
}
|
||||
|
||||
private static final TerracottaState.Exception.Type[] LOOKUP = Type.values();
|
||||
|
||||
@SerializedName("type")
|
||||
private final int type;
|
||||
|
||||
Exception(int port, int index, String state, int type) {
|
||||
super(port, index, state);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (type < 0 || type >= LOOKUP.length) {
|
||||
throw new JsonParseException(String.format("Type must between [0, %s)", LOOKUP.length));
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return LOOKUP[type];
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Fatal extends TerracottaState {
|
||||
public enum Type {
|
||||
OS,
|
||||
NETWORK,
|
||||
INSTALL,
|
||||
TERRACOTTA,
|
||||
UNKNOWN;
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
|
||||
public Fatal(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isRecoverable() {
|
||||
return this.type != Type.OS && this.type != Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.profile;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum ProfileKind {
|
||||
@SerializedName("HOST")
|
||||
HOST,
|
||||
@SerializedName("LOCAL")
|
||||
LOCAL,
|
||||
@SerializedName("GUEST")
|
||||
GUEST
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.profile;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public final class TerracottaProfile {
|
||||
@SerializedName("machine_id")
|
||||
private final String machineID;
|
||||
|
||||
@SerializedName("name")
|
||||
private final String name;
|
||||
|
||||
@SerializedName("vendor")
|
||||
private final String vendor;
|
||||
|
||||
@SerializedName("kind")
|
||||
private final ProfileKind type;
|
||||
|
||||
private TerracottaProfile(String machineID, String name, String vendor, ProfileKind type) {
|
||||
this.machineID = machineID;
|
||||
this.name = name;
|
||||
this.vendor = vendor;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getMachineID() {
|
||||
return machineID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
public ProfileKind getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GeneralProvider implements ITerracottaProvider {
|
||||
private final TerracottaNative target;
|
||||
|
||||
public GeneralProvider(TerracottaNative target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status status() throws IOException {
|
||||
return target.status();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
Task<?> task = target.install(context, tree);
|
||||
context.bindProgress(task.progressProperty());
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(target.getPath(), Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_EXECUTE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
PosixFilePermission.OTHERS_EXECUTE
|
||||
)));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> ofCommandLine(Path path) {
|
||||
return List.of(target.getPath().toString(), "--hmcl", path.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ITerracottaProvider {
|
||||
enum Status {
|
||||
NOT_EXIST,
|
||||
LEGACY_VERSION,
|
||||
READY
|
||||
}
|
||||
|
||||
interface Context {
|
||||
void bindProgress(ObservableValue<? extends Number> value);
|
||||
|
||||
boolean requestInstallFence();
|
||||
|
||||
boolean hasInstallFence();
|
||||
}
|
||||
|
||||
abstract class ProviderException extends IOException {
|
||||
public ProviderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProviderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ProviderException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
final class ArchiveFileMissingException extends ProviderException {
|
||||
public ArchiveFileMissingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ArchiveFileMissingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ArchiveFileMissingException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
Status status() throws IOException;
|
||||
|
||||
Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException;
|
||||
|
||||
List<String> ofCommandLine(Path path);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.terracotta.provider;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MacOSProvider implements ITerracottaProvider {
|
||||
public final TerracottaNative installer, binary;
|
||||
|
||||
public MacOSProvider(TerracottaNative installer, TerracottaNative binary) {
|
||||
this.installer = installer;
|
||||
this.binary = binary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status status() throws IOException {
|
||||
assert binary != null;
|
||||
|
||||
if (!Files.exists(Path.of("/Applications/terracotta.app"))) {
|
||||
return Status.NOT_EXIST;
|
||||
}
|
||||
|
||||
return binary.status();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> install(Context context, @Nullable TarFileTree tree) throws IOException {
|
||||
assert installer != null && binary != null;
|
||||
|
||||
Task<?> installerTask = installer.install(context, tree);
|
||||
Task<?> binaryTask = binary.install(context, tree);
|
||||
context.bindProgress(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.4)); // (1 + 1) * 0.4 = 0.8
|
||||
|
||||
return Task.allOf(
|
||||
installerTask.thenComposeAsync(() -> {
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
"osascript",
|
||||
"-e",
|
||||
String.format(
|
||||
"do shell script \"installer -pkg %s -target /\" with prompt \"%s\" with administrator privileges",
|
||||
installer.getPath(),
|
||||
i18n("terracotta.sudo_installing")
|
||||
)
|
||||
));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit());
|
||||
}),
|
||||
binaryTask.thenRunAsync(() -> Files.setPosixFilePermissions(binary.getPath(), Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE,
|
||||
PosixFilePermission.GROUP_READ,
|
||||
PosixFilePermission.GROUP_EXECUTE,
|
||||
PosixFilePermission.OTHERS_READ,
|
||||
PosixFilePermission.OTHERS_EXECUTE
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> ofCommandLine(Path path) {
|
||||
assert binary != null;
|
||||
|
||||
return List.of(binary.getPath().toString(), "--hmcl", path.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class AwtUtils {
|
||||
private AwtUtils() {
|
||||
}
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
@@ -55,14 +56,17 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.terracotta.TerracottaPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
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.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -72,6 +76,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
public static final String JAVA_VERSION_TIP = "javaVersion";
|
||||
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
|
||||
public static final String SOFTWARE_RENDERING = "softwareRendering";
|
||||
|
||||
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
|
||||
@@ -90,7 +96,7 @@ public final class Controllers {
|
||||
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Path modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
});
|
||||
return gameListPage;
|
||||
@@ -106,6 +112,7 @@ public final class Controllers {
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
private static Lazy<TerracottaPage> terracottaPage = new Lazy<>(TerracottaPage::new);
|
||||
|
||||
private Controllers() {
|
||||
}
|
||||
@@ -148,6 +155,11 @@ public final class Controllers {
|
||||
return downloadPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static Node getTerracottaPage() {
|
||||
return terracottaPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static DecoratorController getDecorator() {
|
||||
return decorator;
|
||||
@@ -176,6 +188,18 @@ public final class Controllers {
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
if (System.getProperty("prism.lcdtext") == null) {
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
LOG.info("Enable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "true");
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|
||||
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|
||||
LOG.info("Disable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "false");
|
||||
}
|
||||
}
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
@@ -205,7 +229,11 @@ public final class Controllers {
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
&& !Controllers.stage.isIconified()
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS ||
|
||||
!Controllers.stage.isFullScreen() && !Controllers.stage.isMaximized())
|
||||
) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
@@ -310,26 +338,45 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION < 10) {
|
||||
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
|
||||
Number shownTipVersion = null;
|
||||
|
||||
try {
|
||||
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
|
||||
} catch (ClassCastException e) {
|
||||
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
|
||||
}
|
||||
|
||||
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
|
||||
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
|
||||
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
|
||||
if (downloadLink != null)
|
||||
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
|
||||
builder.addHyperLink(
|
||||
i18n("fatal.deprecated_java_version.download_link", Metadata.RECOMMENDED_JAVA_VERSION),
|
||||
downloadLink
|
||||
);
|
||||
Controllers.dialog(builder
|
||||
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether JIT is enabled in the current environment
|
||||
if (!JavaRuntime.CURRENT_JIT_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(JAVA_INTERPRETED_MODE_TIP))) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.java_interpreted_mode"), i18n("message.warning"), MessageType.WARNING)
|
||||
.ok(null)
|
||||
.addCancel(i18n("button.do_not_show_again"), () ->
|
||||
config().getShownTips().put(JAVA_INTERPRETED_MODE_TIP, true))
|
||||
.build());
|
||||
}
|
||||
|
||||
// Check whether hardware acceleration is enabled
|
||||
if (!FXUtils.GPU_ACCELERATION_ENABLED && !Boolean.TRUE.equals(config().getShownTips().get(SOFTWARE_RENDERING))) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("warning.software_rendering"), i18n("message.warning"), MessageType.WARNING)
|
||||
.ok(null)
|
||||
.addCancel(i18n("button.do_not_show_again"), () ->
|
||||
config().getShownTips().put(SOFTWARE_RENDERING, true))
|
||||
.build());
|
||||
}
|
||||
|
||||
if (globalConfig().getAgreementVersion() < 1) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
@@ -387,6 +434,38 @@ public final class Controllers {
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
|
||||
}
|
||||
|
||||
public static void confirmWithCountdown(String text, String title, int seconds, MessageType messageType,
|
||||
@Nullable Runnable ok, @Nullable Runnable cancel) {
|
||||
if (seconds <= 0)
|
||||
throw new IllegalArgumentException("Seconds must be greater than 0");
|
||||
|
||||
JFXButton btnOk = new JFXButton(i18n("button.ok"));
|
||||
btnOk.getStyleClass().add(messageType == MessageType.WARNING || messageType == MessageType.ERROR
|
||||
? "dialog-error"
|
||||
: "dialog-accept");
|
||||
|
||||
if (ok != null)
|
||||
btnOk.setOnAction(e -> ok.run());
|
||||
btnOk.setDisable(true);
|
||||
|
||||
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
|
||||
for (int i = 0; i < seconds; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(btnOk.textProperty(), i18n("button.ok.countdown", seconds - i)));
|
||||
}
|
||||
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
|
||||
new KeyValue(btnOk.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(btnOk.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
confirmAction(text, title, messageType, btnOk, () -> {
|
||||
timeline.stop();
|
||||
if (cancel != null)
|
||||
cancel.run();
|
||||
});
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
|
||||
return prompt(title, onResult, "");
|
||||
}
|
||||
@@ -433,6 +512,10 @@ public final class Controllers {
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
break;
|
||||
case "hmcl://game/launch":
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
FXUtils.openLink(href);
|
||||
|
||||
@@ -18,22 +18,25 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -41,24 +44,23 @@ 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;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.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.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.image.ImageLoader;
|
||||
import org.jackhuang.hmcl.ui.image.ImageUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
@@ -66,33 +68,35 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.*;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.*;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
@@ -105,6 +109,32 @@ public final class FXUtils {
|
||||
|
||||
public static final int JAVAFX_MAJOR_VERSION;
|
||||
|
||||
public static final String GRAPHICS_PIPELINE;
|
||||
public static final boolean GPU_ACCELERATION_ENABLED;
|
||||
|
||||
static {
|
||||
String pipelineName = "";
|
||||
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
if (pipeline != null) {
|
||||
pipelineName = pipeline.getClass().getName();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get prism pipeline", e);
|
||||
}
|
||||
|
||||
GRAPHICS_PIPELINE = pipelineName;
|
||||
GPU_ACCELERATION_ENABLED = !pipelineName.endsWith(".SWPipeline");
|
||||
}
|
||||
|
||||
/// @see Platform.Preferences
|
||||
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
|
||||
public static final @Nullable ObservableBooleanValue DARK_MODE;
|
||||
public static final @Nullable Boolean REDUCED_MOTION;
|
||||
|
||||
public static final @Nullable MethodHandle TEXT_TRUNCATED_PROPERTY;
|
||||
|
||||
static {
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
int majorVersion = -1;
|
||||
@@ -115,27 +145,65 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
JAVAFX_MAJOR_VERSION = majorVersion;
|
||||
|
||||
ObservableMap<String, Object> preferences = null;
|
||||
ObservableBooleanValue darkMode = null;
|
||||
Boolean reducedMotion = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 22) {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
Class<?> preferencesClass = Class.forName("javafx.application.Platform$Preferences");
|
||||
@SuppressWarnings("unchecked")
|
||||
var preferences0 = (ObservableMap<String, Object>) lookup.findStatic(Platform.class, "getPreferences", MethodType.methodType(preferencesClass))
|
||||
.invoke();
|
||||
preferences = preferences0;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var colorSchemeProperty =
|
||||
(ReadOnlyObjectProperty<? extends Enum<?>>)
|
||||
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
|
||||
.invoke(preferences);
|
||||
|
||||
darkMode = Bindings.createBooleanBinding(() ->
|
||||
"DARK".equals(colorSchemeProperty.get().name()), colorSchemeProperty);
|
||||
|
||||
if (JAVAFX_MAJOR_VERSION >= 24) {
|
||||
reducedMotion = (boolean)
|
||||
lookup.findVirtual(preferencesClass, "isReducedMotion", MethodType.methodType(boolean.class))
|
||||
.invoke(preferences);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get preferences", e);
|
||||
}
|
||||
}
|
||||
PREFERENCES = preferences;
|
||||
DARK_MODE = darkMode;
|
||||
REDUCED_MOTION = reducedMotion;
|
||||
|
||||
MethodHandle textTruncatedProperty = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 23) {
|
||||
try {
|
||||
textTruncatedProperty = MethodHandles.publicLookup().findVirtual(
|
||||
Labeled.class,
|
||||
"textTruncatedProperty",
|
||||
MethodType.methodType(ReadOnlyBooleanProperty.class)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to lookup textTruncatedProperty", e);
|
||||
}
|
||||
}
|
||||
TEXT_TRUNCATED_PROPERTY = textTruncatedProperty;
|
||||
}
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
|
||||
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp"
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
|
||||
);
|
||||
|
||||
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
|
||||
try {
|
||||
Files.deleteIfExists(entry.getValue());
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
|
||||
}
|
||||
remoteImageCache.remove(entry.getKey());
|
||||
}
|
||||
|
||||
builtinImageCache.clear();
|
||||
}
|
||||
|
||||
@@ -324,12 +392,35 @@ public final class FXUtils {
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
/// If the current environment is JavaFX 23 or higher, this method returns [Labeled#textTruncatedProperty()];
|
||||
/// Otherwise, it returns `null`.
|
||||
public static @Nullable ReadOnlyBooleanProperty textTruncatedProperty(Labeled labeled) {
|
||||
if (TEXT_TRUNCATED_PROPERTY != null) {
|
||||
try {
|
||||
return (ReadOnlyBooleanProperty) TEXT_TRUNCATED_PROPERTY.invokeExact(labeled);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
|
||||
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
tooltip.setShowDelay(showDelay);
|
||||
tooltip.setShowDuration(showDuration);
|
||||
tooltip.setHideDelay(hideDelay);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
@@ -337,7 +428,7 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
@@ -368,20 +459,27 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFolder(File file) {
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
LOG.error("Unable to make directory " + file);
|
||||
public static void openFolder(Path file) {
|
||||
if (file.getFileSystem() != FileSystems.getDefault()) {
|
||||
LOG.warning("Cannot open folder as the file system is not supported: " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = file.getAbsolutePath();
|
||||
try {
|
||||
Files.createDirectories(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to create directory " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
|
||||
String openCommand;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && Files.exists(Path.of("/usr/bin/xdg-open")))
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
else
|
||||
openCommand = null;
|
||||
@@ -403,7 +501,7 @@ public final class FXUtils {
|
||||
|
||||
// Fallback to java.awt.Desktop::open
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
java.awt.Desktop.getDesktop().open(file.toFile());
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
@@ -446,11 +544,11 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
// Fallback to open folder
|
||||
openFolder(file.getParent().toFile());
|
||||
openFolder(file.getParent());
|
||||
});
|
||||
} else {
|
||||
// We do not have a universal method to show file in file manager.
|
||||
openFolder(file.getParent().toFile());
|
||||
openFolder(file.getParent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,37 +571,35 @@ public final class FXUtils {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
String uri = NetworkUtils.encodeLocation(link);
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", uri});
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("An exception occurred while calling rundll32", e);
|
||||
}
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
Path path = SystemUtils.which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), link});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Runtime.getRuntime().exec(new String[]{"open", uri});
|
||||
return;
|
||||
} else {
|
||||
for (String browser : linuxBrowsers) {
|
||||
Path path = SystemUtils.which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), uri});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.warning("No known browser found");
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
LOG.warning("Unable to open link: " + link, ex);
|
||||
}
|
||||
LOG.warning("Failed to open link: " + link + ", fallback to java.awt.Desktop", e);
|
||||
}
|
||||
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(uri));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
@@ -700,6 +796,240 @@ public final class FXUtils {
|
||||
property.removeListener(binding);
|
||||
}
|
||||
|
||||
private static final class PaintBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<ColorPicker> colorPickerRef;
|
||||
private final WeakReference<Property<Paint>> propertyRef;
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private PaintBidirectionalBinding(ColorPicker colorPicker, Property<Paint> property) {
|
||||
this.colorPickerRef = new WeakReference<>(colorPicker);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.hashCode = System.identityHashCode(colorPicker) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable sourceProperty) {
|
||||
if (!updating) {
|
||||
final ColorPicker colorPicker = colorPickerRef.get();
|
||||
final Property<Paint> property = propertyRef.get();
|
||||
|
||||
if (colorPicker == null || property == null) {
|
||||
if (colorPicker != null) {
|
||||
colorPicker.valueProperty().removeListener(this);
|
||||
}
|
||||
|
||||
if (property != null) {
|
||||
property.removeListener(this);
|
||||
}
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
if (property == sourceProperty) {
|
||||
Paint newValue = property.getValue();
|
||||
if (newValue instanceof Color)
|
||||
colorPicker.setValue((Color) newValue);
|
||||
else
|
||||
colorPicker.setValue(null);
|
||||
} else {
|
||||
Paint newValue = colorPicker.getValue();
|
||||
property.setValue(newValue);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return colorPickerRef.get() == null || propertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof FXUtils.PaintBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (FXUtils.PaintBidirectionalBinding) o;
|
||||
|
||||
final ColorPicker colorPicker = this.colorPickerRef.get();
|
||||
final Property<Paint> property = this.propertyRef.get();
|
||||
|
||||
final ColorPicker thatColorPicker = that.colorPickerRef.get();
|
||||
final Property<?> thatProperty = that.propertyRef.get();
|
||||
|
||||
if (colorPicker == null || property == null || thatColorPicker == null || thatProperty == null)
|
||||
return false;
|
||||
|
||||
return colorPicker == thatColorPicker && property == thatProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindPaint(ColorPicker colorPicker, Property<Paint> property) {
|
||||
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
|
||||
|
||||
colorPicker.valueProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
|
||||
if (property.getValue() instanceof Color)
|
||||
colorPicker.setValue((Color) property.getValue());
|
||||
else
|
||||
colorPicker.setValue(null);
|
||||
|
||||
colorPicker.valueProperty().addListener(binding);
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<JFXComboBox<String>> comboBoxRef;
|
||||
private final WeakReference<IntegerProperty> widthPropertyRef;
|
||||
private final WeakReference<IntegerProperty> heightPropertyRef;
|
||||
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
|
||||
IntegerProperty widthProperty,
|
||||
IntegerProperty heightProperty) {
|
||||
this.comboBoxRef = new WeakReference<>(comboBox);
|
||||
this.widthPropertyRef = new WeakReference<>(widthProperty);
|
||||
this.heightPropertyRef = new WeakReference<>(heightProperty);
|
||||
this.hashCode = System.identityHashCode(comboBox)
|
||||
^ System.identityHashCode(widthProperty)
|
||||
^ System.identityHashCode(heightProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (!updating) {
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null) {
|
||||
if (comboBox != null) {
|
||||
comboBox.focusedProperty().removeListener(this);
|
||||
comboBox.sceneProperty().removeListener(this);
|
||||
}
|
||||
if (widthProperty != null)
|
||||
widthProperty.removeListener(this);
|
||||
if (heightProperty != null)
|
||||
heightProperty.removeListener(this);
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
int width = widthProperty.get();
|
||||
int height = heightProperty.get();
|
||||
|
||||
if (observable instanceof ReadOnlyProperty<?>
|
||||
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
|
||||
String value = comboBox.valueProperty().get();
|
||||
if (value == null)
|
||||
value = "";
|
||||
int idx = value.indexOf('x');
|
||||
if (idx < 0)
|
||||
idx = value.indexOf('*');
|
||||
|
||||
if (idx < 0) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
String widthStr = value.substring(0, idx).trim();
|
||||
String heightStr = value.substring(idx + 1).trim();
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
try {
|
||||
newWidth = Integer.parseInt(widthStr);
|
||||
newHeight = Integer.parseInt(heightStr);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
widthProperty.set(newWidth);
|
||||
heightProperty.set(newHeight);
|
||||
} else {
|
||||
comboBox.setValue(width + "x" + height);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return this.comboBoxRef.get() == null
|
||||
|| this.widthPropertyRef.get() == null
|
||||
|| this.heightPropertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof WindowsSizeBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (WindowsSizeBidirectionalBinding) obj;
|
||||
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
var thatComboBox = that.comboBoxRef.get();
|
||||
var thatWidthProperty = that.widthPropertyRef.get();
|
||||
var thatHeightProperty = that.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null
|
||||
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return comboBox == thatComboBox
|
||||
&& widthProperty == thatWidthProperty
|
||||
&& heightProperty == thatHeightProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().addListener(binding);
|
||||
comboBox.sceneProperty().addListener(binding);
|
||||
widthProperty.addListener(binding);
|
||||
heightProperty.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().removeListener(binding);
|
||||
comboBox.sceneProperty().removeListener(binding);
|
||||
widthProperty.removeListener(binding);
|
||||
heightProperty.removeListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
int itemCount = children.length;
|
||||
int childSelectedCount = 0;
|
||||
@@ -767,47 +1097,53 @@ public final class FXUtils {
|
||||
stage.getIcons().add(newBuiltinImage(icon));
|
||||
}
|
||||
|
||||
private static Image loadWebPImage(InputStream input) throws IOException {
|
||||
WebPImageReaderSpi spi = new WebPImageReaderSpi();
|
||||
ImageReader reader = spi.createReaderInstance(null);
|
||||
|
||||
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
|
||||
reader.setInput(imageInput, true, true);
|
||||
return SwingFXUtils.toFXImage(reader.read(0, reader.getDefaultReadParam()), null);
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static Image loadImage(Path path) throws Exception {
|
||||
try (InputStream input = Files.newInputStream(path)) {
|
||||
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
|
||||
return loadWebPImage(input);
|
||||
else {
|
||||
Image image = new Image(input);
|
||||
if (image.isError())
|
||||
throw image.getException();
|
||||
return image;
|
||||
return loadImage(path, 0, 0, false, false);
|
||||
}
|
||||
|
||||
public static Image loadImage(Path path,
|
||||
int requestedWidth, int requestedHeight,
|
||||
boolean preserveRatio, boolean smooth) throws Exception {
|
||||
try (var input = new BufferedInputStream(Files.newInputStream(path))) {
|
||||
String ext = FileUtils.getExtension(path).toLowerCase(Locale.ROOT);
|
||||
ImageLoader loader = ImageUtils.EXT_TO_LOADER.get(ext);
|
||||
if (loader == null && !ImageUtils.DEFAULT_EXTS.contains(ext)) {
|
||||
input.mark(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
byte[] headerBuffer = input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
input.reset();
|
||||
loader = ImageUtils.guessLoader(headerBuffer);
|
||||
}
|
||||
if (loader == null)
|
||||
loader = ImageUtils.DEFAULT;
|
||||
return loader.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
}
|
||||
}
|
||||
|
||||
public static Image loadImage(URL url) throws Exception {
|
||||
URLConnection connection = NetworkUtils.createConnection(url);
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
}
|
||||
public static Image loadImage(String url) throws Exception {
|
||||
URI uri = NetworkUtils.toURI(url);
|
||||
|
||||
try (InputStream input = connection.getInputStream()) {
|
||||
String path = url.getPath();
|
||||
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
|
||||
return loadWebPImage(input);
|
||||
else {
|
||||
Image image = new Image(input);
|
||||
if (image.isError())
|
||||
throw image.getException();
|
||||
return image;
|
||||
URLConnection connection = NetworkUtils.createConnection(uri);
|
||||
if (connection instanceof HttpURLConnection)
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
|
||||
try (BufferedInputStream input = new BufferedInputStream(connection.getInputStream())) {
|
||||
String contentType = Objects.requireNonNull(connection.getContentType(), "");
|
||||
Matcher matcher = ImageUtils.CONTENT_TYPE_PATTERN.matcher(contentType);
|
||||
if (matcher.find())
|
||||
contentType = matcher.group("type");
|
||||
|
||||
ImageLoader loader = ImageUtils.CONTENT_TYPE_TO_LOADER.get(contentType);
|
||||
if (loader == null && !ImageUtils.DEFAULT_CONTENT_TYPES.contains(contentType)) {
|
||||
input.mark(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
byte[] headerBuffer = input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
input.reset();
|
||||
loader = ImageUtils.guessLoader(headerBuffer);
|
||||
}
|
||||
|
||||
if (loader == null)
|
||||
loader = ImageUtils.DEFAULT;
|
||||
|
||||
return loader.load(input, 0, 0, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,78 +1187,22 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(url)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @param requestedWidth the image's bounding box width
|
||||
* @param requestedHeight the image's bounding box height
|
||||
* @param preserveRatio indicates whether to preserve the aspect ratio of
|
||||
* the original image when scaling to fit the image within the
|
||||
* specified bounding box
|
||||
* @param smooth indicates whether to use a better quality filtering
|
||||
* algorithm or a faster one when scaling this image to fit within
|
||||
* the specified bounding box
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
|
||||
Path currentPath = remoteImageCache.get(url);
|
||||
if (currentPath != null) {
|
||||
if (Files.isReadable(currentPath)) {
|
||||
try (InputStream inputStream = Files.newInputStream(currentPath)) {
|
||||
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("An exception encountered while reading data from cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// The file is unavailable or unreadable.
|
||||
remoteImageCache.remove(url);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(currentPath);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("An exception encountered while deleting broken cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
|
||||
image.progressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
|
||||
Task.runAsync(() -> {
|
||||
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
|
||||
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
|
||||
OutputStream outputStream = Files.newOutputStream(newPath);
|
||||
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
|
||||
) {
|
||||
writer.write(PNGJavaFXUtils.asArgbImage(image));
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.delete(newPath);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
throw e2;
|
||||
}
|
||||
throw e;
|
||||
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
var image = new SimpleObjectProperty<Image>();
|
||||
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
image.set(result);
|
||||
} else {
|
||||
LOG.warning("An exception encountered while loading remote image: " + url, exception);
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
})
|
||||
.start();
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -936,29 +1216,57 @@ public final class FXUtils {
|
||||
public static JFXButton newBorderButton(String text) {
|
||||
JFXButton button = new JFXButton(text);
|
||||
button.getStyleClass().add("jfx-button-border");
|
||||
button.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label truncatedLabel(String text, int limit) {
|
||||
Label label = new Label();
|
||||
if (text.length() <= limit) {
|
||||
label.setText(text);
|
||||
} else {
|
||||
label.setText(StringUtils.truncate(text, limit));
|
||||
installFastTooltip(label, text);
|
||||
}
|
||||
public static JFXButton newToggleButton4(SVG icon) {
|
||||
JFXButton button = new JFXButton();
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(icon.createIcon(Theme.blackFill(), -1));
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label newSafeTruncatedLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
|
||||
showTooltipWhenTruncated(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
private static final String LABEL_FULL_TEXT_PROP_KEY = FXUtils.class.getName() + ".LABEL_FULL_TEXT";
|
||||
|
||||
public static void showTooltipWhenTruncated(Labeled labeled) {
|
||||
ReadOnlyBooleanProperty textTruncatedProperty = textTruncatedProperty(labeled);
|
||||
if (textTruncatedProperty != null) {
|
||||
ChangeListener<Boolean> listener = (observable, oldValue, newValue) -> {
|
||||
var label = (Labeled) ((ReadOnlyProperty<?>) observable).getBean();
|
||||
var tooltip = (Tooltip) label.getProperties().get(LABEL_FULL_TEXT_PROP_KEY);
|
||||
|
||||
if (newValue) {
|
||||
if (tooltip == null) {
|
||||
tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(label.textProperty());
|
||||
label.getProperties().put(LABEL_FULL_TEXT_PROP_KEY, tooltip);
|
||||
}
|
||||
|
||||
FXUtils.installFastTooltip(label, tooltip);
|
||||
} else if (tooltip != null) {
|
||||
Tooltip.uninstall(label, tooltip);
|
||||
}
|
||||
};
|
||||
listener.changed(textTruncatedProperty, false, textTruncatedProperty.get());
|
||||
textTruncatedProperty.addListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, PathMatcher filter, Consumer<List<Path>> callback) {
|
||||
applyDragListener(node, filter, callback, null);
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback, Runnable dragDropped) {
|
||||
public static void applyDragListener(Node node, PathMatcher filter, Consumer<List<Path>> callback, Runnable dragDropped) {
|
||||
node.setOnDragOver(event -> {
|
||||
if (event.getGestureSource() != node && event.getDragboard().hasFiles()) {
|
||||
if (event.getDragboard().getFiles().stream().anyMatch(filter::accept))
|
||||
if (event.getDragboard().getFiles().stream().map(File::toPath).anyMatch(filter::matches))
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
}
|
||||
event.consume();
|
||||
@@ -967,7 +1275,7 @@ public final class FXUtils {
|
||||
node.setOnDragDropped(event -> {
|
||||
List<File> files = event.getDragboard().getFiles();
|
||||
if (files != null) {
|
||||
List<File> acceptFiles = files.stream().filter(filter::accept).collect(Collectors.toList());
|
||||
List<Path> acceptFiles = files.stream().map(File::toPath).filter(filter::matches).toList();
|
||||
if (!acceptFiles.isEmpty()) {
|
||||
callback.accept(acceptFiles);
|
||||
event.setDropCompleted(true);
|
||||
@@ -1059,6 +1367,37 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> void onScroll(Node node, List<T> list,
|
||||
ToIntFunction<List<T>> finder,
|
||||
Consumer<T> updater
|
||||
) {
|
||||
node.addEventHandler(ScrollEvent.SCROLL, event -> {
|
||||
double deltaY = event.getDeltaY();
|
||||
if (deltaY == 0)
|
||||
return;
|
||||
|
||||
int index = finder.applyAsInt(list);
|
||||
if (index < 0) return;
|
||||
if (deltaY > 0) // up
|
||||
index--;
|
||||
else // down
|
||||
index++;
|
||||
|
||||
updater.accept(list.get((index + list.size()) % list.size()));
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
public static void clearFocus(Node node) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene != null) {
|
||||
Parent root = scene.getRoot();
|
||||
if (root != null) {
|
||||
root.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyOnDoubleClick(Labeled label) {
|
||||
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||
@@ -1072,12 +1411,16 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
copyText(text, i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static void copyText(String text, @Nullable String toastMessage) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
if (!Controllers.isStopped()) {
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
if (toastMessage != null && !Controllers.isStopped()) {
|
||||
Controllers.showToast(toastMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,4 +1494,65 @@ public final class FXUtils {
|
||||
return new FileChooser.ExtensionFilter(i18n("extension.png"),
|
||||
IMAGE_EXTENSIONS.stream().map(ext -> "*." + ext).toArray(String[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligently determines the popup position to prevent the menu from exceeding screen boundaries.
|
||||
* Supports multi-monitor setups by detecting the current screen where the component is located.
|
||||
* Now handles first-time popup display by forcing layout measurement.
|
||||
*
|
||||
* @param root the root node to calculate position relative to
|
||||
* @param popupInstance the popup instance to position
|
||||
* @return the optimal vertical position for the popup menu
|
||||
*/
|
||||
public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, JFXPopup popupInstance) {
|
||||
// Get the screen bounds in screen coordinates
|
||||
Bounds screenBounds = root.localToScreen(root.getBoundsInLocal());
|
||||
|
||||
// Convert Bounds to Rectangle2D for getScreensForRectangle method
|
||||
Rectangle2D boundsRect = new Rectangle2D(
|
||||
screenBounds.getMinX(), screenBounds.getMinY(),
|
||||
screenBounds.getWidth(), screenBounds.getHeight()
|
||||
);
|
||||
|
||||
// Find the screen that contains this component (supports multi-monitor)
|
||||
List<Screen> screens = Screen.getScreensForRectangle(boundsRect);
|
||||
Screen currentScreen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0);
|
||||
Rectangle2D visualBounds = currentScreen.getVisualBounds();
|
||||
|
||||
double screenHeight = visualBounds.getHeight();
|
||||
double screenMinY = visualBounds.getMinY();
|
||||
double itemScreenY = screenBounds.getMinY();
|
||||
|
||||
// Calculate available space relative to the current screen
|
||||
double availableSpaceAbove = itemScreenY - screenMinY;
|
||||
double availableSpaceBelow = screenMinY + screenHeight - itemScreenY - root.getBoundsInLocal().getHeight();
|
||||
|
||||
// Get popup content and ensure it's properly measured
|
||||
Region popupContent = popupInstance.getPopupContent();
|
||||
|
||||
double menuHeight;
|
||||
if (popupContent.getHeight() <= 0) {
|
||||
// Force layout measurement if height is not yet available
|
||||
popupContent.autosize();
|
||||
popupContent.applyCss();
|
||||
popupContent.layout();
|
||||
|
||||
// Get the measured height, or use a reasonable fallback
|
||||
menuHeight = popupContent.getHeight();
|
||||
if (menuHeight <= 0) {
|
||||
// Fallback: estimate based on number of menu items
|
||||
// Each menu item is roughly 36px height + separators + padding
|
||||
menuHeight = 300; // Conservative estimate for the current menu structure
|
||||
}
|
||||
} else {
|
||||
menuHeight = popupContent.getHeight();
|
||||
}
|
||||
|
||||
// Add some margin for safety
|
||||
menuHeight += 20;
|
||||
|
||||
return (availableSpaceAbove > menuHeight && availableSpaceBelow < menuHeight)
|
||||
? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward
|
||||
: JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,12 @@ import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -59,9 +62,9 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final Version version;
|
||||
@@ -129,7 +132,7 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
@@ -283,7 +286,7 @@ public class GameCrashWindow extends Stage {
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
});
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
@@ -379,7 +382,7 @@ public class GameCrashWindow extends Stage {
|
||||
TwoLineListItem gameDir = new TwoLineListItem();
|
||||
gameDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
gameDir.setTitle(i18n("game.directory"));
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().toAbsolutePath().toString());
|
||||
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
|
||||
|
||||
TwoLineListItem javaDir = new TwoLineListItem();
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
|
||||
@@ -159,28 +160,19 @@ public final class HTMLRenderer {
|
||||
|
||||
private void appendImage(Node node) {
|
||||
String src = node.absUrl("src");
|
||||
URI imageUri = null;
|
||||
try {
|
||||
if (!src.isEmpty())
|
||||
imageUri = URI.create(src);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
String alt = node.attr("alt");
|
||||
|
||||
if (imageUri != null) {
|
||||
URI uri = URI.create(src);
|
||||
|
||||
if (StringUtils.isNotBlank(src)) {
|
||||
String widthAttr = node.attr("width");
|
||||
String heightAttr = node.attr("height");
|
||||
|
||||
double width = 0;
|
||||
double height = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
|
||||
try {
|
||||
width = Double.parseDouble(widthAttr);
|
||||
height = Double.parseDouble(heightAttr);
|
||||
width = (int) Double.parseDouble(widthAttr);
|
||||
height = (int) Double.parseDouble(heightAttr);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
@@ -190,10 +182,12 @@ public final class HTMLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
|
||||
if (image.isError()) {
|
||||
LOG.warning("Failed to load image: " + uri, image.getException());
|
||||
} else {
|
||||
try {
|
||||
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
|
||||
.run();
|
||||
if (image == null)
|
||||
throw new AssertionError("Image loading task returned null");
|
||||
|
||||
ImageView imageView = new ImageView(image);
|
||||
if (hyperlink != null) {
|
||||
URI target = resolveLink(hyperlink);
|
||||
@@ -204,6 +198,8 @@ public final class HTMLRenderer {
|
||||
}
|
||||
children.add(imageView);
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load image: " + src, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -138,6 +139,9 @@ public class InstallerItem extends Control {
|
||||
case "forge":
|
||||
iconType = VersionIconType.FORGE;
|
||||
break;
|
||||
case "cleanroom":
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
break;
|
||||
case "liteloader":
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
break;
|
||||
@@ -232,6 +236,7 @@ public class InstallerItem extends Control {
|
||||
InstallerItem fabric = new InstallerItem(FABRIC, style);
|
||||
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(FORGE, style);
|
||||
InstallerItem cleanroom = new InstallerItem(CLEANROOM, style);
|
||||
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
|
||||
@@ -239,11 +244,11 @@ public class InstallerItem extends Control {
|
||||
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
|
||||
|
||||
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge, cleanroom);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine, cleanroom);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine, cleanroom);
|
||||
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
@@ -277,7 +282,7 @@ public class InstallerItem extends Control {
|
||||
game.versionProperty.set(new InstalledState(gameVersion, false, false));
|
||||
}
|
||||
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, cleanroom};
|
||||
|
||||
for (InstallerItem item : all) {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
@@ -293,6 +298,8 @@ public class InstallerItem extends Control {
|
||||
|
||||
if (gameVersion == null) {
|
||||
this.libraries = all;
|
||||
} else if (gameVersion.equals("1.12.2")) {
|
||||
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, optiFine};
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
|
||||
} else {
|
||||
@@ -312,7 +319,9 @@ public class InstallerItem extends Control {
|
||||
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
|
||||
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
|
||||
private static final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final ChangeListener<Number> holder;
|
||||
|
||||
InstallerItemSkin(InstallerItem control) {
|
||||
super(control);
|
||||
@@ -320,9 +329,10 @@ public class InstallerItem extends Control {
|
||||
Pane pane;
|
||||
if (control.style == Style.CARD) {
|
||||
pane = new VBox();
|
||||
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
|
||||
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
|
||||
} else {
|
||||
pane = new HBox();
|
||||
holder = null;
|
||||
}
|
||||
pane.getStyleClass().add("installer-item");
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
|
||||
@@ -64,11 +64,9 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
vBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
vBox.setPickOnBounds(false);
|
||||
|
||||
JFXButton btnAdd = new JFXButton();
|
||||
JFXButton btnAdd = FXUtils.newRaisedButton("");
|
||||
FXUtils.setLimitWidth(btnAdd, 40);
|
||||
FXUtils.setLimitHeight(btnAdd, 40);
|
||||
btnAdd.getStyleClass().add("jfx-button-raised-round");
|
||||
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnAdd.setGraphic(SVG.ADD.createIcon(Theme.whiteFill(), -1));
|
||||
btnAdd.setOnAction(e -> skinnable.add());
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
@@ -186,7 +186,7 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private void onTerminateGame() {
|
||||
LauncherHelper.stopManagedProcesses();
|
||||
LogWindow.this.gameProcess.stop();
|
||||
}
|
||||
|
||||
private void onClear() {
|
||||
@@ -224,7 +224,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
try {
|
||||
if (gameProcess.isRunning()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@@ -312,6 +312,7 @@ public final class LogWindow extends Stage {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
x.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
|
||||
@@ -100,7 +100,10 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
ret.setOnAction(e -> {
|
||||
onClick.run();
|
||||
FXUtils.clearFocus(ret);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public class TooltipInstaller {
|
||||
public static final TooltipInstaller INSTALLER;
|
||||
|
||||
static {
|
||||
TooltipInstaller installer = null;
|
||||
|
||||
try {
|
||||
installer = new NewInstaller();
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
installer = new OldInstaller();
|
||||
} catch (Throwable e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to initialize TooltipInstaller", e2);
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLER = installer != null ? installer : new TooltipInstaller();
|
||||
}
|
||||
|
||||
TooltipInstaller() {
|
||||
}
|
||||
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
// For Java 8
|
||||
private static final class OldInstaller extends TooltipInstaller {
|
||||
private static final Constructor<?> createTooltipBehavior;
|
||||
private static final Method installTooltipBehavior;
|
||||
|
||||
static {
|
||||
try {
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
createTooltipBehavior.setAccessible(true);
|
||||
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installTooltipBehavior.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
|
||||
installTooltipBehavior.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For Java 9+
|
||||
private static final class NewInstaller extends TooltipInstaller {
|
||||
private static final MethodHandle setTooltipShowDelay;
|
||||
private static final MethodHandle setTooltipShowDuration;
|
||||
private static final MethodHandle setTooltipHideDelay;
|
||||
|
||||
static {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
MethodType methodType = MethodType.methodType(void.class, Duration.class);
|
||||
|
||||
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
|
||||
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
|
||||
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
setTooltipShowDelay.invokeExact(tooltip, showDelay);
|
||||
setTooltipShowDuration.invokeExact(tooltip, showDuration);
|
||||
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
}
|
||||
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,22 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
setBody(new ProgressIndicator());
|
||||
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
boolean isPreview = remoteVersion.isPreview();
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("#nowchange");
|
||||
String id = null;
|
||||
Node node = null;
|
||||
if (isPreview) {
|
||||
id = "nowpreview";
|
||||
node = document.selectFirst("#" + id);
|
||||
}
|
||||
if (node == null) {
|
||||
id = "nowchange";
|
||||
node = document.selectFirst("#" + id);
|
||||
}
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName()))
|
||||
throw new IOException("Cannot find #nowchange in document");
|
||||
throw new IOException("Cannot find current changelog in document");
|
||||
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
LOG.info("Open link: " + uri);
|
||||
@@ -60,7 +71,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
});
|
||||
|
||||
do {
|
||||
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
|
||||
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
|
||||
break;
|
||||
}
|
||||
renderer.appendNode(node);
|
||||
|
||||
@@ -21,7 +21,6 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
@@ -77,18 +76,9 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
|
||||
setActionButtonVisible(false);
|
||||
|
||||
setOnScroll(event -> {
|
||||
Account current = account.get();
|
||||
if (current == null) return;
|
||||
ObservableList<Account> accounts = Accounts.getAccounts();
|
||||
int currentIndex = accounts.indexOf(account.get());
|
||||
if (event.getDeltaY() > 0) { // up
|
||||
currentIndex--;
|
||||
} else { // down
|
||||
currentIndex++;
|
||||
}
|
||||
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
|
||||
});
|
||||
FXUtils.onScroll(this, Accounts.getAccounts(),
|
||||
accounts -> accounts.indexOf(account.get()),
|
||||
Accounts::setSelectedAccount);
|
||||
}
|
||||
|
||||
public ObjectProperty<Account> accountProperty() {
|
||||
|
||||
@@ -41,13 +41,14 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -116,8 +117,7 @@ public class AccountListItem extends RadioButton {
|
||||
}
|
||||
|
||||
public ObservableBooleanValue canUploadSkin() {
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
|
||||
if (account instanceof AuthlibInjectorAccount aiAccount) {
|
||||
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
|
||||
return createBooleanBinding(() -> {
|
||||
Set<TextureType> uploadableTextures = profile.get()
|
||||
@@ -148,7 +148,7 @@ public class AccountListItem extends RadioButton {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("account.skin.upload"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -156,7 +156,7 @@ public class AccountListItem extends RadioButton {
|
||||
return refreshAsync()
|
||||
.thenRunAsync(() -> {
|
||||
Image skinImg;
|
||||
try (FileInputStream input = new FileInputStream(selectedFile)) {
|
||||
try (var input = Files.newInputStream(selectedFile)) {
|
||||
skinImg = new Image(input);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidSkinException("Failed to read skin image", e);
|
||||
@@ -167,7 +167,7 @@ public class AccountListItem extends RadioButton {
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
|
||||
account.uploadSkin(skin.isSlim(), selectedFile);
|
||||
})
|
||||
.thenComposeAsync(refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
|
||||
@@ -33,6 +33,7 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -128,6 +129,10 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
btnRefresh.setDisable(true);
|
||||
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
|
||||
}
|
||||
btnRefresh.setOnAction(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
skinnable.refreshAsync()
|
||||
|
||||
@@ -19,7 +19,13 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -41,58 +47,27 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
|
||||
|
||||
private static boolean isExemptedRegion() {
|
||||
String zoneId = ZoneId.systemDefault().getId();
|
||||
if (Arrays.asList(
|
||||
"Asia/Shanghai",
|
||||
// Although Asia/Beijing is not a legal name, Deepin uses it
|
||||
"Asia/Beijing",
|
||||
"Asia/Chongqing",
|
||||
"Asia/Chungking",
|
||||
"Asia/Harbin"
|
||||
).contains(zoneId))
|
||||
return true;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && NativeUtils.USE_JNA) {
|
||||
Kernel32 kernel32 = Kernel32.INSTANCE;
|
||||
|
||||
// https://learn.microsoft.com/windows/win32/intl/table-of-geographical-locations
|
||||
if (kernel32 != null && kernel32.GetUserGeoID(WinConstants.GEOCLASS_NATION) == 45) // China
|
||||
return true;
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && "GMT+08:00".equals(zoneId))
|
||||
// Some Linux distributions may use invalid time zone ids (e.g., Asia/Beijing)
|
||||
// Java may not be able to resolve this name and use GMT+08:00 instead.
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static {
|
||||
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
|
||||
|
||||
if ("false".equals(property)
|
||||
|| "auto".equals(property) && isExemptedRegion()
|
||||
|| "auto".equals(property) && LocaleUtils.IS_CHINA_MAINLAND
|
||||
|| globalConfig().isEnableOfflineAccount())
|
||||
RESTRICTED.set(false);
|
||||
else
|
||||
@@ -190,7 +165,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
item.titleProperty().bind(title);
|
||||
String host = "";
|
||||
try {
|
||||
host = URI.create(server.getUrl()).getHost();
|
||||
host = NetworkUtils.toURI(server.getUrl()).getHost();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
@@ -196,7 +195,8 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
lblServerName.setText(serverBeingAdded.getName());
|
||||
lblServerUrl.setText(serverBeingAdded.getUrl());
|
||||
|
||||
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
|
||||
//noinspection HttpUrlsUsage
|
||||
lblServerWarning.setVisible(serverBeingAdded.getUrl().startsWith("http://"));
|
||||
|
||||
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
|
||||
} else {
|
||||
|
||||
@@ -19,9 +19,6 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -39,7 +36,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
@@ -267,34 +263,13 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}).executor(true);
|
||||
};
|
||||
|
||||
if (factory instanceof OfflineAccountFactory && username != null && !USERNAME_CHECKER_PATTERN.matcher(username).matches()) {
|
||||
JFXButton btnYes = new JFXButton(i18n("button.ok"));
|
||||
btnYes.getStyleClass().add("dialog-error");
|
||||
btnYes.setOnAction(e -> doCreate.run());
|
||||
btnYes.setDisable(true);
|
||||
|
||||
int countdown = 10;
|
||||
KeyFrame[] keyFrames = new KeyFrame[countdown + 1];
|
||||
for (int i = 0; i < countdown; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok.countdown", countdown - i)));
|
||||
}
|
||||
keyFrames[countdown] = new KeyFrame(Duration.seconds(countdown),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(btnYes.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
Controllers.confirmAction(
|
||||
i18n("account.methods.offline.name.invalid"), i18n("message.warning"),
|
||||
if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) {
|
||||
Controllers.confirmWithCountdown(i18n("account.methods.offline.name.invalid"), i18n("message.warning"), 10,
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
btnYes,
|
||||
() -> {
|
||||
timeline.stop();
|
||||
doCreate, () -> {
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}
|
||||
);
|
||||
timeline.play();
|
||||
});
|
||||
} else {
|
||||
doCreate.run();
|
||||
}
|
||||
|
||||
@@ -39,8 +39,9 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -81,16 +82,16 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
|
||||
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
|
||||
if (e.getDragboard().hasFiles()) {
|
||||
File file = e.getDragboard().getFiles().get(0);
|
||||
if (file.getAbsolutePath().endsWith(".png"))
|
||||
Path file = e.getDragboard().getFiles().get(0).toPath();
|
||||
if (FileUtils.getName(file).endsWith(".png"))
|
||||
e.acceptTransferModes(TransferMode.COPY);
|
||||
}
|
||||
});
|
||||
canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> {
|
||||
if (e.isAccepted()) {
|
||||
File skin = e.getDragboard().getFiles().get(0);
|
||||
Path skin = e.getDragboard().getFiles().get(0).toPath();
|
||||
Platform.runLater(() -> {
|
||||
skinSelector.setValue(skin.getAbsolutePath());
|
||||
skinSelector.setValue(FileUtils.getAbsolutePath(skin));
|
||||
skinItem.setSelectedData(Skin.Type.LOCAL_FILE);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
private Duration duration;
|
||||
private Node previousNode, currentNode;
|
||||
|
||||
{
|
||||
currentNode = getChildren().stream().findFirst().orElse(null);
|
||||
public TransitionPane() {
|
||||
FXUtils.setOverflowHidden(this);
|
||||
}
|
||||
|
||||
@@ -91,8 +90,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
private void updateContent(Node newView) {
|
||||
if (getWidth() > 0 && getHeight() > 0) {
|
||||
previousNode = currentNode;
|
||||
if (previousNode == null)
|
||||
previousNode = EMPTY_PANE;
|
||||
if (previousNode == null) {
|
||||
if (getChildren().isEmpty())
|
||||
previousNode = EMPTY_PANE;
|
||||
else
|
||||
previousNode = getChildren().get(0);
|
||||
}
|
||||
} else
|
||||
previousNode = EMPTY_PANE;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -40,8 +41,16 @@ public class AdvancedListBox extends ScrollPane {
|
||||
setFitToHeight(true);
|
||||
setFitToWidth(true);
|
||||
setHbarPolicy(ScrollBarPolicy.NEVER);
|
||||
setVbarPolicy(ScrollBarPolicy.NEVER);
|
||||
|
||||
container.getStyleClass().add("advanced-list-box-content");
|
||||
|
||||
this.addEventFilter(MouseEvent.MOUSE_ENTERED, event -> {
|
||||
if (container.getHeight() > getHeight())
|
||||
setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
|
||||
});
|
||||
this.addEventFilter(MouseEvent.MOUSE_EXITED,
|
||||
event -> setVbarPolicy(ScrollBarPolicy.NEVER));
|
||||
}
|
||||
|
||||
public AdvancedListBox add(Node child) {
|
||||
|
||||
@@ -73,8 +73,7 @@ final class ComponentListCell extends StackPane {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void updateLayout() {
|
||||
if (content instanceof ComponentList) {
|
||||
ComponentList list = (ComponentList) content;
|
||||
if (content instanceof ComponentList list) {
|
||||
content.getStyleClass().remove("options-list");
|
||||
content.getStyleClass().add("options-sublist");
|
||||
|
||||
@@ -130,7 +129,10 @@ final class ComponentListCell extends StackPane {
|
||||
groupNode.getChildren().add(headerRippler);
|
||||
|
||||
VBox container = new VBox();
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
boolean hasPadding = !(list instanceof ComponentSublist subList) || subList.hasComponentPadding();
|
||||
if (hasPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(content);
|
||||
@@ -149,7 +151,8 @@ final class ComponentListCell extends StackPane {
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + 8 + 10) * (expanded ? 1 : -1);
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double newAnimatedHeight = (list.prefHeight(list.getWidth()) + (hasPadding ? 8 + 10 : 4)) * (expanded ? 1 : -1);
|
||||
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(list.getWidth());
|
||||
double contentHeight = expanded ? newAnimatedHeight : 0;
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
|
||||
@@ -27,6 +29,7 @@ public class ComponentSublist extends ComponentList {
|
||||
|
||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
||||
private final BooleanProperty componentPadding = new SimpleBooleanProperty(this, "componentPadding", true);
|
||||
|
||||
public ComponentSublist() {
|
||||
super();
|
||||
@@ -55,4 +58,16 @@ public class ComponentSublist extends ComponentList {
|
||||
public void setHeaderRight(Node headerRight) {
|
||||
this.headerRight.set(headerRight);
|
||||
}
|
||||
|
||||
public boolean hasComponentPadding() {
|
||||
return componentPadding.get();
|
||||
}
|
||||
|
||||
public BooleanProperty componentPaddingProperty() {
|
||||
return componentPadding;
|
||||
}
|
||||
|
||||
public void setComponentPadding(boolean componentPadding) {
|
||||
this.componentPadding.set(componentPadding);
|
||||
}
|
||||
}
|
||||
@@ -27,17 +27,19 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class FileItem extends BorderPane {
|
||||
private final Label lblPath = new Label();
|
||||
@@ -77,14 +79,14 @@ public class FileItem extends BorderPane {
|
||||
private String processPath(String path) {
|
||||
Path given;
|
||||
try {
|
||||
given = Paths.get(path).toAbsolutePath();
|
||||
given = Path.of(path).toAbsolutePath().normalize();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (isConvertToRelativePath()) {
|
||||
try {
|
||||
return Paths.get(".").normalize().toAbsolutePath().relativize(given).normalize().toString();
|
||||
return Metadata.CURRENT_DIRECTORY.relativize(given).normalize().toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
// the given path can't be relativized against current path
|
||||
}
|
||||
@@ -95,17 +97,22 @@ public class FileItem extends BorderPane {
|
||||
public void onExplore() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
if (path.get() != null) {
|
||||
File file = new File(path.get());
|
||||
if (file.exists()) {
|
||||
if (file.isFile())
|
||||
file = file.getAbsoluteFile().getParentFile();
|
||||
else if (file.isDirectory())
|
||||
file = file.getAbsoluteFile();
|
||||
chooser.setInitialDirectory(file);
|
||||
Path file;
|
||||
try {
|
||||
file = Path.of(path.get());
|
||||
if (Files.exists(file)) {
|
||||
if (Files.isRegularFile(file))
|
||||
file = file.toAbsolutePath().normalize().getParent();
|
||||
else if (Files.isDirectory(file))
|
||||
file = file.toAbsolutePath().normalize();
|
||||
chooser.setInitialDirectory(file.toFile());
|
||||
}
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to resolve path: " + path.get());
|
||||
}
|
||||
}
|
||||
chooser.titleProperty().bind(titleProperty());
|
||||
File selectedDir = chooser.showDialog(Controllers.getStage());
|
||||
var selectedDir = chooser.showDialog(Controllers.getStage());
|
||||
if (selectedDir != null) {
|
||||
path.set(processPath(selectedDir.toString()));
|
||||
}
|
||||
|
||||
@@ -31,8 +31,9 @@ import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -86,9 +87,9 @@ public class FileSelector extends HBox {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
File dir = chooser.showDialog(Controllers.getStage());
|
||||
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
|
||||
if (dir != null) {
|
||||
String path = dir.getAbsolutePath();
|
||||
String path = FileUtils.getAbsolutePath(dir);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
@@ -96,9 +97,9 @@ public class FileSelector extends HBox {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(chooserTitle);
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
String path = file.getAbsolutePath();
|
||||
String path = FileUtils.getAbsolutePath(file);
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -21,7 +22,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class IconedTwoLineListItem extends HBox {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<String> tags = FXCollections.observableArrayList();
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
|
||||
@@ -62,7 +63,7 @@ public class IconedTwoLineListItem extends HBox {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public ObservableList<String> getTags() {
|
||||
public ObservableList<Label> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ public final class MessageDialogPane extends HBox {
|
||||
content.getChildren().setAll(textFlow);
|
||||
else {
|
||||
ScrollPane scrollPane = new ScrollPane(textFlow);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
scrollPane.setPrefHeight(350);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
@@ -24,11 +24,13 @@ import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -297,4 +299,35 @@ public final class MultiFileItem<T> extends VBox {
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PaintOption<T> extends Option<T> {
|
||||
private final ColorPicker colorPicker = new ColorPicker();
|
||||
|
||||
public PaintOption(String title, T data) {
|
||||
super(title, data);
|
||||
}
|
||||
|
||||
public PaintOption<T> bindBidirectional(Property<Paint> property) {
|
||||
FXUtils.bindPaint(colorPicker, property);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createItem(ToggleGroup group) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(3));
|
||||
FXUtils.setLimitHeight(pane, 30);
|
||||
|
||||
left.setText(title);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(data);
|
||||
pane.setLeft(left);
|
||||
|
||||
colorPicker.disableProperty().bind(left.selectedProperty().not());
|
||||
BorderPane.setAlignment(colorPicker, Pos.CENTER_RIGHT);
|
||||
pane.setRight(colorPicker);
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -61,7 +63,12 @@ public class OptionToggleButton extends StackPane {
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
|
||||
container.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
toggleButton.setSelected(!toggleButton.isSelected());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
|
||||
@@ -191,7 +191,7 @@ public class RipplerContainer extends StackPane {
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
}
|
||||
|
||||
private static class StyleableProperties {
|
||||
private final static class StyleableProperties {
|
||||
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
|
||||
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255));
|
||||
|
||||
@@ -225,7 +225,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
}
|
||||
}
|
||||
|
||||
private class HeadersRegion extends StackPane {
|
||||
private final class HeadersRegion extends StackPane {
|
||||
private SideAction action;
|
||||
private final ObjectProperty<Side> side = new SimpleObjectProperty<Side>() {
|
||||
@Override
|
||||
@@ -379,7 +379,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
}
|
||||
}
|
||||
|
||||
private class Top extends Horizontal {
|
||||
private final class Top extends Horizontal {
|
||||
@Override
|
||||
public void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
@@ -404,7 +404,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
}
|
||||
}
|
||||
|
||||
private class Bottom extends Horizontal {
|
||||
private final class Bottom extends Horizontal {
|
||||
@Override
|
||||
public void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
@@ -519,7 +519,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
}
|
||||
}
|
||||
|
||||
private class Left extends Vertical {
|
||||
private final class Left extends Vertical {
|
||||
@Override
|
||||
public void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
@@ -543,7 +543,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
}
|
||||
}
|
||||
|
||||
private class Right extends Vertical {
|
||||
private final class Right extends Vertical {
|
||||
@Override
|
||||
public void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
|
||||
@@ -22,13 +22,10 @@ import javafx.application.Platform;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -43,7 +40,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class TaskExecutorDialogPane extends BorderPane {
|
||||
private TaskExecutor executor;
|
||||
private TaskCancellationAction onCancel;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
private final Label lblTitle;
|
||||
private final Label lblProgress;
|
||||
@@ -61,16 +59,10 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
lblTitle = new Label();
|
||||
lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
{
|
||||
taskListPane = new TaskListPane();
|
||||
scrollPane.setContent(taskListPane);
|
||||
}
|
||||
taskListPane = new TaskListPane();
|
||||
VBox.setVgrow(taskListPane, Priority.ALWAYS);
|
||||
|
||||
center.getChildren().setAll(lblTitle, scrollPane);
|
||||
center.getChildren().setAll(lblTitle, taskListPane);
|
||||
}
|
||||
|
||||
BorderPane bottom = new BorderPane();
|
||||
@@ -93,7 +85,7 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
}
|
||||
});
|
||||
|
||||
speedEventHandler = speedEvent -> {
|
||||
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
|
||||
String unit = "B/s";
|
||||
double speed = speedEvent.getSpeed();
|
||||
if (speed > 1024) {
|
||||
@@ -104,11 +96,10 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
speed /= 1024;
|
||||
unit = "MiB/s";
|
||||
}
|
||||
double finalSpeed = speed;
|
||||
String finalUnit = unit;
|
||||
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
|
||||
};
|
||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
|
||||
String message = String.format("%.1f %s", speed, unit);
|
||||
Platform.runLater(() -> lblProgress.setText(message));
|
||||
});
|
||||
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
}
|
||||
|
||||
@@ -17,17 +17,23 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
|
||||
@@ -52,6 +58,7 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthInstallTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
@@ -63,54 +70,86 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
|
||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
|
||||
|
||||
private TaskExecutor executor;
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXListView<Node> listView = new JFXListView<>();
|
||||
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
|
||||
private final List<StageNode> stageNodes = new ArrayList<>();
|
||||
private final Map<String, StageNode> stageNodes = new HashMap<>();
|
||||
private final ObjectProperty<Insets> progressNodePadding = new SimpleObjectProperty<>(Insets.EMPTY);
|
||||
private final DoubleProperty cellWidth = new SimpleDoubleProperty();
|
||||
|
||||
private Cell lastCell;
|
||||
|
||||
public TaskListPane() {
|
||||
listBox.setSpacing(0);
|
||||
listView.setPadding(new Insets(12, 0, 0, 0));
|
||||
listView.setCellFactory(l -> new Cell());
|
||||
listView.setSelectionModel(null);
|
||||
FXUtils.onChangeAndOperate(listView.widthProperty(), width -> {
|
||||
double w = width.doubleValue();
|
||||
cellWidth.set(w <= 12.0 ? w : w - 12.0);
|
||||
});
|
||||
|
||||
getChildren().setAll(listBox);
|
||||
getChildren().setAll(listView);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private void addStages(@NotNull Collection<String> stages) {
|
||||
for (String stage : stages) {
|
||||
stageNodes.computeIfAbsent(stage, s -> {
|
||||
StageNode node = new StageNode(stage);
|
||||
listView.getItems().add(node);
|
||||
return node;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private void updateProgressNodePadding() {
|
||||
progressNodePadding.set(stageNodes.isEmpty() ? DEFAULT_PROGRESS_NODE_PADDING : STAGED_PROGRESS_NODE_PADDING);
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
List<String> stages = Lang.removingDuplicates(executor.getStages());
|
||||
this.executor = executor;
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStart() {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.clear();
|
||||
listBox.clear();
|
||||
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
|
||||
stageNodes.forEach(listBox::add);
|
||||
|
||||
if (stages.isEmpty()) progressNodePadding.setValue(new Insets(0, 0, 8, 0));
|
||||
else progressNodePadding.setValue(new Insets(0, 0, 8, 26));
|
||||
listView.getItems().clear();
|
||||
addStages(executor.getStages());
|
||||
updateProgressNodePadding();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady(Task<?> task) {
|
||||
if (task instanceof Task.StagesHintTask) {
|
||||
Platform.runLater(() -> {
|
||||
addStages(((Task<?>.StagesHintTask) task).getStages());
|
||||
updateProgressNodePadding();
|
||||
});
|
||||
}
|
||||
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
|
||||
StageNode node = stageNodes.get(task.getStage());
|
||||
if (node != null)
|
||||
node.begin();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -123,7 +162,11 @@ public final class TaskListPane extends StackPane {
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
task.setName(i18n("assets.download_all"));
|
||||
} else if (task instanceof GameInstallTask) {
|
||||
if (task.getInheritedStage() != null && task.getInheritedStage().startsWith("hmcl.install.game"))
|
||||
return;
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
|
||||
} else if (task instanceof CleanroomInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.cleanroom")));
|
||||
} 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) {
|
||||
@@ -156,7 +199,7 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
@@ -169,25 +212,25 @@ public final class TaskListPane extends StackPane {
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
|
||||
listBox.add(listBox.indexOf(stageNode) + 1, node);
|
||||
StageNode stageNode = stageNodes.get(task.getInheritedStage());
|
||||
listView.getItems().add(listView.getItems().indexOf(stageNode) + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task<?> task) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
|
||||
});
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (task.getStage() != null) {
|
||||
StageNode stageNode = stageNodes.get(task.getStage());
|
||||
if (stageNode != null)
|
||||
stageNode.succeed();
|
||||
}
|
||||
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
listBox.remove(node);
|
||||
if (node != null) {
|
||||
node.unbind();
|
||||
listView.getItems().remove(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,25 +238,23 @@ public final class TaskListPane extends StackPane {
|
||||
public void onFailed(Task<?> task, Throwable throwable) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
|
||||
StageNode stageNode = stageNodes.get(task.getStage());
|
||||
if (stageNode != null)
|
||||
stageNode.fail();
|
||||
});
|
||||
}
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
Platform.runLater(() -> {
|
||||
node.setThrowable(throwable);
|
||||
});
|
||||
if (node != null)
|
||||
Platform.runLater(() -> node.setThrowable(throwable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPropertiesUpdate(Task<?> task) {
|
||||
if (task instanceof Task.CountTask) {
|
||||
runInFX(() -> {
|
||||
stageNodes.stream()
|
||||
.filter(x -> x.stage.equals(((Task<?>.CountTask) task).getCountStage()))
|
||||
.findAny()
|
||||
.ifPresent(StageNode::count);
|
||||
StageNode stageNode = stageNodes.get(((Task<?>.CountTask) task).getCountStage());
|
||||
if (stageNode != null)
|
||||
stageNode.count();
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -222,121 +263,252 @@ public final class TaskListPane extends StackPane {
|
||||
if (task.getStage() != null) {
|
||||
int total = tryCast(task.getProperties().get("total"), Integer.class).orElse(0);
|
||||
runInFX(() -> {
|
||||
stageNodes.stream()
|
||||
.filter(x -> x.stage.equals(task.getStage()))
|
||||
.findAny()
|
||||
.ifPresent(stageNode -> {
|
||||
stageNode.setTotal(total);
|
||||
});
|
||||
StageNode stageNode = stageNodes.get(task.getStage());
|
||||
if (stageNode != null)
|
||||
stageNode.addTotal(total);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class StageNode extends BorderPane {
|
||||
private final String stage;
|
||||
private final class Cell extends ListCell<Node> {
|
||||
private static final double STATUS_ICON_SIZE = 14;
|
||||
|
||||
private final BorderPane pane = new BorderPane();
|
||||
private final StackPane left = new StackPane();
|
||||
private final Label title = new Label();
|
||||
private final Label message = new Label();
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
|
||||
private WeakReference<StageNode> prevStageNodeRef;
|
||||
private StatusChangeListener statusChangeListener;
|
||||
|
||||
private Cell() {
|
||||
setPadding(new Insets(0, 0, 4, 0));
|
||||
|
||||
prefWidthProperty().bind(cellWidth);
|
||||
|
||||
FXUtils.setLimitHeight(left, STATUS_ICON_SIZE);
|
||||
FXUtils.setLimitWidth(left, STATUS_ICON_SIZE);
|
||||
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(left, new Insets(0, 12, 0, 0));
|
||||
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
||||
pane.setCenter(title);
|
||||
|
||||
DoubleBinding barWidth = Bindings.createDoubleBinding(() -> {
|
||||
Insets padding = pane.getPadding();
|
||||
Insets insets = pane.getInsets();
|
||||
return pane.getWidth() - padding.getLeft() - padding.getRight() - insets.getLeft() - insets.getRight();
|
||||
}, pane.paddingProperty(), pane.widthProperty());
|
||||
bar.minWidthProperty().bind(barWidth);
|
||||
bar.prefWidthProperty().bind(barWidth);
|
||||
bar.maxWidthProperty().bind(barWidth);
|
||||
|
||||
setGraphic(pane);
|
||||
}
|
||||
|
||||
private void updateLeftIcon(StageNode.Status status) {
|
||||
left.getChildren().setAll(status.svg.createIcon(Theme.blackFill(), STATUS_ICON_SIZE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Node item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell && !isVisible())
|
||||
return;
|
||||
lastCell = this;
|
||||
|
||||
pane.paddingProperty().unbind();
|
||||
title.textProperty().unbind();
|
||||
message.textProperty().unbind();
|
||||
bar.progressProperty().unbind();
|
||||
|
||||
StageNode prevStageNode;
|
||||
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||
prevStageNode.status.removeListener(statusChangeListener);
|
||||
|
||||
if (item instanceof ProgressListNode) {
|
||||
var progressListNode = (ProgressListNode) item;
|
||||
title.setText(progressListNode.title);
|
||||
message.textProperty().bind(progressListNode.message);
|
||||
bar.progressProperty().bind(progressListNode.progress);
|
||||
|
||||
pane.paddingProperty().bind(progressNodePadding);
|
||||
pane.setLeft(null);
|
||||
pane.setRight(message);
|
||||
pane.setBottom(bar);
|
||||
} else if (item instanceof StageNode) {
|
||||
var stageNode = (StageNode) item;
|
||||
title.textProperty().bind(stageNode.title);
|
||||
message.setText("");
|
||||
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
|
||||
pane.setPadding(Insets.EMPTY);
|
||||
pane.setLeft(left);
|
||||
pane.setRight(message);
|
||||
pane.setBottom(null);
|
||||
|
||||
updateLeftIcon(stageNode.status.get());
|
||||
if (statusChangeListener == null)
|
||||
statusChangeListener = new StatusChangeListener(this);
|
||||
stageNode.status.addListener(statusChangeListener);
|
||||
prevStageNodeRef = new WeakReference<>(stageNode);
|
||||
} else { // item == null
|
||||
title.setText("");
|
||||
message.setText("");
|
||||
bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
|
||||
pane.setPadding(Insets.EMPTY);
|
||||
pane.setLeft(null);
|
||||
pane.setRight(null);
|
||||
pane.setBottom(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StatusChangeListener implements ChangeListener<StageNode.Status>, WeakListener {
|
||||
|
||||
private final WeakReference<Cell> cellRef;
|
||||
|
||||
private StatusChangeListener(Cell cell) {
|
||||
this.cellRef = new WeakReference<>(cell);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return cellRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends StageNode.Status> observable,
|
||||
StageNode.Status oldValue,
|
||||
StageNode.Status newValue) {
|
||||
Cell cell = cellRef.get();
|
||||
if (cell == null) {
|
||||
if (observable != null)
|
||||
observable.removeListener(this);
|
||||
return;
|
||||
}
|
||||
cell.updateLeftIcon(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Node {
|
||||
|
||||
}
|
||||
|
||||
private static final class StageNode extends Node {
|
||||
private enum Status {
|
||||
WAITING(SVG.MORE_HORIZ),
|
||||
RUNNING(SVG.ARROW_FORWARD),
|
||||
SUCCESS(SVG.CHECK),
|
||||
FAILED(SVG.CLOSE);
|
||||
|
||||
private final SVG svg;
|
||||
|
||||
Status(SVG svg) {
|
||||
this.svg = svg;
|
||||
}
|
||||
}
|
||||
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.WAITING);
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final String message;
|
||||
private int count = 0;
|
||||
private int total = 0;
|
||||
private boolean started = false;
|
||||
|
||||
public StageNode(String stage) {
|
||||
this.stage = stage;
|
||||
private StageNode(String stage) {
|
||||
String stageKey;
|
||||
String stageValue;
|
||||
|
||||
String stageKey = StringUtils.substringBefore(stage, ':');
|
||||
String stageValue = StringUtils.substringAfter(stage, ':');
|
||||
int idx = stage.indexOf(':');
|
||||
if (idx >= 0) {
|
||||
stageKey = stage.substring(0, idx);
|
||||
stageValue = stage.substring(idx + 1);
|
||||
} else {
|
||||
stageKey = stage;
|
||||
stageValue = null;
|
||||
}
|
||||
|
||||
// CHECKSTYLE:OFF
|
||||
// @formatter:off
|
||||
switch (stageKey) {
|
||||
case "hmcl.modpack": message = i18n("install.modpack"); break;
|
||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||
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.modpack": message = i18n("install.modpack"); break;
|
||||
case "hmcl.modpack.download": message = i18n("launch.state.modpack"); break;
|
||||
case "hmcl.install.assets": message = i18n("assets.download"); break;
|
||||
case "hmcl.install.libraries": message = i18n("libraries.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.cleanroom": message = i18n("install.installer.install", i18n("install.installer.cleanroom") + " " + 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;
|
||||
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;
|
||||
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
|
||||
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
||||
default: message = i18n(stageKey); break;
|
||||
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
|
||||
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
|
||||
default: message = i18n(stageKey); break;
|
||||
}
|
||||
// @formatter:on
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
title.setText(message);
|
||||
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
|
||||
setPadding(new Insets(0, 0, 8, 4));
|
||||
setCenter(title);
|
||||
setLeft(FXUtils.limitingSize(SVG.MORE_HORIZ.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
title.set(message);
|
||||
}
|
||||
|
||||
public void begin() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
setLeft(FXUtils.limitingSize(SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
private void begin() {
|
||||
if (status.get() == Status.WAITING) {
|
||||
status.set(Status.RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
public void fail() {
|
||||
setLeft(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
status.set(Status.FAILED);
|
||||
}
|
||||
|
||||
public void succeed() {
|
||||
setLeft(FXUtils.limitingSize(SVG.CHECK.createIcon(Theme.blackFill(), 14), 14, 14));
|
||||
status.set(Status.SUCCESS);
|
||||
}
|
||||
|
||||
public void count() {
|
||||
updateCounter(++count, total);
|
||||
}
|
||||
|
||||
public void setTotal(int total) {
|
||||
this.total = total;
|
||||
public void addTotal(int n) {
|
||||
this.total += n;
|
||||
updateCounter(count, total);
|
||||
}
|
||||
|
||||
public void updateCounter(int count, int total) {
|
||||
if (total > 0)
|
||||
title.setText(String.format("%s - %d/%d", message, count, total));
|
||||
else
|
||||
title.setText(message);
|
||||
title.setValue(total > 0
|
||||
? message + " - " + count + "/" + total
|
||||
: message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressListNode extends BorderPane {
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
private final DoubleBinding binding = Bindings.createDoubleBinding(() ->
|
||||
getWidth() - getPadding().getLeft() - getPadding().getRight() - getInsets().getLeft() - getInsets().getRight(),
|
||||
paddingProperty(), widthProperty());
|
||||
private static final class ProgressListNode extends Node {
|
||||
private final String title;
|
||||
private final StringProperty message = new SimpleStringProperty("");
|
||||
private final DoubleProperty progress = new SimpleDoubleProperty(0.0);
|
||||
|
||||
public ProgressListNode(Task<?> task) {
|
||||
bar.progressProperty().bind(task.progressProperty());
|
||||
title.setText(task.getName());
|
||||
state.textProperty().bind(task.messageProperty());
|
||||
|
||||
setLeft(title);
|
||||
setRight(state);
|
||||
setBottom(bar);
|
||||
|
||||
bar.minWidthProperty().bind(binding);
|
||||
bar.prefWidthProperty().bind(binding);
|
||||
bar.maxWidthProperty().bind(binding);
|
||||
|
||||
paddingProperty().bind(progressNodePadding);
|
||||
private ProgressListNode(Task<?> task) {
|
||||
this.title = task.getName();
|
||||
message.bind(task.messageProperty());
|
||||
progress.bind(task.progressProperty());
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
message.unbind();
|
||||
progress.unbind();
|
||||
}
|
||||
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
message.set(throwable.getLocalizedMessage());
|
||||
progress.set(0.);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,16 +29,21 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
private static Label createTagLabel(String tag) {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<String> tags = FXCollections.observableArrayList();
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final ObservableList<Node> tagLabels;
|
||||
private final AggregatedObservableList<Node> firstLineChildren;
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
@@ -58,16 +63,9 @@ public class TwoLineListItem extends VBox {
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
tagLabels = MappedObservableList.create(tags, tag -> {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
});
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
|
||||
firstLineChildren.appendList(tagLabels);
|
||||
firstLineChildren.appendList(tags);
|
||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||
|
||||
Label lblSubtitle = new Label();
|
||||
@@ -111,7 +109,19 @@ public class TwoLineListItem extends VBox {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public ObservableList<String> getTags() {
|
||||
public void addTag(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag-warning");
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@ import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -58,9 +55,9 @@ public class URLValidator extends ValidatorBase {
|
||||
hasErrors.set(!nullable);
|
||||
else {
|
||||
try {
|
||||
new URL(textField.getText()).toURI();
|
||||
NetworkUtils.toURI(textField.getText());
|
||||
hasErrors.set(false);
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
hasErrors.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,13 @@ import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
@@ -44,6 +45,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
@@ -55,26 +57,23 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DecoratorController {
|
||||
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
|
||||
@@ -124,18 +123,13 @@ public class DecoratorController {
|
||||
|
||||
// Setup background
|
||||
decorator.setContentBackground(getBackground());
|
||||
changeBackgroundListener = o -> {
|
||||
final int currentCount = ++this.changeBackgroundCount;
|
||||
CompletableFuture.supplyAsync(this::getBackground, Schedulers.io())
|
||||
.thenAcceptAsync(background -> {
|
||||
if (this.changeBackgroundCount == currentCount)
|
||||
decorator.setContentBackground(background);
|
||||
}, Schedulers.javafx());
|
||||
};
|
||||
changeBackgroundListener = o -> updateBackground();
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener);
|
||||
config().backgroundImageTypeProperty().addListener(weakListener);
|
||||
config().backgroundImageProperty().addListener(weakListener);
|
||||
config().backgroundImageUrlProperty().addListener(weakListener);
|
||||
config().backgroundPaintProperty().addListener(weakListener);
|
||||
config().backgroundImageOpacityProperty().addListener(weakListener);
|
||||
|
||||
// pass key events to current dialog / current page
|
||||
decorator.addEventFilter(KeyEvent.ANY, e -> {
|
||||
@@ -171,17 +165,23 @@ public class DecoratorController {
|
||||
// press ESC to go back
|
||||
onEscPressed(navigator, this::back);
|
||||
|
||||
try {
|
||||
// For JavaFX 12+
|
||||
MouseButton button = MouseButton.valueOf("BACK");
|
||||
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == button) {
|
||||
back();
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
// press F11 to toggle full screen
|
||||
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.BACK) {
|
||||
back();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Decorator getDecorator() {
|
||||
@@ -196,8 +196,24 @@ public class DecoratorController {
|
||||
@SuppressWarnings("FieldCanBeLocal") // Strong reference
|
||||
private final InvalidationListener changeBackgroundListener;
|
||||
|
||||
private void updateBackground() {
|
||||
final int currentCount = ++this.changeBackgroundCount;
|
||||
Task.supplyAsync(Schedulers.io(), this::getBackground)
|
||||
.setName("Update background")
|
||||
.whenComplete(Schedulers.javafx(), (background, exception) -> {
|
||||
if (exception == null) {
|
||||
if (this.changeBackgroundCount == currentCount)
|
||||
decorator.setContentBackground(background);
|
||||
} else {
|
||||
LOG.warning("Failed to update background", exception);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private Background getBackground() {
|
||||
EnumBackgroundImage imageType = config().getBackgroundImageType();
|
||||
if (imageType == null)
|
||||
imageType = EnumBackgroundImage.DEFAULT;
|
||||
|
||||
Image image = null;
|
||||
switch (imageType) {
|
||||
@@ -214,7 +230,7 @@ public class DecoratorController {
|
||||
String backgroundImageUrl = config().getBackgroundImageUrl();
|
||||
if (backgroundImageUrl != null) {
|
||||
try {
|
||||
image = FXUtils.loadImage(new URL(backgroundImageUrl));
|
||||
image = FXUtils.loadImage(backgroundImageUrl);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Couldn't load background image", e);
|
||||
}
|
||||
@@ -223,13 +239,60 @@ public class DecoratorController {
|
||||
case CLASSIC:
|
||||
image = newBuiltinImage("/assets/img/background-classic.jpg");
|
||||
break;
|
||||
case TRANSLUCENT:
|
||||
case TRANSLUCENT: // Deprecated
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
case PAINT:
|
||||
Paint paint = config().getBackgroundPaint();
|
||||
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
|
||||
if (paint instanceof Color || paint == null) {
|
||||
Color color = (Color) paint;
|
||||
if (color == null)
|
||||
color = Color.WHITE; // Default to white if no color is set
|
||||
if (opacity < 1.)
|
||||
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), opacity);
|
||||
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else {
|
||||
// TODO: Support opacity for non-color paints
|
||||
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
}
|
||||
if (image == null) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
}
|
||||
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
|
||||
return createBackgroundWithOpacity(image, config().getBackgroundImageOpacity());
|
||||
}
|
||||
|
||||
private Background createBackgroundWithOpacity(Image image, int opacity) {
|
||||
if (opacity <= 0) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else if (opacity >= 100) {
|
||||
return new Background(new BackgroundImage(
|
||||
image,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
} else {
|
||||
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
|
||||
PixelReader pixelReader = image.getPixelReader();
|
||||
PixelWriter pixelWriter = tempImage.getPixelWriter();
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
Color color = pixelReader.getColor(x, y);
|
||||
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity / 100);
|
||||
pixelWriter.setColor(x, y, newColor);
|
||||
}
|
||||
}
|
||||
|
||||
return new Background(new BackgroundImage(
|
||||
tempImage,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -29,6 +32,7 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -43,6 +47,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane root, parent;
|
||||
@@ -50,6 +55,10 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final InvalidationListener onWindowsStatusChange;
|
||||
private final EventHandler<MouseEvent> onTitleBarDoubleClick;
|
||||
|
||||
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
|
||||
|
||||
/**
|
||||
@@ -80,9 +89,41 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
skinnable.getSnackbar().registerSnackbarContainer(parent);
|
||||
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
|
||||
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
|
||||
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
|
||||
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4290
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
onWindowsStatusChange = observable -> {
|
||||
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
|
||||
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
} else {
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
};
|
||||
onTitleBarDoubleClick = event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
primaryStage.setMaximized(!primaryStage.isMaximized());
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
|
||||
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
|
||||
onWindowsStatusChange.invalidated(null);
|
||||
} else {
|
||||
onWindowsStatusChange = null;
|
||||
onTitleBarDoubleClick = null;
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
|
||||
shadowContainer.getChildren().setAll(parent);
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
@@ -282,6 +323,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
BorderPane.setAlignment(titleNode, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(titleNode, new Insets(0, 0, 0, 8));
|
||||
}
|
||||
if (onTitleBarDoubleClick != null)
|
||||
center.setOnMouseClicked(onTitleBarDoubleClick);
|
||||
navBar.setCenter(center);
|
||||
|
||||
if (canRefresh) {
|
||||
|
||||
@@ -23,8 +23,8 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.wizard.*;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
|
||||
@@ -94,7 +94,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTask(Map<String, Object> settings, Task<?> task) {
|
||||
public void handleTask(SettingsMap settings, Task<?> task) {
|
||||
displayer.handleTask(settings, task);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
|
||||
import java.util.Map;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -80,12 +79,12 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
protected abstract void reload();
|
||||
|
||||
@Override
|
||||
public void onNavigate(Map<String, Object> settings) {
|
||||
public void onNavigate(SettingsMap settings) {
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract void cleanup(Map<String, Object> settings);
|
||||
public abstract void cleanup(SettingsMap settings);
|
||||
|
||||
protected abstract void onInstall();
|
||||
|
||||
@@ -120,20 +119,15 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
{
|
||||
InstallerItem[] libraries = control.group.getLibraries();
|
||||
|
||||
FlowPane libraryPane = new FlowPane(libraries);
|
||||
libraryPane.setVgap(16);
|
||||
libraryPane.setHgap(16);
|
||||
FlowPane libraryPane = new FlowPane(16, 16, libraries);
|
||||
ScrollPane scrollPane = new ScrollPane(libraryPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(scrollPane);
|
||||
|
||||
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);
|
||||
}
|
||||
if (libraries.length <= 8)
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@ import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
@@ -109,6 +109,6 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
}
|
||||
@@ -51,15 +51,13 @@ import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -146,17 +144,17 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
|
||||
if (!OperatingSystem.isNameValid(result)) {
|
||||
if (!FileUtils.isNameValid(result)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
}
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
|
||||
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
|
||||
task.setName(file.getName());
|
||||
return task;
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
@@ -224,7 +222,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private static final class DownloadNavigator implements Navigation {
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
private final SettingsMap settings = new SettingsMap();
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
@@ -261,7 +259,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getSettings() {
|
||||
public SettingsMap getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -288,37 +286,39 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
settings.put(PROFILE, profile);
|
||||
public void start(SettingsMap settings) {
|
||||
settings.put(ModpackPage.PROFILE, profile);
|
||||
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion);
|
||||
}
|
||||
|
||||
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
|
||||
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameBuilder builder = dependencyManager.gameBuilder();
|
||||
|
||||
String name = (String) settings.get("name");
|
||||
builder.name(name);
|
||||
builder.gameVersion(((RemoteVersion) settings.get(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())).getGameVersion());
|
||||
|
||||
for (Map.Entry<String, Object> entry : settings.entrySet())
|
||||
if (!LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(entry.getKey()) && entry.getValue() instanceof RemoteVersion)
|
||||
builder.version((RemoteVersion) entry.getValue());
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
if (!LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(key)
|
||||
&& value instanceof RemoteVersion remoteVersion)
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
|
||||
return builder.buildAsync().whenComplete(any -> profile.getRepository().refreshVersions())
|
||||
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.new_game.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
|
||||
return finishVersionDownloadingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
|
||||
@@ -331,7 +331,5 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final String PROFILE = "PROFILE";
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,7 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -74,14 +72,27 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
|
||||
private static boolean checkName(String name) {
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char c = name.charAt(i);
|
||||
if (!(c >= '0' && c <= '9')
|
||||
&& !(c >= 'a' && c <= 'z')
|
||||
&& !(c >= 'A' && c <= 'Z')
|
||||
&& c != '-' && c != '_' && c != '.'
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void onInstall() {
|
||||
String name = txtName.getText();
|
||||
|
||||
// Check for non-ASCII characters.
|
||||
if (!StringUtils.isASCII(name)) {
|
||||
if (!checkName(name)) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("install.name.invalid"),
|
||||
i18n("message.warning"),
|
||||
@@ -118,6 +129,9 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
case NEO_FORGE:
|
||||
loaderName = i18n("install.installer.neoforge");
|
||||
break;
|
||||
case CLEANROOM:
|
||||
loaderName = i18n("install.installer.cleanroom");
|
||||
break;
|
||||
case FABRIC:
|
||||
loaderName = i18n("install.installer.fabric");
|
||||
break;
|
||||
|
||||
@@ -36,16 +36,14 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -58,11 +56,11 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
public LocalModpackPage(WizardController controller) {
|
||||
super(controller);
|
||||
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
|
||||
|
||||
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
String name = controller.getSettings().get(MODPACK_NAME);
|
||||
if (name != null) {
|
||||
txtModpackName.setText(name);
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
FXUtils.onChangeAndOperate(installAsVersion, installAsVersion -> {
|
||||
@@ -82,15 +80,15 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
|
||||
btnDescription.setVisible(false);
|
||||
|
||||
File selectedFile;
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
selectedFile = filePath.get();
|
||||
Path selectedFile;
|
||||
Path filePath = controller.getSettings().get(MODPACK_FILE);
|
||||
if (filePath != null) {
|
||||
selectedFile = filePath;
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
controller.onEnd();
|
||||
return;
|
||||
@@ -100,19 +98,19 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
showSpinner();
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile))
|
||||
.thenApplyAsync(encoding -> {
|
||||
charset = encoding;
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding);
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile, encoding);
|
||||
return manifest;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
hideSpinner();
|
||||
lblName.setText(selectedFile.getName());
|
||||
lblName.setText(FileUtils.getName(selectedFile));
|
||||
installAsVersion.set(false);
|
||||
|
||||
if (!name.isPresent()) {
|
||||
if (name == null) {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
|
||||
}
|
||||
@@ -133,7 +131,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
if (name == null) {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
}
|
||||
@@ -144,7 +142,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
settings.remove(MODPACK_FILE);
|
||||
}
|
||||
|
||||
@@ -177,9 +175,9 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||
}
|
||||
|
||||
public static final String MODPACK_FILE = "MODPACK_FILE";
|
||||
public static final String MODPACK_NAME = "MODPACK_NAME";
|
||||
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
|
||||
public static final String MODPACK_CHARSET = "MODPACK_CHARSET";
|
||||
public static final String MODPACK_MANUALLY_CREATED = "MODPACK_MANUALLY_CREATED";
|
||||
public static final SettingsMap.Key<Path> MODPACK_FILE = new SettingsMap.Key<>("MODPACK_FILE");
|
||||
public static final SettingsMap.Key<String> MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
|
||||
public static final SettingsMap.Key<Modpack> MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
|
||||
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
|
||||
public static final SettingsMap.Key<Boolean> MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
|
||||
}
|
||||
+27
-33
@@ -18,11 +18,11 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -32,26 +32,25 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
private final Profile profile;
|
||||
private final File file;
|
||||
private final Path file;
|
||||
private final String updateVersion;
|
||||
|
||||
public ModpackInstallWizardProvider(Profile profile) {
|
||||
this(profile, null, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(Profile profile, File modpackFile) {
|
||||
public ModpackInstallWizardProvider(Profile profile, Path modpackFile) {
|
||||
this(profile, modpackFile, null);
|
||||
}
|
||||
|
||||
@@ -59,28 +58,28 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
this(profile, null, updateVersion);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(Profile profile, File modpackFile, String updateVersion) {
|
||||
public ModpackInstallWizardProvider(Profile profile, Path modpackFile, String updateVersion) {
|
||||
this.profile = profile;
|
||||
this.file = modpackFile;
|
||||
this.updateVersion = updateVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
public void start(SettingsMap settings) {
|
||||
if (file != null)
|
||||
settings.put(LocalModpackPage.MODPACK_FILE, file);
|
||||
if (updateVersion != null)
|
||||
settings.put(LocalModpackPage.MODPACK_NAME, updateVersion);
|
||||
settings.put(PROFILE, profile);
|
||||
settings.put(ModpackPage.PROFILE, profile);
|
||||
}
|
||||
|
||||
private Task<?> finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
File selected = tryCast(settings.get(LocalModpackPage.MODPACK_FILE), File.class).orElse(null);
|
||||
ServerModpackManifest serverModpackManifest = tryCast(settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST), ServerModpackManifest.class).orElse(null);
|
||||
Modpack modpack = tryCast(settings.get(LocalModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
|
||||
String name = tryCast(settings.get(LocalModpackPage.MODPACK_NAME), String.class).orElse(null);
|
||||
Charset charset = tryCast(settings.get(LocalModpackPage.MODPACK_CHARSET), Charset.class).orElse(null);
|
||||
boolean isManuallyCreated = tryCast(settings.get(LocalModpackPage.MODPACK_MANUALLY_CREATED), Boolean.class).orElse(false);
|
||||
private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
|
||||
Path selected = settings.get(LocalModpackPage.MODPACK_FILE);
|
||||
ServerModpackManifest serverModpackManifest = settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST);
|
||||
Modpack modpack = settings.get(LocalModpackPage.MODPACK_MANIFEST);
|
||||
String name = settings.get(LocalModpackPage.MODPACK_NAME);
|
||||
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
|
||||
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
|
||||
|
||||
if (isManuallyCreated) {
|
||||
return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset);
|
||||
@@ -102,7 +101,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
} catch (MismatchedModpackTypeException e) {
|
||||
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
|
||||
Controllers.dialog(i18n("modpack.mismatched_type", e.getRequired(), e.getFound()), i18n("message.error"), MessageType.ERROR);
|
||||
} catch (IOException e) {
|
||||
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageType.ERROR);
|
||||
}
|
||||
@@ -119,21 +118,18 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.modpack.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", new FailureCallback() {
|
||||
@Override
|
||||
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
|
||||
if (exception instanceof ModpackCompletionException) {
|
||||
if (exception.getCause() instanceof FileNotFoundException) {
|
||||
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.SUCCESS, next);
|
||||
}
|
||||
settings.put(FailureCallback.KEY, (ignored, exception, next) -> {
|
||||
if (exception instanceof ModpackCompletionException) {
|
||||
if (exception.getCause() instanceof FileNotFoundException) {
|
||||
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
|
||||
} else {
|
||||
UpdateInstallerWizardProvider.alertFailureMessage(exception, next);
|
||||
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.SUCCESS, next);
|
||||
}
|
||||
} else {
|
||||
UpdateInstallerWizardProvider.alertFailureMessage(exception, next);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,7 +137,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new ModpackSelectionPage(controller);
|
||||
@@ -161,6 +157,4 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final String PROFILE = "PROFILE";
|
||||
}
|
||||
@@ -7,16 +7,20 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
public static final SettingsMap.Key<Profile> PROFILE = new SettingsMap.Key<>("PROFILE");
|
||||
|
||||
protected final WizardController controller;
|
||||
|
||||
protected final Label lblName;
|
||||
@@ -78,8 +82,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
BorderPane descriptionPane = new BorderPane();
|
||||
{
|
||||
btnDescription = new JFXButton(i18n("modpack.description"));
|
||||
btnDescription.getStyleClass().add("jfx-button-border");
|
||||
btnDescription = FXUtils.newBorderButton(i18n("modpack.description"));
|
||||
btnDescription.setOnAction(e -> onDescribe());
|
||||
descriptionPane.setLeft(btnDescription);
|
||||
|
||||
|
||||
@@ -29,30 +29,25 @@ import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_FILE;
|
||||
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_NAME;
|
||||
import static org.jackhuang.hmcl.ui.download.RemoteModpackPage.MODPACK_SERVER_MANIFEST;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
@@ -74,14 +69,14 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
createButton("repository", this::onChooseRepository)
|
||||
);
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
controller.getSettings().put(MODPACK_FILE, filePath.get());
|
||||
Path filePath = controller.getSettings().get(MODPACK_FILE);
|
||||
if (filePath != null) {
|
||||
controller.getSettings().put(MODPACK_FILE, filePath);
|
||||
Platform.runLater(controller::onNext);
|
||||
}
|
||||
|
||||
FXUtils.applyDragListener(this, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Path modpack = modpacks.get(0);
|
||||
controller.getSettings().put(MODPACK_FILE, modpack);
|
||||
controller.onNext();
|
||||
});
|
||||
@@ -115,7 +110,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (selectedFile == null) {
|
||||
Platform.runLater(controller::onEnd);
|
||||
return;
|
||||
@@ -126,10 +121,9 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
private void onChooseRemoteFile() {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
if (urlString.endsWith("server-manifest.json")) {
|
||||
if (url.endsWith("server-manifest.json")) {
|
||||
// if urlString ends with .json, we assume that the url is server-manifest.json
|
||||
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||
@@ -150,11 +144,11 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
resolve.run();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack.toFile(), null)
|
||||
new FileDownloadTask(url, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
resolve.run();
|
||||
controller.getSettings().put(MODPACK_FILE, modpack.toFile());
|
||||
controller.getSettings().put(MODPACK_FILE, modpack);
|
||||
controller.onNext();
|
||||
} else {
|
||||
reject.accept(e.getMessage());
|
||||
@@ -171,14 +165,14 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
public void onChooseRepository() {
|
||||
String modPackName = (String) controller.getSettings().get(MODPACK_NAME);
|
||||
String modPackName = controller.getSettings().get(MODPACK_NAME);
|
||||
DownloadPage downloadPage = new DownloadPage(modPackName);
|
||||
downloadPage.showModpackDownloads();
|
||||
Controllers.navigate(downloadPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -27,13 +28,11 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class RemoteModpackPage extends ModpackPage {
|
||||
@@ -42,8 +41,9 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
public RemoteModpackPage(WizardController controller) {
|
||||
super(controller);
|
||||
|
||||
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
|
||||
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
|
||||
manifest = controller.getSettings().get(MODPACK_SERVER_MANIFEST);
|
||||
if (manifest == null)
|
||||
throw new IllegalStateException("MODPACK_SERVER_MANIFEST should exist");
|
||||
|
||||
try {
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack(null));
|
||||
@@ -57,10 +57,10 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
|
||||
String name = controller.getSettings().get(MODPACK_NAME);
|
||||
if (name != null) {
|
||||
txtModpackName.setText(name);
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
// trim: https://github.com/HMCL-dev/HMCL/issues/962
|
||||
@@ -75,7 +75,7 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
settings.remove(MODPACK_SERVER_MANIFEST);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ public final class RemoteModpackPage extends ModpackPage {
|
||||
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||
}
|
||||
|
||||
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
|
||||
public static final String MODPACK_NAME = "MODPACK_NAME";
|
||||
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
|
||||
public static final SettingsMap.Key<ServerModpackManifest> MODPACK_SERVER_MANIFEST = new SettingsMap.Key<>("MODPACK_SERVER_MANIFEST");
|
||||
public static final SettingsMap.Key<String> MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
|
||||
public static final SettingsMap.Key<Modpack> MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
|
||||
}
|
||||
+19
-19
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
@@ -37,10 +38,9 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
@@ -66,29 +66,29 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
public void start(SettingsMap settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.change_version.process"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
|
||||
settings.put(FailureCallback.KEY, (settings1, exception, next) -> alertFailureMessage(exception, next));
|
||||
|
||||
// We remove library but not save it,
|
||||
// so if installation failed will not break down current version.
|
||||
Task<Version> ret = Task.supplyAsync(() -> version);
|
||||
List<String> stages = new ArrayList<>();
|
||||
for (Object value : settings.values()) {
|
||||
if (value instanceof RemoteVersion) {
|
||||
RemoteVersion remoteVersion = (RemoteVersion) value;
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof RemoteVersion remoteVersion) {
|
||||
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
|
||||
stages.add(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion()));
|
||||
if ("game".equals(remoteVersion.getLibraryId())) {
|
||||
stages.add("hmcl.install.libraries");
|
||||
stages.add("hmcl.install.assets");
|
||||
}
|
||||
} else if (value instanceof RemoveVersionAction) {
|
||||
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, ((RemoveVersionAction) value).libraryId));
|
||||
} else if (value instanceof RemoveVersionAction removeVersionAction) {
|
||||
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, removeVersionAction.libraryId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> {
|
||||
@@ -133,28 +133,28 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", url);
|
||||
message += i18n("download.code.404", uri);
|
||||
else
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else if (exception instanceof UnsupportedInstallationException) {
|
||||
switch (((UnsupportedInstallationException) exception).getReason()) {
|
||||
|
||||
+11
-13
@@ -28,8 +28,7 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
|
||||
import java.util.Map;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -45,36 +44,37 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
settings.put(PROFILE, profile);
|
||||
public void start(SettingsMap settings) {
|
||||
settings.put(ModpackPage.PROFILE, profile);
|
||||
}
|
||||
|
||||
private Task<Void> finishVersionDownloadingAsync(Map<String, Object> settings) {
|
||||
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameBuilder builder = dependencyManager.gameBuilder();
|
||||
|
||||
String name = (String) settings.get("name");
|
||||
builder.name(name);
|
||||
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
|
||||
|
||||
for (Map.Entry<String, Object> entry : settings.entrySet())
|
||||
if (!"game".equals(entry.getKey()) && entry.getValue() instanceof RemoteVersion)
|
||||
builder.version((RemoteVersion) entry.getValue());
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
if (!"game".equals(key) && value instanceof RemoteVersion remoteVersion)
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
|
||||
return builder.buildAsync().whenComplete(any -> profile.getRepository().refreshVersions())
|
||||
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.new_game.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
settings.put(FailureCallback.KEY, (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
|
||||
return finishVersionDownloadingAsync(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
|
||||
@@ -88,6 +88,4 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final String PROFILE = "PROFILE";
|
||||
}
|
||||
@@ -17,27 +17,23 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
@@ -47,297 +43,86 @@ 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.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
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.Holder;
|
||||
import org.jackhuang.hmcl.util.NativePatcher;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final String libraryId;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
private final JFXSpinner spinner;
|
||||
private final StackPane failedPane;
|
||||
private final StackPane emptyPane;
|
||||
private final TransitionPane root;
|
||||
private final JFXCheckBox chkRelease;
|
||||
private final JFXCheckBox chkSnapshot;
|
||||
private final JFXCheckBox chkOld;
|
||||
private final ComponentList centrePane;
|
||||
private final StackPane center;
|
||||
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final VersionList<?> versionList;
|
||||
private CompletableFuture<?> executor;
|
||||
private final Runnable callback;
|
||||
|
||||
private final HBox searchBar;
|
||||
private final StringProperty queryString = new SimpleStringProperty();
|
||||
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
public VersionsPage(Navigation navigation,
|
||||
String title, String gameVersion,
|
||||
DownloadProvider downloadProvider,
|
||||
String libraryId,
|
||||
Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.navigation = navigation;
|
||||
|
||||
HintPane hintPane = new HintPane();
|
||||
hintPane.setText(i18n("sponsor.bmclapi"));
|
||||
hintPane.getStyleClass().add("sponsor-pane");
|
||||
FXUtils.onClicked(hintPane, this::onSponsor);
|
||||
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
|
||||
this.setTop(hintPane);
|
||||
|
||||
root = new TransitionPane();
|
||||
BorderPane toolbarPane = new BorderPane();
|
||||
JFXButton btnRefresh;
|
||||
{
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
center = new StackPane();
|
||||
center.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
HBox checkPane = new HBox();
|
||||
checkPane.setSpacing(10);
|
||||
{
|
||||
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
|
||||
chkRelease.setSelected(true);
|
||||
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
|
||||
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkOld = new JFXCheckBox(i18n("version.game.old"));
|
||||
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
|
||||
|
||||
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld);
|
||||
}
|
||||
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
TransitionPane rightToolbarPane = new TransitionPane();
|
||||
{
|
||||
HBox refreshPane = new HBox();
|
||||
refreshPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnRefresh = new JFXButton(i18n("button.refresh"));
|
||||
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRefresh.setOnAction(e -> onRefresh());
|
||||
|
||||
JFXButton btnSearch = new JFXButton(i18n("search"));
|
||||
btnSearch.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
searchBar = new HBox();
|
||||
{
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 0));
|
||||
|
||||
JFXTextField searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
|
||||
JFXButton closeSearchBar = new JFXButton();
|
||||
closeSearchBar.getStyleClass().add("jfx-tool-bar-button");
|
||||
closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1)));
|
||||
closeSearchBar.setOnAction(e -> {
|
||||
searchField.clear();
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE);
|
||||
});
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> queryString.set(searchField.getText()));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
btnSearch.setOnAction(e -> {
|
||||
rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE);
|
||||
searchField.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh));
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE);
|
||||
}
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
toolbarPane.setLeft(checkPane);
|
||||
toolbarPane.setRight(rightToolbarPane);
|
||||
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
}
|
||||
|
||||
center.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, this::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, this::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
}
|
||||
this.setCenter(root);
|
||||
|
||||
versionList = downloadProvider.getVersionListById(libraryId);
|
||||
boolean hasType = versionList.hasType();
|
||||
chkRelease.setManaged(hasType);
|
||||
chkRelease.setVisible(hasType);
|
||||
chkSnapshot.setManaged(hasType);
|
||||
chkSnapshot.setVisible(hasType);
|
||||
chkOld.setManaged(hasType);
|
||||
chkOld.setVisible(hasType);
|
||||
|
||||
if (hasType) {
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
} else {
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
List<RemoteVersion> versions = loadVersions();
|
||||
String query = queryString.get();
|
||||
if (!StringUtils.isBlank(query)) {
|
||||
Predicate<RemoteVersion> predicate;
|
||||
if (query.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(query.substring("regex:".length()));
|
||||
predicate = it -> pattern.matcher(it.getSelfVersion()).find();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression", e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = query.toLowerCase(Locale.ROOT);
|
||||
predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString);
|
||||
}
|
||||
|
||||
versions = versions.stream().filter(predicate).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
list.getItems().setAll(versions);
|
||||
};
|
||||
chkRelease.selectedProperty().addListener(listener);
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
queryString.addListener(listener);
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
|
||||
callback.run();
|
||||
});
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
this.callback = callback;
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private List<RemoteVersion> loadVersions() {
|
||||
return versionList.getVersions(gameVersion).stream()
|
||||
.filter(it -> {
|
||||
switch (it.getVersionType()) {
|
||||
case RELEASE:
|
||||
return chkRelease.isSelected();
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
return chkSnapshot.isSelected();
|
||||
case OLD:
|
||||
return chkOld.isSelected();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.sorted().collect(Collectors.toList());
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new VersionsPageSkin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
VersionList<?> currentVersionList = versionList;
|
||||
root.setContent(spinner, ContainerAnimations.FADE);
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
|
||||
if (exception == null) {
|
||||
List<RemoteVersion> items = loadVersions();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
|
||||
root.setContent(emptyPane, ContainerAnimations.FADE);
|
||||
status.set(Status.LOADING);
|
||||
Task<?> task = versionList.refreshAsync(gameVersion)
|
||||
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
|
||||
.whenComplete(Schedulers.javafx(), (items, exception) -> {
|
||||
if (exception == null) {
|
||||
versions.setAll(items);
|
||||
status.set(Status.SUCCESS);
|
||||
} else {
|
||||
if (items.isEmpty()) {
|
||||
chkRelease.setSelected(true);
|
||||
chkSnapshot.setSelected(true);
|
||||
chkOld.setSelected(true);
|
||||
} else {
|
||||
list.getItems().setAll(items);
|
||||
}
|
||||
root.setContent(center, ContainerAnimations.FADE);
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
status.set(Status.FAILED);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
root.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
});
|
||||
task.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -346,10 +131,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
settings.remove(libraryId);
|
||||
if (executor != null)
|
||||
executor.cancel(true);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
@@ -360,27 +143,79 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
navigation.onPrev(true);
|
||||
}
|
||||
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
|
||||
private enum Status {
|
||||
LOADING,
|
||||
FAILED,
|
||||
SUCCESS,
|
||||
}
|
||||
|
||||
private enum VersionTypeFilter {
|
||||
ALL,
|
||||
RELEASE,
|
||||
SNAPSHOTS,
|
||||
APRIL_FOOLS,
|
||||
OLD
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
private final VersionsPage control;
|
||||
|
||||
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, String libraryId) {
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, VersionsPage control) {
|
||||
this.lastCell = lastCell;
|
||||
if ("game".equals(libraryId)) {
|
||||
content.getExternalLinkButton().setGraphic(SVG.GLOBE_BOOK.createIcon(Theme.blackFill(), -1));
|
||||
FXUtils.installFastTooltip(content.getExternalLinkButton(), i18n("wiki.tooltip"));
|
||||
this.control = control;
|
||||
|
||||
HBox hbox = new HBox(16);
|
||||
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
|
||||
HBox actions = new HBox(8);
|
||||
actions.setAlignment(Pos.CENTER);
|
||||
{
|
||||
if ("game".equals(control.libraryId)) {
|
||||
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
|
||||
wikiButton.setOnAction(event -> {
|
||||
onOpenWiki();
|
||||
FXUtils.clearFocus(wikiButton);
|
||||
});
|
||||
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
|
||||
actions.getChildren().add(wikiButton);
|
||||
}
|
||||
|
||||
JFXButton actionButton = newToggleButton4(SVG.ARROW_FORWARD);
|
||||
actionButton.setOnAction(e -> onAction());
|
||||
actions.getChildren().add(actionButton);
|
||||
}
|
||||
|
||||
hbox.getChildren().setAll(imageView, twoLineListItem, actions);
|
||||
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
StackPane.setMargin(hbox, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(new RipplerContainer(hbox));
|
||||
|
||||
FXUtils.onClicked(this, this::onAction);
|
||||
}
|
||||
|
||||
private void onAction() {
|
||||
RemoteVersion item = getItem();
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
control.navigation.getSettings().put(control.libraryId, item);
|
||||
control.callback.run();
|
||||
}
|
||||
|
||||
private void onOpenWiki() {
|
||||
RemoteVersion item = getItem();
|
||||
if (!(item instanceof GameRemoteVersion))
|
||||
return;
|
||||
|
||||
FXUtils.openLink(I18n.getWikiLink((GameRemoteVersion) item));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -398,31 +233,42 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
twoLineListItem.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
} else {
|
||||
content.setSubtitle(null);
|
||||
twoLineListItem.setSubtitle(null);
|
||||
}
|
||||
twoLineListItem.getTags().clear();
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(VersionIconType.GRASS.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
content.setExternalLink(null);
|
||||
break;
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
|
||||
switch (versionType) {
|
||||
case RELEASE -> {
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
}
|
||||
case PENDING, SNAPSHOT -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
}
|
||||
}
|
||||
|
||||
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
|
||||
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
|
||||
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
@@ -432,6 +278,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
iconType = VersionIconType.OPTIFINE;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof CleanroomRemoteVersion)
|
||||
iconType = VersionIconType.CLEANROOM;
|
||||
else if (remoteVersion instanceof NeoForgeRemoteVersion)
|
||||
iconType = VersionIconType.NEO_FORGE;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
@@ -439,15 +287,201 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = null;
|
||||
iconType = VersionIconType.COMMAND;
|
||||
|
||||
content.setImage(iconType != null ? iconType.getIcon() : null);
|
||||
if (content.getSubtitle() == null)
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
imageView.setImage(iconType.getIcon());
|
||||
if (twoLineListItem.getSubtitle() == null)
|
||||
twoLineListItem.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
content.setExternalLink(null);
|
||||
twoLineListItem.addTag(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
|
||||
private final TransitionPane transitionPane;
|
||||
private final JFXSpinner spinner;
|
||||
|
||||
private final JFXTextField nameField;
|
||||
private final JFXComboBox<VersionTypeFilter> categoryField = new JFXComboBox<>();
|
||||
|
||||
VersionsPageSkin(VersionsPage control) {
|
||||
super(control);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
root.setTop(searchPane);
|
||||
searchPane.getStyleClass().addAll("card");
|
||||
BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10));
|
||||
|
||||
ColumnConstraints nameColumn = new ColumnConstraints();
|
||||
nameColumn.setMinWidth(USE_PREF_SIZE);
|
||||
ColumnConstraints column1 = new ColumnConstraints();
|
||||
column1.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints column2 = new ColumnConstraints();
|
||||
column2.setMaxWidth(150);
|
||||
ColumnConstraints column3 = new ColumnConstraints();
|
||||
|
||||
if (control.versionList.hasType())
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
|
||||
else
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
|
||||
|
||||
searchPane.setHgap(16);
|
||||
searchPane.setVgap(10);
|
||||
|
||||
{
|
||||
int rowIndex = 0;
|
||||
|
||||
{
|
||||
nameField = new JFXTextField();
|
||||
nameField.setPromptText(i18n("version.search.prompt"));
|
||||
nameField.textProperty().addListener(o -> updateList());
|
||||
|
||||
if ("game".equals(control.libraryId)) {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS,
|
||||
VersionTypeFilter.APRIL_FOOLS,
|
||||
VersionTypeFilter.OLD
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
|
||||
} else {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
|
||||
}
|
||||
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
|
||||
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
|
||||
|
||||
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
refreshButton.setOnAction(event -> control.onRefresh());
|
||||
|
||||
if (control.versionList.hasType()) {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
new Label(i18n("version.game.type")), categoryField,
|
||||
refreshButton
|
||||
);
|
||||
} else {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
refreshButton
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
root.setCenter(spinnerPane);
|
||||
|
||||
transitionPane = new TransitionPane();
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
StackPane centerWrapper = new StackPane();
|
||||
centerWrapper.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
ComponentList centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
control.versions.addListener((InvalidationListener) o -> updateList());
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control));
|
||||
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
|
||||
centerWrapper.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
StackPane failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, control::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
StackPane emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, control::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(control.status, status -> {
|
||||
if (status == Status.LOADING)
|
||||
transitionPane.setContent(spinner, ContainerAnimations.FADE);
|
||||
else if (status == Status.SUCCESS)
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
else // if (status == Status.FAILED)
|
||||
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
root.setCenter(transitionPane);
|
||||
}
|
||||
|
||||
this.getChildren().setAll(root);
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
|
||||
|
||||
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
return switch (filter) {
|
||||
case RELEASE -> versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD -> versionType == RemoteVersion.Type.OLD;
|
||||
// case ALL,
|
||||
default -> true;
|
||||
};
|
||||
});
|
||||
|
||||
String nameQuery = nameField.getText();
|
||||
if (!StringUtils.isBlank(nameQuery)) {
|
||||
if (nameQuery.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length()));
|
||||
versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find());
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression: " + nameQuery, e);
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT);
|
||||
versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString));
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
list.getItems().setAll(versions.collect(Collectors.toList()));
|
||||
list.scrollTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.Config;
|
||||
import org.jackhuang.hmcl.setting.FontManager;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -34,10 +35,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -54,28 +55,31 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
public void start(SettingsMap settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
|
||||
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
|
||||
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
|
||||
public Object finish(SettingsMap settings) {
|
||||
List<String> whitelist = settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
|
||||
Path modpackFile = settings.get(ModpackInfoPage.MODPACK_FILE);
|
||||
ModpackExportInfo exportInfo = settings.get(ModpackInfoPage.MODPACK_INFO);
|
||||
exportInfo.setWhitelist(whitelist);
|
||||
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
|
||||
String modpackType = settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
|
||||
|
||||
return exportWithLauncher(modpackType, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
Path launcherJar = JarUtils.thisJarPath();
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
|
||||
return new Task<Object>() {
|
||||
File tempModpack;
|
||||
return new Task<>() {
|
||||
Path tempModpack;
|
||||
Task<?> exportTask;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
@@ -83,9 +87,9 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
File dest;
|
||||
Path dest;
|
||||
if (packWithLauncher) {
|
||||
dest = tempModpack = Files.createTempFile("hmcl", ".zip").toFile();
|
||||
dest = tempModpack = Files.createTempFile("hmcl", ".zip");
|
||||
} else {
|
||||
dest = modpackFile;
|
||||
}
|
||||
@@ -100,6 +104,9 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
exportTask = exportAsServer(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH:
|
||||
exportTask = exportAsModrinth(exportInfo, dest);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
|
||||
}
|
||||
@@ -114,7 +121,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!packWithLauncher) return;
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
try (Zipper zip = new Zipper(modpackFile)) {
|
||||
Config exported = new Config();
|
||||
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
@@ -157,10 +164,14 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency = null;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
@@ -173,10 +184,14 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
@@ -217,10 +232,14 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
@@ -233,18 +252,39 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ModrinthModpackExportTask(
|
||||
profile.getRepository(),
|
||||
version,
|
||||
exportInfo,
|
||||
modpackFile
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependencies() {
|
||||
return Collections.singleton(dependency);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new ModpackTypeSelectionPage(controller);
|
||||
case 1:
|
||||
return new ModpackInfoPage(controller, profile.getRepository(), version);
|
||||
case 2:
|
||||
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
|
||||
default:
|
||||
throw new IllegalArgumentException("step");
|
||||
}
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
return switch (step) {
|
||||
case 0 -> new ModpackTypeSelectionPage(controller);
|
||||
case 1 -> new ModpackInfoPage(controller, profile.getRepository(), version);
|
||||
case 2 -> new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
|
||||
default -> throw new IllegalArgumentException("step");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loaded 100 of 546 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user