Compare commits
127
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6375558591 | ||
|
|
12b525fdc1 | ||
|
|
ca69dd8032 | ||
|
|
95cbc6f9b0 | ||
|
|
d0145d53d7 | ||
|
|
ee0ae6e4ec | ||
|
|
210ab077cb | ||
|
|
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 |
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
|
||||
|
||||
@@ -22,4 +22,4 @@ jobs:
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
@@ -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
|
||||
@@ -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
|
||||
+7
-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
|
||||
|
||||
@@ -14,14 +15,17 @@ hs_err_pid*
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
/.java/
|
||||
/.local/
|
||||
/.cache/
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLBoot/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLMultiMCBootstrap/build/
|
||||
/buildSrc/build/
|
||||
|
||||
# idea
|
||||
@@ -30,12 +34,14 @@ hmcl-exported-logs-*
|
||||
/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
|
||||
|
||||
+80
-35
@@ -1,5 +1,11 @@
|
||||
import org.jackhuang.hmcl.gradle.CheckTranslations
|
||||
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
|
||||
@@ -13,30 +19,36 @@ 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
|
||||
|
||||
@@ -47,7 +59,7 @@ dependencies {
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
|
||||
if (launcherExe == null) {
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
@@ -109,8 +121,25 @@ tasks.compileJava {
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -137,22 +166,6 @@ val createPropertiesFile by tasks.registering {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
tasks.jar {
|
||||
enabled = false
|
||||
dependsOn(tasks["shadowJar"])
|
||||
@@ -192,7 +205,7 @@ tasks.shadowJar {
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
if (launcherExe != null) {
|
||||
if (launcherExe.isNotBlank()) {
|
||||
into("assets") {
|
||||
from(file(launcherExe))
|
||||
}
|
||||
@@ -206,11 +219,20 @@ tasks.shadowJar {
|
||||
|
||||
tasks.processResources {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
@@ -344,6 +366,29 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
||||
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") {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copy from https://github.com/sshahine/JFoenix/blob/d427fd801a338f934307ba41ce604eb5c79f0b20/jfoenix/src/main/java/com/jfoenix/skins/JFXListViewSkin.java
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.skins.ListViewSkin;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4720
|
||||
public class JFXListViewSkin<T> extends ListViewSkin<T> {
|
||||
|
||||
private final VirtualFlow<ListCell<T>> flow;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public JFXListViewSkin(final JFXListView<T> listView) {
|
||||
super(listView);
|
||||
flow = (VirtualFlow<ListCell<T>>) getChildren().get(0);
|
||||
JFXDepthManager.setDepth(flow, listView.depthProperty().get());
|
||||
listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
final int itemsCount = getSkinnable().getItems().size();
|
||||
if (getSkinnable().maxHeightProperty().isBound() || itemsCount <= 0) {
|
||||
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
}
|
||||
|
||||
final double fixedCellSize = getSkinnable().getFixedCellSize();
|
||||
double computedHeight = fixedCellSize != Region.USE_COMPUTED_SIZE ?
|
||||
fixedCellSize * itemsCount + snapVerticalInsets() : estimateHeight();
|
||||
double height = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
if (height > computedHeight) {
|
||||
height = computedHeight;
|
||||
}
|
||||
|
||||
if (getSkinnable().getMaxHeight() > 0 && computedHeight > getSkinnable().getMaxHeight()) {
|
||||
return getSkinnable().getMaxHeight();
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
private double estimateHeight() {
|
||||
// compute the border/padding for the list
|
||||
double borderWidth = snapVerticalInsets();
|
||||
// compute the gap between list cells
|
||||
|
||||
JFXListView<T> listview = (JFXListView<T>) getSkinnable();
|
||||
double gap = listview.isExpanded() ? ((JFXListView<T>) getSkinnable()).getVerticalGap() * (getSkinnable().getItems()
|
||||
.size()) : 0;
|
||||
// compute the height of each list cell
|
||||
double cellsHeight = 0;
|
||||
for (int i = 0; i < flow.getCellCount(); i++) {
|
||||
ListCell<T> cell = flow.getCell(i);
|
||||
cellsHeight += cell.getHeight();
|
||||
}
|
||||
return cellsHeight + gap + borderWidth;
|
||||
}
|
||||
|
||||
private double snapVerticalInsets() {
|
||||
return getSkinnable().snappedBottomInset() + getSkinnable().snappedTopInset();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,21 +46,7 @@ public final class EntryPoint {
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
checkDirectoryPath();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
@@ -80,6 +66,71 @@ public final class EntryPoint {
|
||||
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 {
|
||||
|
||||
@@ -75,13 +75,7 @@ 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));
|
||||
|
||||
@@ -243,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"));
|
||||
}
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -26,7 +26,6 @@ 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.nio.file.*;
|
||||
import java.util.*;
|
||||
@@ -64,7 +63,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
if (config().isDisableAutoGameOptions())
|
||||
return;
|
||||
|
||||
Path runDir = repository.getRunDirectory(version.getId()).toPath();
|
||||
Path runDir = repository.getRunDirectory(version.getId());
|
||||
Path optionsFile = runDir.resolve("options.txt");
|
||||
Path configFolder = runDir.resolve("config");
|
||||
|
||||
@@ -81,8 +80,6 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
Locale locale = Locale.getDefault();
|
||||
if (LocaleUtils.isEnglish(locale))
|
||||
return;
|
||||
|
||||
/*
|
||||
* 1.0 : No language option, do not set for these versions
|
||||
@@ -113,7 +110,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
||||
String region = locale.getCountry();
|
||||
|
||||
return switch (LocaleUtils.getISO1Language(locale)) {
|
||||
return switch (LocaleUtils.getRootLanguage(locale)) {
|
||||
case "es" -> "es_ES";
|
||||
case "ja" -> "ja_JP";
|
||||
case "ru" -> "ru_RU";
|
||||
@@ -130,6 +127,13 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
yield "zh_CN";
|
||||
}
|
||||
case "en" -> {
|
||||
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
|
||||
yield "en_UD";
|
||||
}
|
||||
|
||||
yield "";
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
@@ -141,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,22 +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);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -143,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);
|
||||
|
||||
@@ -178,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(Files.readString(file.toPath()), 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.
|
||||
@@ -256,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);
|
||||
}
|
||||
}
|
||||
@@ -269,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);
|
||||
@@ -277,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,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);
|
||||
}
|
||||
@@ -334,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) {
|
||||
@@ -368,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()
|
||||
@@ -379,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()
|
||||
@@ -404,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())
|
||||
@@ -425,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 = Files.readString(json.toPath());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,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) {
|
||||
@@ -458,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()
|
||||
@@ -489,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,14 +29,15 @@ import org.jackhuang.hmcl.task.Task;
|
||||
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.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;
|
||||
@@ -44,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());
|
||||
@@ -64,8 +65,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.fromJsonFile(json.toPath(), 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,15 +49,16 @@ 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.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;
|
||||
|
||||
@@ -76,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;
|
||||
@@ -119,9 +120,8 @@ public final class LauncherHelper {
|
||||
launch0();
|
||||
}
|
||||
|
||||
public void makeLaunchScript(File scriptFile) {
|
||||
public void makeLaunchScript(Path scriptFile) {
|
||||
this.scriptFile = Objects.requireNonNull(scriptFile);
|
||||
|
||||
launch();
|
||||
}
|
||||
|
||||
@@ -160,19 +160,17 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT
|
||||
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& OperatingSystem.WINDOWS_VERSION != null) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
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)
|
||||
@@ -193,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,
|
||||
@@ -226,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(
|
||||
@@ -709,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;
|
||||
@@ -852,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,7 +915,15 @@ public final class LauncherHelper {
|
||||
|
||||
}
|
||||
|
||||
public static final Queue<WeakReference<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())
|
||||
|
||||
@@ -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;
|
||||
@@ -58,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),
|
||||
@@ -69,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);
|
||||
}
|
||||
@@ -132,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.fromJsonFile(file.toPath(), 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) {
|
||||
@@ -171,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 = () -> {
|
||||
@@ -230,8 +233,8 @@ 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();
|
||||
|
||||
@@ -207,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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -220,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,10 +20,13 @@ 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.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;
|
||||
@@ -98,8 +101,7 @@ 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(HexFormat.of().formatHex(messageDigest.digest()), size));
|
||||
}
|
||||
|
||||
@@ -209,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 {
|
||||
@@ -255,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
|
||||
@@ -280,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) {
|
||||
@@ -310,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -37,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";
|
||||
|
||||
@@ -34,20 +34,18 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.ObservableField;
|
||||
import org.jackhuang.hmcl.util.gson.PaintAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(value = Config.Adapter.class)
|
||||
@@ -57,7 +55,7 @@ public final class Config implements Observable {
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
|
||||
public static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(Path.class, PathTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
||||
@@ -223,7 +221,7 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("localization")
|
||||
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
||||
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(SupportedLocale.DEFAULT);
|
||||
|
||||
public ObjectProperty<SupportedLocale> localizationProperty() {
|
||||
return localization;
|
||||
@@ -252,6 +250,21 @@ public final class Config implements Observable {
|
||||
this.promptedVersion.set(promptedVersion);
|
||||
}
|
||||
|
||||
@SerializedName("acceptPreviewUpdate")
|
||||
private final BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty acceptPreviewUpdateProperty() {
|
||||
return acceptPreviewUpdate;
|
||||
}
|
||||
|
||||
public boolean isAcceptPreviewUpdate() {
|
||||
return acceptPreviewUpdate.get();
|
||||
}
|
||||
|
||||
public void setAcceptPreviewUpdate(boolean acceptPreviewUpdate) {
|
||||
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
|
||||
}
|
||||
|
||||
@SerializedName("shownTips")
|
||||
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
@@ -260,7 +273,7 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("commonDirType")
|
||||
private final ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
private final ObjectProperty<EnumCommonDirectory> commonDirType = new RawPreservingObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
|
||||
public ObjectProperty<EnumCommonDirectory> commonDirTypeProperty() {
|
||||
return commonDirType;
|
||||
@@ -367,7 +380,11 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("animationDisabled")
|
||||
private final BooleanProperty animationDisabled = new SimpleBooleanProperty();
|
||||
private final BooleanProperty animationDisabled = new SimpleBooleanProperty(
|
||||
FXUtils.REDUCED_MOTION == Boolean.TRUE
|
||||
|| !JavaRuntime.CURRENT_JIT_ENABLED
|
||||
|| !FXUtils.GPU_ACCELERATION_ENABLED
|
||||
);
|
||||
|
||||
public BooleanProperty animationDisabledProperty() {
|
||||
return animationDisabled;
|
||||
@@ -397,7 +414,7 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("backgroundType")
|
||||
private final ObjectProperty<EnumBackgroundImage> backgroundImageType = new SimpleObjectProperty<>(EnumBackgroundImage.DEFAULT);
|
||||
private final ObjectProperty<EnumBackgroundImage> backgroundImageType = new RawPreservingObjectProperty<>(EnumBackgroundImage.DEFAULT);
|
||||
|
||||
public ObjectProperty<EnumBackgroundImage> backgroundImageTypeProperty() {
|
||||
return backgroundImageType;
|
||||
@@ -769,9 +786,7 @@ public final class Config implements Observable {
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(config);
|
||||
if (config.tracker.isDirty(observable)) {
|
||||
JsonElement serialized = field.serialize(config, context);
|
||||
if (serialized != null && !serialized.isJsonNull())
|
||||
result.add(field.getSerializedName(), serialized);
|
||||
field.serialize(result, config, context);
|
||||
}
|
||||
}
|
||||
config.unknownFields.forEach(result::add);
|
||||
@@ -791,6 +806,14 @@ public final class Config implements Observable {
|
||||
var values = new LinkedHashMap<>(json.getAsJsonObject().asMap());
|
||||
for (ObservableField<Config> field : FIELDS) {
|
||||
JsonElement value = values.remove(field.getSerializedName());
|
||||
if (value == null) {
|
||||
for (String alternateName : field.getAlternateNames()) {
|
||||
value = values.remove(alternateName);
|
||||
if (value != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
config.tracker.markDirty(field.get(config));
|
||||
field.deserialize(config, value, context);
|
||||
|
||||
@@ -31,23 +31,21 @@ import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
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,71 +67,68 @@ 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) {
|
||||
|
||||
@@ -48,6 +48,8 @@ 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();
|
||||
@@ -94,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();
|
||||
}
|
||||
@@ -153,6 +167,7 @@ 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",
|
||||
@@ -169,6 +184,7 @@ 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()));
|
||||
@@ -196,6 +212,7 @@ 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));
|
||||
|
||||
@@ -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,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 {
|
||||
@@ -39,6 +40,18 @@ public enum VersionIconType {
|
||||
|
||||
// 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());
|
||||
@@ -827,6 +842,7 @@ 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));
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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.CancellationException;
|
||||
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 {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
if (TerracottaMetadata.PROVIDER == null)
|
||||
throw new IOException("Unsupported platform: " + org.jackhuang.hmcl.util.platform.Platform.CURRENT_PLATFORM);
|
||||
|
||||
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()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 = 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 CancellationException) {
|
||||
} 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 compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
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,205 @@
|
||||
/*
|
||||
* 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.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 @Nullable TerracottaNative of(String classifier) {
|
||||
String hash = this.classifiers.get(classifier);
|
||||
if (hash == null)
|
||||
return null;
|
||||
|
||||
if (!hash.startsWith("sha256:"))
|
||||
throw new IllegalArgumentException(String.format("Invalid hash value %s for classifier %s.", hash, classifier));
|
||||
hash = hash.substring("sha256:".length());
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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 arch = Architecture.SYSTEM_ARCH.getCheckedName();
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
if (!OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
|
||||
yield null;
|
||||
|
||||
TerracottaNative target = config.of("windows-%s.exe".formatted(arch));
|
||||
yield target != null
|
||||
? new ProviderContext(new GeneralProvider(target), "windows", arch)
|
||||
: null;
|
||||
}
|
||||
case LINUX -> {
|
||||
TerracottaNative target = config.of("linux-%s".formatted(arch));
|
||||
yield target != null
|
||||
? new ProviderContext(new GeneralProvider(target), "linux", arch)
|
||||
: null;
|
||||
}
|
||||
case MACOS -> {
|
||||
TerracottaNative installer = config.of("macos-%s.pkg".formatted(arch));
|
||||
TerracottaNative binary = config.of("macos-%s".formatted(arch));
|
||||
|
||||
yield installer != null && binary != null
|
||||
? new ProviderContext(new MacOSProvider(installer, binary), "macos", arch)
|
||||
: null;
|
||||
}
|
||||
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.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-28
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -15,33 +15,15 @@
|
||||
* 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.event;
|
||||
package org.jackhuang.hmcl.terracotta.profile;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
public class FailedEvent<T> extends Event {
|
||||
|
||||
private final int failedTime;
|
||||
private T newResult;
|
||||
|
||||
public FailedEvent(Object source, int failedTime, T newResult) {
|
||||
super(source);
|
||||
this.failedTime = failedTime;
|
||||
this.newResult = newResult;
|
||||
}
|
||||
|
||||
public int getFailedTime() {
|
||||
return failedTime;
|
||||
}
|
||||
|
||||
public T getNewResult() {
|
||||
return newResult;
|
||||
}
|
||||
|
||||
public void setNewResult(T newResult) {
|
||||
this.newResult = newResult;
|
||||
}
|
||||
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,121 @@
|
||||
/*
|
||||
* 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.Metadata;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaNative;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
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.StandardCopyOption;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
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(() -> {
|
||||
Path osascript = SystemUtils.which("osascript");
|
||||
if (osascript == null) {
|
||||
throw new IllegalStateException("Cannot locate 'osascript' system executable on MacOS for installing Terracotta.");
|
||||
}
|
||||
|
||||
Path pkg = Files.createTempDirectory(Metadata.HMCL_GLOBAL_DIRECTORY, "terracotta-pkg")
|
||||
.toRealPath()
|
||||
.resolve(FileUtils.getName(installer.getPath()));
|
||||
Files.copy(installer.getPath(), pkg, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
|
||||
osascript.toString(), "-e", String.format(
|
||||
"do shell script \"installer -pkg '%s' -target /\" with prompt \"%s\" with administrator privileges",
|
||||
pkg, i18n("terracotta.sudo_installing")
|
||||
)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
return Task.fromCompletableFuture(process.getProcess().onExit()).thenRunAsync(() -> {
|
||||
try {
|
||||
FileUtils.cleanDirectory(pkg.getParent());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Cannot remove temporary Terracotta package file.", e);
|
||||
}
|
||||
|
||||
if (process.getExitCode() != 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Cannot install Terracotta %s: system installer exited with code %d",
|
||||
pkg,
|
||||
process.getExitCode()
|
||||
));
|
||||
}
|
||||
});
|
||||
}),
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -347,6 +359,24 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
// 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")));
|
||||
@@ -404,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, "");
|
||||
}
|
||||
@@ -450,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);
|
||||
|
||||
@@ -36,6 +36,7 @@ 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;
|
||||
@@ -49,9 +50,7 @@ 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.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
@@ -79,24 +78,25 @@ import org.xml.sax.SAXException;
|
||||
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;
|
||||
@@ -109,6 +109,25 @@ 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;
|
||||
@@ -440,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;
|
||||
@@ -475,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);
|
||||
}
|
||||
@@ -518,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1190,7 +1216,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1234,14 +1259,14 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
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();
|
||||
@@ -1250,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);
|
||||
@@ -1342,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) {
|
||||
@@ -1355,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.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;
|
||||
@@ -318,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);
|
||||
@@ -326,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());
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
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;
|
||||
@@ -311,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) {
|
||||
|
||||
@@ -69,6 +69,7 @@ public enum SVG {
|
||||
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
|
||||
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
|
||||
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
|
||||
GRAPH2("M5 22q-1.25 0-2.125-.875T2 19q0-.975.5625-1.75T4 16.175V14q0-1.25.875-2.125T7 11h4V7.825q-.875-.3-1.4375-1.075T9 5q0-1.25.875-2.125T12 2t2.125.875T15 5q0 .975-.5625 1.75T13 7.825V11h4q1.25 0 2.125.875T20 14v2.175q.875.3 1.4375 1.075T22 19q0 1.25-.875 2.125T19 22t-2.125-.875T16 19q0-.975.5625-1.75T18 16.175V14q0-.425-.2875-.7125T17 13H13v3.175q.875.3 1.4375 1.075T15 19q0 1.25-.875 2.125T12 22t-2.125-.875T9 19q0-.975.5625-1.75T11 16.175V13H7q-.425 0-.7125.2875T6 14v2.175q.875.3 1.4375 1.075T8 19q0 1.25-.875 2.125T5 22Zm0-2q.425 0 .7125-.2875T6 19q0-.425-.2875-.7125T5 18q-.425 0-.7125.2875T4 19q0 .425.2875.7125T5 20Zm7 0q.425 0 .7125-.2875T13 19q0-.425-.2875-.7125T12 18q-.425 0-.7125.2875T11 19q0 .425.2875.7125T12 20Zm7 0q.425 0 .7125-.2875T20 19q0-.425-.2875-.7125T19 18q-.425 0-.7125.2875T18 19q0 .425.2875.7125T19 20ZM12 6q.425 0 .7125-.2875T13 5q0-.425-.2875-.7125T12 4q-.425 0-.7125.2875T11 5q0 .425.2875.7125T12 6Z"),
|
||||
HELP("M11.95 18Q12.475 18 12.8375 17.6375T13.2 16.75Q13.2 16.225 12.8375 15.8625T11.95 15.5Q11.425 15.5 11.0625 15.8625T10.7 16.75Q10.7 17.275 11.0625 17.6375T11.95 18ZM11.05 14.15H12.9Q12.9 13.325 13.0875 12.85T14.15 11.55Q14.8 10.9 15.175 10.3125T15.55 8.9Q15.55 7.5 14.525 6.75T12.1 6Q10.675 6 9.7875 6.75T8.55 8.55L10.2 9.2Q10.325 8.75 10.7625 8.225T12.1 7.7Q12.9 7.7 13.3 8.1375T13.7 9.1Q13.7 9.6 13.4 10.0375T12.65 10.85Q11.55 11.825 11.3 12.325T11.05 14.15ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"),
|
||||
HOST("M4 21Q3.175 21 2.5875 20.4125T2 19V5Q2 4.175 2.5875 3.5875T4 3H9Q9.825 3 10.4125 3.5875T11 5V19Q11 19.825 10.4125 20.4125T9 21H4ZM15 21Q14.175 21 13.5875 20.4125T13 19V5Q13 4.175 13.5875 3.5875T15 3H20Q20.825 3 21.4125 3.5875T22 5V19Q22 19.825 21.4125 20.4125T20 21H15ZM4 19H9V5H4V19ZM15 19H20V5H15V19ZM5 15H8V13H5V15ZM16 15H19V13H16V15ZM5 12H8V10H5V12ZM16 12H19V10H16V12ZM5 9H8V7H5V9ZM16 9H19V7H16V9ZM4 19H9 4ZM15 19H20 15Z"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,25 +76,9 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
|
||||
setActionButtonVisible(false);
|
||||
|
||||
setOnScroll(event -> {
|
||||
double deltaY = event.getDeltaY();
|
||||
if (deltaY == 0)
|
||||
return;
|
||||
|
||||
Account current = account.get();
|
||||
if (current == null) return;
|
||||
|
||||
ObservableList<Account> accounts = Accounts.getAccounts();
|
||||
int currentIndex = accounts.indexOf(current);
|
||||
if (currentIndex < 0) return;
|
||||
|
||||
if (deltaY > 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 -> {
|
||||
|
||||
@@ -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;
|
||||
@@ -45,51 +51,23 @@ 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.time.Duration;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
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() {
|
||||
if ("Asia/Shanghai".equals(ZoneId.systemDefault().getId()))
|
||||
return true;
|
||||
|
||||
// Check if the time zone is UTC+8
|
||||
if (ZonedDateTime.now().getOffset().getTotalSeconds() == Duration.ofHours(8).toSeconds()) {
|
||||
if ("CN".equals(LocaleUtils.SYSTEM_DEFAULT.getCountry()))
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
@@ -268,33 +264,12 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
};
|
||||
|
||||
if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) {
|
||||
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"),
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
/**
|
||||
@@ -37,7 +36,7 @@ public final class AnimationUtils {
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled() && FXUtils.REDUCED_MOTION != Boolean.TRUE;
|
||||
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
|
||||
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
|
||||
|
||||
public static boolean isAnimationEnabled() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -40,6 +40,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class TaskExecutorDialogPane extends BorderPane {
|
||||
private TaskExecutor executor;
|
||||
private TaskCancellationAction onCancel;
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
private final Label lblTitle;
|
||||
@@ -84,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) {
|
||||
@@ -95,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(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
|
||||
String message = String.format("%.1f %s", speed, unit);
|
||||
Platform.runLater(() -> lblProgress.setText(message));
|
||||
});
|
||||
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
}
|
||||
|
||||
@@ -33,9 +33,8 @@ import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
public static Label createTagLabel(String tag) {
|
||||
private static Label createTagLabel(String tag) {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
@@ -111,7 +110,15 @@ public class TwoLineListItem extends VBox {
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
getTags().add(createTagLabel(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() {
|
||||
|
||||
@@ -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,14 +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.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;
|
||||
|
||||
@@ -145,10 +144,10 @@ 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;
|
||||
}
|
||||
@@ -223,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() {
|
||||
@@ -260,7 +259,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getSettings() {
|
||||
public SettingsMap getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -287,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);
|
||||
@@ -330,7 +331,5 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final String PROFILE = "PROFILE";
|
||||
}
|
||||
}
|
||||
@@ -27,8 +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 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;
|
||||
@@ -73,7 +72,7 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
|
||||
private static boolean checkName(String name) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
+24
-30
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -36,20 +36,18 @@ 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.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 {
|
||||
@@ -71,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();
|
||||
});
|
||||
@@ -112,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;
|
||||
@@ -150,7 +148,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
.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());
|
||||
@@ -167,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");
|
||||
}
|
||||
+9
-10
@@ -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;
|
||||
@@ -40,7 +41,6 @@ import java.net.SocketTimeoutException;
|
||||
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,30 +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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,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, () -> {
|
||||
|
||||
+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";
|
||||
}
|
||||
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -49,24 +50,30 @@ 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.*;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
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.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
@@ -124,7 +131,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
settings.remove(libraryId);
|
||||
}
|
||||
|
||||
@@ -172,7 +179,10 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
{
|
||||
if ("game".equals(control.libraryId)) {
|
||||
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
|
||||
wikiButton.setOnAction(event -> onOpenWiki());
|
||||
wikiButton.setOnAction(event -> {
|
||||
onOpenWiki();
|
||||
FXUtils.clearFocus(wikiButton);
|
||||
});
|
||||
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
|
||||
actions.getChildren().add(wikiButton);
|
||||
}
|
||||
@@ -233,13 +243,14 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
case RELEASE -> {
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
}
|
||||
case PENDING, SNAPSHOT -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
@@ -248,11 +259,16 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default -> {
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -361,18 +377,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
);
|
||||
}
|
||||
}
|
||||
// {
|
||||
// HBox actionsBox = new HBox(8);
|
||||
// GridPane.setColumnSpan(actionsBox, 4);
|
||||
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
//
|
||||
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
// refreshButton.setOnAction(event -> control.onRefresh());
|
||||
//
|
||||
// actionsBox.getChildren().setAll(refreshButton);
|
||||
//
|
||||
// searchPane.addRow(rowIndex++, actionsBox);
|
||||
// }
|
||||
}
|
||||
|
||||
{
|
||||
@@ -448,21 +452,16 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
switch (filter) {
|
||||
case RELEASE:
|
||||
return versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD:
|
||||
return versionType == RemoteVersion.Type.OLD;
|
||||
case ALL:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -35,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.*;
|
||||
@@ -55,26 +55,25 @@ 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;
|
||||
|
||||
{
|
||||
@@ -88,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;
|
||||
}
|
||||
@@ -122,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());
|
||||
@@ -165,7 +164,7 @@ 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;
|
||||
|
||||
@@ -185,7 +184,7 @@ 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;
|
||||
|
||||
@@ -233,7 +232,7 @@ 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;
|
||||
|
||||
@@ -253,7 +252,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@@ -279,17 +278,13 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@@ -33,10 +33,13 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
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.StringUtils;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
@@ -45,6 +48,7 @@ import java.util.Objects;
|
||||
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;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -80,16 +84,18 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
this.setBottom(nextPane);
|
||||
}
|
||||
|
||||
private CheckBoxTreeItem<String> getTreeItem(File file, String basePath) {
|
||||
if (!file.exists())
|
||||
private CheckBoxTreeItem<String> getTreeItem(Path file, String basePath) {
|
||||
if (Files.notExists(file))
|
||||
return null;
|
||||
|
||||
boolean isDirectory = Files.isDirectory(file);
|
||||
|
||||
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
|
||||
if (basePath.length() > "minecraft/".length()) {
|
||||
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (file.isDirectory() ? "/" : ""), file.isDirectory());
|
||||
if (file.isFile() && Objects.equals(FileUtils.getNameWithoutExtension(file), version)) // Ignore <version>.json, <version>.jar
|
||||
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (isDirectory ? "/" : ""), isDirectory);
|
||||
if (!isDirectory && Objects.equals(FileUtils.getNameWithoutExtension(file), version))
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (file.isDirectory() && Objects.equals(file.getName(), version + "-natives")) // Ignore <version>-natives
|
||||
if (isDirectory && Objects.equals(FileUtils.getName(file), version + "-natives")) // Ignore <version>-natives
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (state == ModAdviser.ModSuggestion.HIDDEN)
|
||||
return null;
|
||||
@@ -99,11 +105,10 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
if (state == ModAdviser.ModSuggestion.SUGGESTED)
|
||||
node.setSelected(true);
|
||||
|
||||
if (file.isDirectory()) {
|
||||
File[] files = file.listFiles();
|
||||
if (files != null) {
|
||||
for (File it : files) {
|
||||
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + it.getName());
|
||||
if (isDirectory) {
|
||||
try (var stream = Files.list(file)) {
|
||||
stream.forEach(it -> {
|
||||
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + FileUtils.getName(it));
|
||||
if (subNode != null) {
|
||||
node.setSelected(subNode.isSelected() || node.isSelected());
|
||||
if (!subNode.isSelected()) {
|
||||
@@ -111,8 +116,11 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
}
|
||||
node.getChildren().add(subNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list contents of " + file, e);
|
||||
}
|
||||
|
||||
if (!node.isSelected()) node.setIndeterminate(false);
|
||||
|
||||
// Empty folder need not to be displayed.
|
||||
@@ -155,7 +163,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
controller.getSettings().remove(MODPACK_FILE_SELECTION);
|
||||
}
|
||||
|
||||
@@ -171,7 +179,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
return i18n("modpack.wizard.step.2.title");
|
||||
}
|
||||
|
||||
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
|
||||
public static final SettingsMap.Key<List<String>> MODPACK_FILE_SELECTION = new SettingsMap.Key<>("modpack.accepted");
|
||||
private static final Map<String, String> TRANSLATION = mapOf(
|
||||
pair("minecraft/hmclversion.cfg", i18n("modpack.files.hmclversion_cfg")),
|
||||
pair("minecraft/servers.dat", i18n("modpack.files.servers_dat")),
|
||||
|
||||
@@ -41,11 +41,13 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
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.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -56,7 +58,6 @@ import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
@@ -87,10 +88,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
|
||||
this.controller = controller;
|
||||
this.gameRepository = gameRepository;
|
||||
this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
|
||||
this.options = controller.getSettings().get(MODPACK_INFO_OPTION);
|
||||
this.versionName = version;
|
||||
|
||||
if (this.options == null)
|
||||
throw new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required");
|
||||
|
||||
name.set(version);
|
||||
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
|
||||
|
||||
@@ -110,9 +113,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
fileChooser.setInitialFileName(name.get() + ".mrpack");
|
||||
} else {
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
fileChooser.setInitialFileName(name.get() + ".zip");
|
||||
fileChooser.setInitialFileName(name.get() + ".zip");
|
||||
}
|
||||
File file = fileChooser.showSaveDialog(Controllers.getStage());
|
||||
Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file == null) {
|
||||
controller.onEnd();
|
||||
return;
|
||||
@@ -146,7 +149,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
controller.getSettings().remove(MODPACK_INFO);
|
||||
}
|
||||
|
||||
@@ -160,9 +163,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
return new ModpackInfoPageSkin(this);
|
||||
}
|
||||
|
||||
public static final String MODPACK_INFO = "modpack.info";
|
||||
public static final String MODPACK_FILE = "modpack.file";
|
||||
public static final String MODPACK_INFO_OPTION = "modpack.info.option";
|
||||
public static final SettingsMap.Key<ModpackExportInfo> MODPACK_INFO = new SettingsMap.Key<>("modpack.info");
|
||||
public static final SettingsMap.Key<Path> MODPACK_FILE = new SettingsMap.Key<>("modpack.file");
|
||||
public static final SettingsMap.Key<ModpackExportInfo.Options> MODPACK_INFO_OPTION = new SettingsMap.Key<>("modpack.info.option");
|
||||
|
||||
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
|
||||
private ObservableList<Node> originList;
|
||||
@@ -189,7 +192,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
|
||||
hyperlink.setOnAction(e -> FXUtils.openLink(Metadata.DOCS_URL + "/modpack/serverpack.html"));
|
||||
borderPane.setTop(hyperlink);
|
||||
} if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
|
||||
}
|
||||
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
|
||||
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
pane.setText(i18n("modpack.wizard.step.initialization.modrinth.info"));
|
||||
borderPane.setTop(pane);
|
||||
@@ -381,7 +385,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
|
||||
|
||||
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
|
||||
BorderPane noCreateRemoteFiles = new BorderPane();
|
||||
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
|
||||
|
||||
@@ -33,8 +33,7 @@ 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 java.util.Map;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -89,7 +88,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,7 +96,7 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
return i18n("modpack.wizard.step.3.title");
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "modpack.type";
|
||||
public static final SettingsMap.Key<String> MODPACK_TYPE = new SettingsMap.Key<>("modpack.type");
|
||||
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
|
||||
+2
-2
@@ -287,7 +287,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
|
||||
* the mark position becomes invalid.
|
||||
* @see InputStream#reset()
|
||||
*/
|
||||
public synchronized void mark(int readlimit) {
|
||||
public void mark(int readlimit) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,7 +302,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
|
||||
* @see InputStream#mark(int)
|
||||
* @see IOException
|
||||
*/
|
||||
public synchronized void reset() throws IOException {
|
||||
public void reset() throws IOException {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,9 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
@@ -340,8 +341,8 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
|
||||
throw new IOException("Missing download URI: " + json);
|
||||
|
||||
File targetFile = File.createTempFile("hmcl-java-", "." + version.getArchiveType());
|
||||
targetFile.deleteOnExit();
|
||||
Path targetFile = Files.createTempFile("hmcl-java-", "." + version.getArchiveType());
|
||||
targetFile.toFile().deleteOnExit();
|
||||
|
||||
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
|
||||
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
|
||||
@@ -363,8 +364,8 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
return getIntegrityCheck
|
||||
.thenComposeAsync(integrityCheck ->
|
||||
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
|
||||
targetFile.toPath(), integrityCheck).setName(fileInfo.getFileName()))
|
||||
.thenSupplyAsync(targetFile::toPath);
|
||||
targetFile, integrityCheck).setName(fileInfo.getFileName()))
|
||||
.thenSupplyAsync(() -> targetFile);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
if (exception == null) {
|
||||
|
||||
@@ -45,13 +45,14 @@ import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -79,19 +80,19 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
|
||||
}
|
||||
|
||||
FXUtils.applyDragListener(this, it -> {
|
||||
String name = it.getName();
|
||||
return it.isDirectory() || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
|
||||
String name = FileUtils.getName(it);
|
||||
return Files.isDirectory(it) || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
|
||||
}, files -> {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
onAddJavaHome(file.toPath());
|
||||
for (Path file : files) {
|
||||
if (Files.isDirectory(file)) {
|
||||
onAddJavaHome(file);
|
||||
} else {
|
||||
String fileName = file.getName();
|
||||
String fileName = FileUtils.getName(file);
|
||||
|
||||
if (fileName.equals(OperatingSystem.CURRENT_OS.getJavaExecutable())) {
|
||||
onAddJavaBinary(file.toPath());
|
||||
onAddJavaBinary(file);
|
||||
} else if (fileName.endsWith(".zip") || fileName.endsWith(".tar.gz")) {
|
||||
onInstallArchive(file.toPath());
|
||||
onInstallArchive(file);
|
||||
} else {
|
||||
throw new AssertionError("Unreachable code");
|
||||
}
|
||||
@@ -110,9 +111,9 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
|
||||
chooser.setTitle(i18n("settings.game.java_directory.choose"));
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
JavaManager.getAddJavaTask(file.toPath()).whenComplete(Schedulers.javafx(), exception -> {
|
||||
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to add java", exception);
|
||||
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
|
||||
@@ -72,6 +72,7 @@ import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
@@ -115,6 +116,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
ImageView titleIcon = new ImageView(FXUtils.newBuiltinImage("/assets/img/icon-title.png"));
|
||||
Label titleLabel = new Label(Metadata.FULL_TITLE);
|
||||
if (I18n.isUpsideDown()) {
|
||||
titleIcon.setRotate(180);
|
||||
titleLabel.setRotate(180);
|
||||
}
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleNode.getChildren().setAll(titleIcon, titleLabel);
|
||||
|
||||
@@ -208,20 +213,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchPane.getStyleClass().add("launch-pane");
|
||||
launchPane.setMaxWidth(230);
|
||||
launchPane.setMaxHeight(55);
|
||||
launchPane.setOnScroll(event -> {
|
||||
double deltaY = event.getDeltaY();
|
||||
if (deltaY == 0)
|
||||
return;
|
||||
|
||||
FXUtils.onScroll(launchPane, versions, list -> {
|
||||
String currentId = getCurrentGame();
|
||||
int index = Lang.indexWhere(versions, instance -> instance.getId().equals(currentId));
|
||||
if (index < 0) return;
|
||||
if (deltaY > 0) // up
|
||||
index--;
|
||||
else // down
|
||||
index++;
|
||||
profile.setSelectedVersion(versions.get((index + versions.size()) % versions.size()).getId());
|
||||
});
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, it -> profile.setSelectedVersion(it.getId()));
|
||||
|
||||
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
|
||||
{
|
||||
JFXButton launchButton = new JFXButton();
|
||||
@@ -435,6 +431,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
return state;
|
||||
}
|
||||
|
||||
public Profile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public String getCurrentGame() {
|
||||
return currentGame.get();
|
||||
}
|
||||
@@ -447,6 +447,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
this.currentGame.set(currentGame);
|
||||
}
|
||||
|
||||
public ObservableList<Version> getVersions() {
|
||||
return versions;
|
||||
}
|
||||
|
||||
public boolean isShowUpdate() {
|
||||
return showUpdate.get();
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
Label textOpacity = new Label();
|
||||
FXUtils.setLimitWidth(textOpacity, 35);
|
||||
|
||||
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
|
||||
textOpacity.textProperty().bind(valueBinding);
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -49,9 +50,11 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -92,16 +95,16 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
FXUtils.applyDragListener(mainPage,
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file.toPath()),
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
|
||||
modpacks -> {
|
||||
File file = modpacks.get(0);
|
||||
Path file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file),
|
||||
i18n("install.modpack"));
|
||||
} else if (NBTFileType.isNBTFileByExtension(file.toPath())) {
|
||||
} else if (NBTFileType.isNBTFileByExtension(file)) {
|
||||
try {
|
||||
Controllers.navigate(new NBTEditorPage(file.toPath()));
|
||||
Controllers.navigate(new NBTEditorPage(file));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Fail to open nbt file", e);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
|
||||
@@ -153,6 +156,10 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
Versions.modifyGameSettings(profile, version);
|
||||
}
|
||||
});
|
||||
FXUtils.onScroll(gameListItem, getSkinnable().getMainPage().getVersions(), list -> {
|
||||
String currentId = getSkinnable().getMainPage().getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, it -> getSkinnable().getMainPage().getProfile().setSelectedVersion(it.getId()));
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
@@ -177,11 +184,29 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem chatItem = new AdvancedListItem();
|
||||
chatItem.setLeftGraphic(wrap(SVG.CHAT));
|
||||
chatItem.setActionButtonVisible(false);
|
||||
chatItem.setTitle(i18n("chat"));
|
||||
chatItem.setOnAction(e -> FXUtils.openLink(Metadata.GROUPS_URL));
|
||||
AdvancedListItem terracottaItem = new AdvancedListItem();
|
||||
terracottaItem.setLeftGraphic(wrap(SVG.GRAPH2));
|
||||
terracottaItem.setActionButtonVisible(false);
|
||||
terracottaItem.setTitle(i18n("terracotta"));
|
||||
|
||||
terracottaItem.setOnAction(e -> {
|
||||
if (TerracottaMetadata.PROVIDER != null) {
|
||||
Controllers.navigate(Controllers.getTerracottaPage());
|
||||
} else {
|
||||
String message;
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
message = i18n("terracotta.unsupported.arch.32bit");
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& !OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
|
||||
message = i18n("terracotta.unsupported.os.windows.old");
|
||||
else if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW))
|
||||
message = i18n("terracotta.unsupported.arch.loongarch64_ow");
|
||||
else
|
||||
message = i18n("terracotta.unsupported");
|
||||
|
||||
Controllers.dialog(message, null, MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
});
|
||||
|
||||
// the left sidebar
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
@@ -193,8 +218,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
.add(downloadItem)
|
||||
.startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT))
|
||||
.add(launcherSettingsItem)
|
||||
.add(chatItem)
|
||||
;
|
||||
.add(terracottaItem)
|
||||
.addNavigationDrawerItem(i18n("chat"), SVG.CHAT, () -> FXUtils.openLink(Metadata.GROUPS_URL));
|
||||
|
||||
// the root page, with the sidebar in left, navigator in center.
|
||||
setLeft(sideBar);
|
||||
@@ -211,14 +236,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
checkedModpack = true;
|
||||
|
||||
if (repository.getVersionCount() == 0) {
|
||||
File modpackFile = new File("modpack.zip").getAbsoluteFile();
|
||||
if (modpackFile.exists()) {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
|
||||
Path modpackFile = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
|
||||
if (Files.exists(modpackFile)) {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
|
||||
.thenApplyAsync(
|
||||
encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
|
||||
encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
|
||||
.thenApplyAsync(modpack -> ModpackHelper
|
||||
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(),
|
||||
modpack)
|
||||
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
|
||||
.executor())
|
||||
.thenAcceptAsync(Schedulers.javafx(), executor -> {
|
||||
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
|
||||
|
||||
@@ -33,12 +33,11 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -66,7 +65,7 @@ public final class SettingsPage extends SettingsView {
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
|
||||
// ==== Languages ====
|
||||
cboLanguage.getItems().setAll(Locales.LOCALES);
|
||||
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
|
||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
@@ -115,9 +114,12 @@ public final class SettingsPage extends SettingsView {
|
||||
chkUpdateStable.setUserData(UpdateChannel.STABLE);
|
||||
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
|
||||
updateChannel.set(UpdateChannel.getChannel());
|
||||
updateChannel.addListener((a, b, newValue) -> {
|
||||
UpdateChecker.requestCheckUpdate(newValue);
|
||||
});
|
||||
|
||||
InvalidationListener checkUpdateListener = e -> {
|
||||
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
|
||||
};
|
||||
updateChannel.addListener(checkUpdateListener);
|
||||
previewPane.selectedProperty().addListener(checkUpdateListener);
|
||||
// ====
|
||||
}
|
||||
|
||||
@@ -254,6 +256,9 @@ public final class SettingsPage extends SettingsView {
|
||||
|
||||
@Override
|
||||
protected void clearCacheDirectory() {
|
||||
FileUtils.cleanDirectoryQuietly(new File(Settings.instance().getCommonDirectory(), "cache"));
|
||||
String commonDirectory = Settings.instance().getCommonDirectory();
|
||||
if (commonDirectory != null) {
|
||||
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -58,6 +58,7 @@ public abstract class SettingsView extends StackPane {
|
||||
protected final JFXRadioButton chkUpdateStable;
|
||||
protected final JFXRadioButton chkUpdateDev;
|
||||
protected final JFXButton btnUpdate;
|
||||
protected final OptionToggleButton previewPane;
|
||||
protected final ScrollPane scroll;
|
||||
|
||||
public SettingsView() {
|
||||
@@ -147,6 +148,15 @@ public abstract class SettingsView extends StackPane {
|
||||
settingsPane.getContent().add(updatePane);
|
||||
}
|
||||
|
||||
{
|
||||
previewPane = new OptionToggleButton();
|
||||
previewPane.setTitle(i18n("update.preview"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
|
||||
|
||||
settingsPane.getContent().add(previewPane);
|
||||
}
|
||||
|
||||
{
|
||||
fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocationSublist = new ComponentSublist();
|
||||
@@ -162,9 +172,8 @@ public abstract class SettingsView extends StackPane {
|
||||
));
|
||||
|
||||
{
|
||||
JFXButton cleanButton = new JFXButton(i18n("launcher.cache_directory.clean"));
|
||||
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
|
||||
cleanButton.setOnAction(e -> clearCacheDirectory());
|
||||
cleanButton.getStyleClass().add("jfx-button-border");
|
||||
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
}
|
||||
@@ -216,9 +225,8 @@ public abstract class SettingsView extends StackPane {
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
logButton.setOnAction(e -> onExportLogs());
|
||||
logButton.getStyleClass().add("jfx-button-border");
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
@@ -236,7 +244,7 @@ public abstract class SettingsView extends StackPane {
|
||||
}
|
||||
|
||||
public void openLogFolder() {
|
||||
FXUtils.openFolder(LOG.getLogFile().getParent().toFile());
|
||||
FXUtils.openFolder(LOG.getLogFile().getParent());
|
||||
}
|
||||
|
||||
protected abstract void onUpdate();
|
||||
|
||||
@@ -27,13 +27,14 @@ import javafx.scene.layout.HBox;
|
||||
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.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
@@ -68,9 +69,7 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
actions.setPadding(new Insets(8));
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton saveButton = new JFXButton(i18n("button.save"));
|
||||
saveButton.getStyleClass().add("jfx-button-raised");
|
||||
saveButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
JFXButton saveButton = FXUtils.newRaisedButton(i18n("button.save"));
|
||||
saveButton.setOnAction(e -> {
|
||||
try {
|
||||
save();
|
||||
@@ -80,9 +79,7 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("jfx-button-raised");
|
||||
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import kala.compress.utils.BoundedInputStream;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -42,8 +42,9 @@ import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -68,7 +69,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
|
||||
state.set(State.fromTitle(profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName));
|
||||
location = new SimpleStringProperty(this, "location",
|
||||
Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(".minecraft"));
|
||||
Optional.ofNullable(profile).map(Profile::getGameDir).map(FileUtils::getAbsolutePath).orElse(".minecraft"));
|
||||
|
||||
ScrollPane scroll = new ScrollPane();
|
||||
this.setCenter(scroll);
|
||||
@@ -153,13 +154,13 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
|
||||
profile.setName(txtProfileName.getText());
|
||||
profile.setUseRelativePath(toggleUseRelativePath.isSelected());
|
||||
if (StringUtils.isNotBlank(getLocation())) {
|
||||
profile.setGameDir(new File(getLocation()));
|
||||
profile.setGameDir(Path.of(getLocation()));
|
||||
}
|
||||
} else {
|
||||
if (StringUtils.isBlank(getLocation())) {
|
||||
gameDir.onExplore();
|
||||
}
|
||||
Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation()));
|
||||
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
|
||||
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
|
||||
Profiles.getProfiles().add(newProfile);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.terracotta;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaManager;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaState;
|
||||
import org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
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.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
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.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class TerracottaControllerPage extends StackPane {
|
||||
private static final String FEEDBACK_TIP = "terracotta-feedback";
|
||||
private static final ObjectProperty<TerracottaState> UI_STATE = new SimpleObjectProperty<>();
|
||||
|
||||
static {
|
||||
FXUtils.onChangeAndOperate(TerracottaManager.stateProperty(), state -> {
|
||||
if (state != null) {
|
||||
UI_STATE.set(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
/* FIXME: It's sucked to have such a long logic, containing UI for all states defined in TerracottaState, with unclear control flows.
|
||||
Consider moving UI into multiple files for each state respectively. */
|
||||
public TerracottaControllerPage() {
|
||||
TransitionPane transition = new TransitionPane();
|
||||
|
||||
ObjectProperty<String> statusProperty = new SimpleObjectProperty<>();
|
||||
DoubleProperty progressProperty = new SimpleDoubleProperty();
|
||||
ObservableList<Node> nodesProperty = FXCollections.observableList(new ArrayList<>());
|
||||
|
||||
FXUtils.applyDragListener(this, path -> {
|
||||
TerracottaState state = UI_STATE.get();
|
||||
|
||||
if (state instanceof TerracottaState.Uninitialized ||
|
||||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
|
||||
state instanceof TerracottaState.Fatal fatal && fatal.getType() == TerracottaState.Fatal.Type.NETWORK
|
||||
) {
|
||||
return Files.isReadable(path) && FileUtils.getName(path).toLowerCase(Locale.ROOT).endsWith(".tar.gz");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}, files -> {
|
||||
Path path = files.get(0);
|
||||
|
||||
if (!TerracottaManager.validate(path)) {
|
||||
Controllers.dialog(
|
||||
i18n("terracotta.from_local.file_name_mismatch", TerracottaMetadata.PACKAGE_NAME, FileUtils.getName(path)),
|
||||
i18n("message.error"),
|
||||
MessageDialogPane.MessageType.ERROR
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
TerracottaState state = UI_STATE.get(), next;
|
||||
if (state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence()) {
|
||||
if (state instanceof TerracottaState.Uninitialized uninitialized && !uninitialized.hasLegacy()) {
|
||||
Controllers.confirmWithCountdown(i18n("terracotta.confirm.desc"), i18n("terracotta.confirm.title"), 5,
|
||||
MessageDialogPane.MessageType.INFO, () -> {
|
||||
TerracottaState.Preparing s = TerracottaManager.install(path);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
}, null);
|
||||
return;
|
||||
}
|
||||
|
||||
next = TerracottaManager.install(path);
|
||||
} else if (state instanceof TerracottaState.Fatal fatal && fatal.getType() == TerracottaState.Fatal.Type.NETWORK) {
|
||||
next = TerracottaManager.recover(path);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (next != null) {
|
||||
UI_STATE.set(next);
|
||||
}
|
||||
});
|
||||
|
||||
ChangeListener<TerracottaState> listener = (_uiState, legacyState, state) -> {
|
||||
if (legacyState != null && legacyState.isUIFakeState() && !state.isUIFakeState() && legacyState.getClass() == state.getClass()) {
|
||||
return;
|
||||
}
|
||||
|
||||
progressProperty.unbind();
|
||||
|
||||
if (state instanceof TerracottaState.Bootstrap) {
|
||||
statusProperty.set(i18n("terracotta.status.bootstrap"));
|
||||
progressProperty.set(-1);
|
||||
nodesProperty.setAll();
|
||||
} else if (state instanceof TerracottaState.Uninitialized uninitialized) {
|
||||
String fork = uninitialized.hasLegacy() ? "update" : "not_exist";
|
||||
|
||||
statusProperty.set(i18n("terracotta.status.uninitialized." + fork));
|
||||
progressProperty.set(0);
|
||||
|
||||
TextFlow body = FXUtils.segmentToTextFlow(i18n("terracotta.confirm.desc"), Controllers::onHyperlinkAction);
|
||||
body.setLineSpacing(4);
|
||||
|
||||
LineButton download = LineButton.of();
|
||||
download.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
download.setTitle(i18n(String.format("terracotta.status.uninitialized.%s.title", fork)));
|
||||
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
|
||||
download.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(download, () -> {
|
||||
if (globalConfig().getTerracottaAgreementVersion() >= 1) {
|
||||
TerracottaState.Preparing s = TerracottaManager.install(null);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
|
||||
if (uninitialized.hasLegacy() && I18n.isUseChinese()) {
|
||||
Object feedback = config().getShownTips().get(FEEDBACK_TIP);
|
||||
if (!(feedback instanceof Number number) || number.intValue() < 1) {
|
||||
Controllers.confirm(i18n("terracotta.feedback.desc"), i18n("terracotta.feedback.title"), () -> {
|
||||
FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK);
|
||||
config().getShownTips().put(FEEDBACK_TIP, 1);
|
||||
}, () -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Controllers.confirmWithCountdown(i18n("terracotta.confirm.desc"), i18n("terracotta.confirm.title"), 5, MessageDialogPane.MessageType.INFO, () -> {
|
||||
globalConfig().setTerracottaAgreementVersion(1);
|
||||
TerracottaState.Preparing s = TerracottaManager.install(null);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
}, () -> {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(body, download, getThirdPartyDownloadNodes());
|
||||
} else if (state instanceof TerracottaState.Preparing) {
|
||||
statusProperty.set(i18n("terracotta.status.preparing"));
|
||||
progressProperty.bind(((TerracottaState.Preparing) state).progressProperty());
|
||||
nodesProperty.setAll(getThirdPartyDownloadNodes());
|
||||
} else if (state instanceof TerracottaState.Launching) {
|
||||
statusProperty.set(i18n("terracotta.status.launching"));
|
||||
progressProperty.set(-1);
|
||||
nodesProperty.setAll();
|
||||
} else if (state instanceof TerracottaState.Unknown) {
|
||||
statusProperty.set(i18n("terracotta.status.unknown"));
|
||||
progressProperty.set(-1);
|
||||
nodesProperty.setAll();
|
||||
} else if (state instanceof TerracottaState.Waiting) {
|
||||
statusProperty.set(i18n("terracotta.status.waiting"));
|
||||
progressProperty.set(1);
|
||||
|
||||
TextFlow flow = FXUtils.segmentToTextFlow(i18n("terracotta.confirm.desc"), Controllers::onHyperlinkAction);
|
||||
flow.setLineSpacing(4);
|
||||
|
||||
LineButton host = LineButton.of();
|
||||
host.setLeftIcon(SVG.HOST);
|
||||
host.setTitle(i18n("terracotta.status.waiting.host.title"));
|
||||
host.setSubtitle(i18n("terracotta.status.waiting.host.desc"));
|
||||
host.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(host, () -> {
|
||||
if (LauncherHelper.countMangedProcesses() >= 1) {
|
||||
TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
|
||||
if (s1 != null) {
|
||||
UI_STATE.set(s1);
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("terracotta.status.waiting.host.launch.desc"),
|
||||
i18n("terracotta.status.waiting.host.launch.title"),
|
||||
MessageDialogPane.MessageType.QUESTION
|
||||
).addAction(i18n("version.launch"), () -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
|
||||
}).addCancel(i18n("terracotta.status.waiting.host.launch.skip"), () -> {
|
||||
TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
|
||||
if (s1 != null) {
|
||||
UI_STATE.set(s1);
|
||||
}
|
||||
}).addCancel(() -> {
|
||||
}).build());
|
||||
}
|
||||
});
|
||||
|
||||
LineButton guest = LineButton.of();
|
||||
guest.setLeftIcon(SVG.ADD_CIRCLE);
|
||||
guest.setTitle(i18n("terracotta.status.waiting.guest.title"));
|
||||
guest.setSubtitle(i18n("terracotta.status.waiting.guest.desc"));
|
||||
guest.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(guest, () -> {
|
||||
Controllers.prompt(i18n("terracotta.status.waiting.guest.prompt.title"), (code, resolve, reject) -> {
|
||||
Task<TerracottaState.GuestStarting> task = TerracottaManager.setGuesting(code);
|
||||
if (task != null) {
|
||||
task.whenComplete(Schedulers.javafx(), (s, e) -> {
|
||||
if (e != null) {
|
||||
reject.accept(i18n("terracotta.status.waiting.guest.prompt.invalid"));
|
||||
} else {
|
||||
resolve.run();
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
}).setSignificance(Task.TaskSignificance.MINOR).start();
|
||||
} else {
|
||||
resolve.run();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
|
||||
LineButton feedback = LineButton.of();
|
||||
feedback.setLeftIcon(SVG.FEEDBACK);
|
||||
feedback.setTitle(i18n("terracotta.feedback.title"));
|
||||
feedback.setSubtitle(i18n("terracotta.feedback.desc"));
|
||||
feedback.setRightIcon(SVG.OPEN_IN_NEW);
|
||||
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
|
||||
nodesProperty.setAll(flow, host, guest, feedback);
|
||||
} else {
|
||||
nodesProperty.setAll(flow, host, guest);
|
||||
}
|
||||
} else if (state instanceof TerracottaState.HostScanning) {
|
||||
statusProperty.set(i18n("terracotta.status.scanning"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
TextFlow body = FXUtils.segmentToTextFlow(i18n("terracotta.status.scanning.desc"), Controllers::onHyperlinkAction);
|
||||
body.setLineSpacing(4);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.scanning.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(body, room);
|
||||
} else if (state instanceof TerracottaState.HostStarting) {
|
||||
statusProperty.set(i18n("terracotta.status.host_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.host_starting.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(room);
|
||||
} else if (state instanceof TerracottaState.HostOK hostOK) {
|
||||
if (hostOK.isForkOf(legacyState)) {
|
||||
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
|
||||
profileUI.updateProfiles(hostOK.getProfiles());
|
||||
} else { // Should NOT happen
|
||||
nodesProperty.add(new PlayerProfileUI(hostOK.getProfiles()));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
String cs = hostOK.getCode();
|
||||
copyCode(cs);
|
||||
|
||||
statusProperty.set(i18n("terracotta.status.host_ok"));
|
||||
progressProperty.set(1);
|
||||
|
||||
VBox code = new VBox(4);
|
||||
code.setAlignment(Pos.CENTER);
|
||||
{
|
||||
Label desc = new Label(i18n("terracotta.status.host_ok.code"));
|
||||
desc.setMouseTransparent(true);
|
||||
|
||||
Label label = new Label(cs);
|
||||
label.setMouseTransparent(true);
|
||||
label.setStyle("-fx-font-size: 24");
|
||||
label.setAlignment(Pos.CENTER);
|
||||
VBox.setMargin(label, new Insets(10, 0, 10, 0));
|
||||
|
||||
code.getChildren().setAll(desc, label);
|
||||
}
|
||||
code.setCursor(Cursor.HAND);
|
||||
FXUtils.onClicked(code, () -> copyCode(cs));
|
||||
|
||||
LineButton copy = LineButton.of();
|
||||
copy.setLeftIcon(SVG.CONTENT_COPY);
|
||||
copy.setTitle(i18n("terracotta.status.host_ok.code.copy"));
|
||||
copy.setSubtitle(i18n("terracotta.status.host_ok.code.desc"));
|
||||
FXUtils.onClicked(copy, () -> copyCode(cs));
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.host_ok.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
if (hostOK.getProfiles().isEmpty()) {
|
||||
nodesProperty.setAll(code, copy, back);
|
||||
} else {
|
||||
nodesProperty.setAll(code, copy, back, new PlayerProfileUI(hostOK.getProfiles()));
|
||||
}
|
||||
}
|
||||
} else if (state instanceof TerracottaState.GuestStarting) {
|
||||
statusProperty.set(i18n("terracotta.status.guest_starting"));
|
||||
progressProperty.set(-1);
|
||||
|
||||
LineButton room = LineButton.of();
|
||||
room.setLeftIcon(SVG.ARROW_BACK);
|
||||
room.setTitle(i18n("terracotta.back"));
|
||||
room.setSubtitle(i18n("terracotta.status.guest_starting.back"));
|
||||
FXUtils.onClicked(room, () -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(room);
|
||||
} else if (state instanceof TerracottaState.GuestOK guestOK) {
|
||||
if (guestOK.isForkOf(legacyState)) {
|
||||
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
|
||||
profileUI.updateProfiles(guestOK.getProfiles());
|
||||
} else { // Should NOT happen
|
||||
nodesProperty.add(new PlayerProfileUI(guestOK.getProfiles()));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
statusProperty.set(i18n("terracotta.status.guest_ok"));
|
||||
progressProperty.set(1);
|
||||
|
||||
LineButton tutorial = LineButton.of();
|
||||
tutorial.setTitle(i18n("terracotta.status.guest_ok.title"));
|
||||
tutorial.setSubtitle(i18n("terracotta.status.guest_ok.desc", guestOK.getUrl()));
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.guest_ok.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
if (guestOK.getProfiles().isEmpty()) {
|
||||
nodesProperty.setAll(tutorial, back);
|
||||
} else {
|
||||
nodesProperty.setAll(tutorial, back, new PlayerProfileUI(guestOK.getProfiles()));
|
||||
}
|
||||
}
|
||||
} else if (state instanceof TerracottaState.Exception exception) {
|
||||
statusProperty.set(i18n("terracotta.status.exception.desc." + exception.getType().name().toLowerCase(Locale.ROOT)));
|
||||
progressProperty.set(1);
|
||||
nodesProperty.setAll();
|
||||
|
||||
LineButton back = LineButton.of();
|
||||
back.setLeftIcon(SVG.ARROW_BACK);
|
||||
back.setTitle(i18n("terracotta.back"));
|
||||
back.setSubtitle(i18n("terracotta.status.exception.back"));
|
||||
FXUtils.onClicked(back, () -> {
|
||||
TerracottaState.Waiting s = TerracottaManager.setWaiting();
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
SpinnerPane exportLog = new SpinnerPane();
|
||||
LineButton exportLogInner = LineButton.of();
|
||||
exportLogInner.setLeftIcon(SVG.OUTPUT);
|
||||
exportLogInner.setTitle(i18n("terracotta.export_log"));
|
||||
exportLogInner.setSubtitle(i18n("terracotta.export_log.desc"));
|
||||
exportLog.setContent(exportLogInner);
|
||||
exportLog.getProperties().put("ComponentList.noPadding", true);
|
||||
// FIXME: SpinnerPane loses its content width in loading state.
|
||||
exportLog.minHeightProperty().bind(back.heightProperty());
|
||||
|
||||
FXUtils.onClicked(exportLogInner, () -> {
|
||||
exportLog.setLoading(true);
|
||||
|
||||
TerracottaManager.exportLogs().thenAcceptAsync(Schedulers.io(), data -> {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path path = Path.of("terracotta-log-" + LocalDateTime.now().format(
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")
|
||||
) + ".zip").toAbsolutePath();
|
||||
try (Zipper zipper = new Zipper(path)) {
|
||||
zipper.putTextFile(data, StandardCharsets.UTF_8, "terracotta.log");
|
||||
try (OutputStream os = zipper.putStream("hmcl-latest.log")) {
|
||||
Logger.LOG.exportLogs(os);
|
||||
}
|
||||
}
|
||||
FXUtils.showFileInExplorer(path);
|
||||
}).thenRunAsync(
|
||||
() -> Thread.sleep(3000)
|
||||
).whenComplete(
|
||||
Schedulers.javafx(),
|
||||
e -> exportLog.setLoading(false)
|
||||
).start();
|
||||
});
|
||||
|
||||
nodesProperty.setAll(back, exportLog);
|
||||
} else if (state instanceof TerracottaState.Fatal fatal) {
|
||||
String message = i18n("terracotta.status.fatal." + fatal.getType().name().toLowerCase(Locale.ROOT));
|
||||
|
||||
statusProperty.set(message);
|
||||
progressProperty.set(1);
|
||||
|
||||
if (fatal.isRecoverable()) {
|
||||
LineButton retry = LineButton.of();
|
||||
retry.setLeftIcon(SVG.RESTORE);
|
||||
retry.setTitle(i18n("terracotta.status.fatal.retry"));
|
||||
retry.setSubtitle(message);
|
||||
FXUtils.onClicked(retry, () -> {
|
||||
TerracottaState s = TerracottaManager.recover(null);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
});
|
||||
|
||||
if (fatal.getType() == TerracottaState.Fatal.Type.NETWORK) {
|
||||
nodesProperty.setAll(retry, getThirdPartyDownloadNodes());
|
||||
} else {
|
||||
nodesProperty.setAll(retry);
|
||||
}
|
||||
} else {
|
||||
nodesProperty.setAll();
|
||||
}
|
||||
} else {
|
||||
throw new AssertionError(state.getClass().getName());
|
||||
}
|
||||
|
||||
ComponentList components = new ComponentList();
|
||||
{
|
||||
VBox statusPane = new VBox(8);
|
||||
VBox.setMargin(statusPane, new Insets(0, 0, 0, 4));
|
||||
{
|
||||
Label status = new Label();
|
||||
status.textProperty().bind(statusProperty);
|
||||
JFXProgressBar progress = new JFXProgressBar();
|
||||
progress.progressProperty().bind(progressProperty);
|
||||
progress.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
statusPane.getChildren().setAll(status, progress);
|
||||
}
|
||||
|
||||
ObservableList<Node> children = components.getContent();
|
||||
children.add(statusPane);
|
||||
children.addAll(nodesProperty);
|
||||
}
|
||||
|
||||
transition.setContent(components, ContainerAnimations.SWIPE_LEFT_FADE_SHORT);
|
||||
};
|
||||
listener.changed(UI_STATE, null, UI_STATE.get());
|
||||
holder.add(listener);
|
||||
UI_STATE.addListener(new WeakChangeListener<>(listener));
|
||||
|
||||
VBox content = new VBox(10);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("terracotta.status")), transition);
|
||||
content.setPadding(new Insets(10));
|
||||
content.setFillWidth(true);
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
private ComponentList getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
locals.setComponentPadding(false);
|
||||
|
||||
LineButton header = LineButton.of(false);
|
||||
header.setLeftImage(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
header.setTitle(i18n("terracotta.from_local.title"));
|
||||
header.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
locals.setHeaderLeft(header);
|
||||
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
|
||||
HBox node = new HBox();
|
||||
node.setAlignment(Pos.CENTER_LEFT);
|
||||
node.setPadding(new Insets(10, 16, 10, 16));
|
||||
|
||||
Label description = new Label(link.description().getText(I18n.getLocale().getCandidateLocales()));
|
||||
HBox placeholder = new HBox();
|
||||
HBox.setHgrow(placeholder, Priority.ALWAYS);
|
||||
Node icon = SVG.OPEN_IN_NEW.createIcon(Theme.blackFill(), 16);
|
||||
node.getChildren().setAll(description, placeholder, icon);
|
||||
|
||||
String url = link.link();
|
||||
RipplerContainer container = new RipplerContainer(node);
|
||||
container.setOnMouseClicked(ev -> Controllers.dialog(
|
||||
i18n("terracotta.from_local.guide", TerracottaMetadata.PACKAGE_NAME),
|
||||
i18n("message.info"), MessageDialogPane.MessageType.INFO,
|
||||
() -> FXUtils.openLink(url)
|
||||
));
|
||||
container.getProperties().put("ComponentList.noPadding", true);
|
||||
locals.getContent().add(container);
|
||||
}
|
||||
return locals;
|
||||
}
|
||||
|
||||
private void copyCode(String code) {
|
||||
FXUtils.copyText(code, i18n("terracotta.status.host_ok.code.copy.toast"));
|
||||
}
|
||||
|
||||
private static final class LineButton extends RipplerContainer {
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
|
||||
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "");
|
||||
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", "");
|
||||
|
||||
public static LineButton of() {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
public static LineButton of(boolean padding) {
|
||||
HBox container = new HBox();
|
||||
if (padding) {
|
||||
container.setPadding(new Insets(10, 16, 10, 16));
|
||||
}
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
container.setCursor(Cursor.HAND);
|
||||
container.setSpacing(16);
|
||||
|
||||
LineButton button = new LineButton(container);
|
||||
VBox spacing = new VBox();
|
||||
HBox.setHgrow(spacing, Priority.ALWAYS);
|
||||
button.holder.add(FXUtils.observeWeak(() -> {
|
||||
List<Node> nodes = new ArrayList<>(4);
|
||||
Node left = button.left.get();
|
||||
if (left != null) {
|
||||
nodes.add(left);
|
||||
}
|
||||
|
||||
{
|
||||
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
|
||||
VBox middle = new VBox();
|
||||
middle.getStyleClass().add("two-line-list-item");
|
||||
middle.setMouseTransparent(true);
|
||||
{
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
{
|
||||
Label lblTitle = new Label(button.title.get());
|
||||
lblTitle.getStyleClass().add("title");
|
||||
firstLine.getChildren().setAll(lblTitle);
|
||||
}
|
||||
|
||||
HBox secondLine = new HBox();
|
||||
{
|
||||
Text text = new Text(button.subTitle.get());
|
||||
text.setFill(new Color(0, 0, 0, 0.5));
|
||||
|
||||
TextFlow lblSubtitle = new TextFlow(text);
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
secondLine.getChildren().setAll(lblSubtitle);
|
||||
}
|
||||
|
||||
middle.getChildren().setAll(firstLine, secondLine);
|
||||
}
|
||||
nodes.add(middle);
|
||||
}
|
||||
|
||||
nodes.add(spacing);
|
||||
|
||||
Node right = button.right.get();
|
||||
if (right != null) {
|
||||
nodes.add(right);
|
||||
}
|
||||
|
||||
container.getChildren().setAll(nodes);
|
||||
}, button.title, button.subTitle, button.left, button.right));
|
||||
button.getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private LineButton(Node container) {
|
||||
super(container);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subTitle.set(subtitle);
|
||||
}
|
||||
|
||||
public void setLeftImage(Image left) {
|
||||
this.left.set(new ImageView(left));
|
||||
}
|
||||
|
||||
public void setLeftIcon(SVG left) {
|
||||
this.left.set(left.createIcon(Theme.blackFill(), 28));
|
||||
}
|
||||
|
||||
public void setRightIcon(SVG right) {
|
||||
this.right.set(right.createIcon(Theme.blackFill(), 28));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PlayerProfileUI extends VBox {
|
||||
private final TransitionPane transition;
|
||||
|
||||
public PlayerProfileUI(List<TerracottaProfile> profiles) {
|
||||
super(8);
|
||||
VBox.setMargin(this, new Insets(0, 0, 0, 4));
|
||||
{
|
||||
Label status = new Label();
|
||||
status.setText(i18n("terracotta.player_list"));
|
||||
|
||||
transition = new TransitionPane();
|
||||
getChildren().setAll(status, transition);
|
||||
|
||||
updateProfiles(profiles);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProfiles(List<TerracottaProfile> profiles) {
|
||||
VBox pane = new VBox(8);
|
||||
|
||||
for (TerracottaProfile profile : profiles) {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setTitle(profile.getName());
|
||||
item.setSubtitle(profile.getVendor());
|
||||
item.addTag(i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)));
|
||||
pane.getChildren().add(item);
|
||||
}
|
||||
|
||||
this.transition.setContent(pane, ContainerAnimations.SWIPE_LEFT_FADE_SHORT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.terracotta;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
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.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.main.MainPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("terracotta.terracotta")));
|
||||
private final TabHeader tab;
|
||||
private final TabHeader.Tab<TerracottaControllerPage> statusPage = new TabHeader.Tab<>("statusPage");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private ChangeListener<String> instanceChangeListenerHolder;
|
||||
|
||||
public TerracottaPage() {
|
||||
statusPage.setNodeSupplier(TerracottaControllerPage::new);
|
||||
tab = new TabHeader(statusPage);
|
||||
tab.select(statusPage);
|
||||
|
||||
transitionPane.setContent(statusPage.getNode(), ContainerAnimations.NONE);
|
||||
FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
BorderPane left = new BorderPane();
|
||||
FXUtils.setLimitWidth(left, 200);
|
||||
VBox.setVgrow(left, Priority.ALWAYS);
|
||||
setLeft(left);
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(tab, statusPage, i18n("terracotta.status"), SVG.TUNE);
|
||||
left.setTop(sideBar);
|
||||
|
||||
AdvancedListBox toolbar = new AdvancedListBox()
|
||||
.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
|
||||
}, item -> {
|
||||
instanceChangeListenerHolder = FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(),
|
||||
instanceName -> item.setSubtitle(StringUtils.isNotBlank(instanceName) ? instanceName : i18n("version.empty"))
|
||||
);
|
||||
|
||||
MainPage mainPage = Controllers.getRootPage().getMainPage();
|
||||
FXUtils.onScroll(item, mainPage.getVersions(), list -> {
|
||||
String currentId = mainPage.getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, it -> mainPage.getProfile().setSelectedVersion(it.getId()));
|
||||
})
|
||||
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK))
|
||||
.addNavigationDrawerItem(i18n("terracotta.easytier"), SVG.HOST, () -> FXUtils.openLink("https://easytier.cn/"));
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
left.setBottom(toolbar);
|
||||
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
tab.onPageShown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
tab.onPageHidden();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
private final JFXTextField txtPreLaunchCommand;
|
||||
private final JFXTextField txtPostExitCommand;
|
||||
private final OptionToggleButton noJVMArgsPane;
|
||||
private final OptionToggleButton noOptimizingJVMArgsPane;
|
||||
private final OptionToggleButton noGameCheckPane;
|
||||
private final OptionToggleButton noJVMCheckPane;
|
||||
private final OptionToggleButton noNativesPatchPane;
|
||||
@@ -183,6 +184,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
noJVMArgsPane = new OptionToggleButton();
|
||||
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
|
||||
|
||||
noOptimizingJVMArgsPane = new OptionToggleButton();
|
||||
noOptimizingJVMArgsPane.setTitle(i18n("settings.advanced.no_optimizing_jvm_args"));
|
||||
noOptimizingJVMArgsPane.disableProperty().bind(noJVMArgsPane.selectedProperty());
|
||||
|
||||
noGameCheckPane = new OptionToggleButton();
|
||||
noGameCheckPane.setTitle(i18n("settings.advanced.dont_check_game_completeness"));
|
||||
|
||||
@@ -199,7 +204,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noGameCheckPane,
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
noJVMCheckPane, noNativesPatchPane
|
||||
);
|
||||
|
||||
@@ -235,6 +240,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
noOptimizingJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noOptimizingJVMArgsProperty());
|
||||
noNativesPatchPane.selectedProperty().bindBidirectional(versionSetting.notPatchNativesProperty());
|
||||
useNativeGLFWPane.selectedProperty().bindBidirectional(versionSetting.useNativeGLFWProperty());
|
||||
useNativeOpenALPane.selectedProperty().bindBidirectional(versionSetting.useNativeOpenALProperty());
|
||||
@@ -245,12 +251,12 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
String nativesDirName = "natives-" + Platform.SYSTEM_PLATFORM;
|
||||
if (versionId == null) {
|
||||
return String.format("%s/%s/%s",
|
||||
profile.getRepository().getBaseDirectory().toPath().resolve("versions").toAbsolutePath().normalize(),
|
||||
profile.getRepository().getBaseDirectory().resolve("versions").toAbsolutePath().normalize(),
|
||||
i18n("settings.advanced.natives_directory.default.version_id"),
|
||||
nativesDirName
|
||||
);
|
||||
} else {
|
||||
return profile.getRepository().getVersionRoot(versionId).toPath()
|
||||
return profile.getRepository().getVersionRoot(versionId)
|
||||
.toAbsolutePath().normalize()
|
||||
.resolve(nativesDirName)
|
||||
.toString();
|
||||
@@ -276,6 +282,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
noOptimizingJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noOptimizingJVMArgsProperty());
|
||||
noNativesPatchPane.selectedProperty().unbindBidirectional(versionSetting.notPatchNativesProperty());
|
||||
useNativeGLFWPane.selectedProperty().unbindBidirectional(versionSetting.useNativeGLFWProperty());
|
||||
useNativeOpenALPane.selectedProperty().unbindBidirectional(versionSetting.useNativeOpenALProperty());
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -54,9 +53,9 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
|
||||
}
|
||||
|
||||
private void installSingleDatapack(File datapack) {
|
||||
private void installSingleDatapack(Path datapack) {
|
||||
try {
|
||||
Datapack zip = new Datapack(datapack.toPath());
|
||||
Datapack zip = new Datapack(datapack);
|
||||
zip.loadFromZip();
|
||||
zip.installTo(worldDir);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
@@ -82,7 +81,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("datapack.choose_datapack"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
|
||||
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null)
|
||||
res.forEach(this::installSingleDatapack);
|
||||
|
||||
@@ -532,6 +532,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
|
||||
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
}
|
||||
@@ -547,7 +550,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
.map(category -> getSkinnable().getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,11 +49,12 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -172,14 +173,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
fileChooser.setTitle(i18n("button.save_as"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("file"), "*." + extension));
|
||||
fileChooser.setInitialFileName(file.getFile().getFilename());
|
||||
File dest = fileChooser.showSaveDialog(Controllers.getStage());
|
||||
Path dest = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage()));
|
||||
if (dest == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(() -> {
|
||||
var task = new FileDownloadTask(file.getFile().getUrl(), dest.toPath(), file.getFile().getIntegrityCheck());
|
||||
var task = new FileDownloadTask(file.getFile().getUrl(), dest, file.getFile().getIntegrityCheck());
|
||||
task.setName(file.getName());
|
||||
return task;
|
||||
}),
|
||||
@@ -218,8 +219,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
|
||||
{
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true));
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
|
||||
|
||||
@@ -340,6 +343,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
@@ -357,12 +362,12 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
.map(page::getLocalizedCategory)
|
||||
.forEach(content::addTag);
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
} else {
|
||||
content.setTitle(i18n("mods.broken_dependency.title"));
|
||||
content.setSubtitle(i18n("mods.broken_dependency.desc"));
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@8x.png", 40, 40, true, true));
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@4x.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -137,12 +137,12 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
public void installOffline() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
private void doInstallOffline(File file) {
|
||||
Task<?> task = profile.getDependency().installLibraryAsync(version, file.toPath())
|
||||
private void doInstallOffline(Path file) {
|
||||
Task<?> task = profile.getDependency().installLibraryAsync(version, file)
|
||||
.thenComposeAsync(profile.getRepository()::saveAsync)
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -27,6 +26,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -39,30 +39,33 @@ import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
|
||||
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private ModManager modManager;
|
||||
private LibraryAnalyzer libraryAnalyzer;
|
||||
private Profile profile;
|
||||
private String versionId;
|
||||
private String instanceId;
|
||||
private String gameVersion;
|
||||
|
||||
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
|
||||
public ModListPage() {
|
||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||
mods.forEach(it -> {
|
||||
try {
|
||||
modManager.addMod(it.toPath());
|
||||
modManager.addMod(it);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
LOG.warning("Unable to parse mod file " + it, e);
|
||||
}
|
||||
@@ -83,41 +86,89 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
@Override
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.versionId = id;
|
||||
this.instanceId = id;
|
||||
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
Version resolved = repository.getResolvedPreservingPatchesVersion(id);
|
||||
libraryAnalyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null));
|
||||
modded.set(libraryAnalyzer.hasModLoader());
|
||||
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
|
||||
modded.set(analyzer.hasModLoader());
|
||||
loadMods(profile.getRepository().getModManager(id));
|
||||
}
|
||||
|
||||
private CompletableFuture<?> loadMods(ModManager modManager) {
|
||||
private void loadMods(ModManager modManager) {
|
||||
setLoading(true);
|
||||
|
||||
this.modManager = modManager;
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
lock.lock();
|
||||
try {
|
||||
synchronized (ModListPage.this) {
|
||||
runInFX(() -> loadingProperty().set(true));
|
||||
modManager.refreshMods();
|
||||
return new ArrayList<>(modManager.getMods());
|
||||
}
|
||||
modManager.refreshMods();
|
||||
return modManager.getMods().stream().map(ModListPageSkin.ModInfoObject::new).toList();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}, Schedulers.defaultScheduler()).whenCompleteAsync((list, exception) -> {
|
||||
loadingProperty().set(false);
|
||||
if (exception == null)
|
||||
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).sorted().collect(Collectors.toList()));
|
||||
else
|
||||
getProperties().remove(ModListPage.class);
|
||||
}, Platform::runLater);
|
||||
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
|
||||
updateSupportedLoaders(modManager);
|
||||
|
||||
if (exception == null) {
|
||||
getItems().setAll(list);
|
||||
} else {
|
||||
LOG.warning("Failed to load mods", exception);
|
||||
getItems().clear();
|
||||
}
|
||||
setLoading(false);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private void updateSupportedLoaders(ModManager modManager) {
|
||||
supportedLoaders.clear();
|
||||
|
||||
LibraryAnalyzer analyzer = modManager.getLibraryAnalyzer();
|
||||
if (analyzer == null) {
|
||||
Collections.addAll(supportedLoaders, ModLoaderType.values());
|
||||
return;
|
||||
}
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
if (type.isModLoader() && analyzer.has(type)) {
|
||||
ModLoaderType modLoaderType = type.getModLoaderType();
|
||||
if (modLoaderType != null) {
|
||||
supportedLoaders.add(modLoaderType);
|
||||
|
||||
if (modLoaderType == ModLoaderType.CLEANROOM)
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGED);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED)
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
}
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.choose_mod"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res == null) return;
|
||||
|
||||
@@ -126,13 +177,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
List<String> failed = new ArrayList<>();
|
||||
|
||||
Task.runAsync(() -> {
|
||||
for (File file : res) {
|
||||
for (Path file : res) {
|
||||
try {
|
||||
modManager.addMod(file.toPath());
|
||||
succeeded.add(file.getName());
|
||||
modManager.addMod(file);
|
||||
succeeded.add(FileUtils.getName(file));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to add mod " + file, e);
|
||||
failed.add(file.getName());
|
||||
failed.add(FileUtils.getName(file));
|
||||
|
||||
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
|
||||
}
|
||||
@@ -148,7 +199,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
try {
|
||||
modManager.removeMods(selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
@@ -160,14 +211,14 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
}
|
||||
|
||||
public void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.forEach(info -> info.setActive(true));
|
||||
}
|
||||
|
||||
public void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
@@ -175,13 +226,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void openModFolder() {
|
||||
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(versionId), "mods"));
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
|
||||
}
|
||||
|
||||
public void checkUpdates() {
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(versionId);
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
if (gameVersion.isPresent()) {
|
||||
return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods());
|
||||
}
|
||||
@@ -199,7 +250,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
.withStagesHint(Collections.singletonList("mods.check_updates")),
|
||||
i18n("update.checking"), TaskCancellationAction.NORMAL);
|
||||
|
||||
if (profile.getRepository().isModpack(versionId)) {
|
||||
if (profile.getRepository().isModpack(instanceId)) {
|
||||
Controllers.confirm(
|
||||
i18n("mods.update_modpack_mod.warning"), null,
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
@@ -210,7 +261,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void download() {
|
||||
Controllers.getDownloadPage().showModDownloads().selectVersion(versionId);
|
||||
Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId);
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
@@ -239,7 +290,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
return this.profile;
|
||||
}
|
||||
|
||||
public String getVersionId() {
|
||||
return this.versionId;
|
||||
public String getInstanceId() {
|
||||
return this.instanceId;
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -36,6 +36,7 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
@@ -45,6 +46,7 @@ import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
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.Controllers;
|
||||
@@ -53,35 +55,32 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
private final HBox searchBar;
|
||||
@@ -94,6 +93,9 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
// FXThread
|
||||
private boolean isSearching = false;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final ChangeListener<Boolean> holder;
|
||||
|
||||
ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@@ -105,6 +107,12 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
|
||||
this.holder = FXUtils.onWeakChange(skinnable.loadingProperty(), loading -> {
|
||||
if (!loading) {
|
||||
listView.scrollTo(0);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
|
||||
@@ -272,139 +280,180 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|| predicate.test(modInfo.getGameVersion())
|
||||
|| predicate.test(modInfo.getId())
|
||||
|| predicate.test(Objects.toString(modInfo.getModLoaderType()))
|
||||
|| predicate.test((item.getMod() != null ? item.getMod().getDisplayName() : null))) {
|
||||
|| predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) {
|
||||
listView.getItems().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
|
||||
static final class ModInfoObject {
|
||||
private final BooleanProperty active;
|
||||
private final LocalModFile localModFile;
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final ModTranslations.Mod mod;
|
||||
private final @Nullable ModTranslations.Mod modTranslations;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
StringBuilder title = new StringBuilder(localModFile.getName());
|
||||
if (isNotBlank(localModFile.getVersion()))
|
||||
title.append(" ").append(localModFile.getVersion());
|
||||
this.title = title.toString();
|
||||
|
||||
StringBuilder message = new StringBuilder(localModFile.getFileName());
|
||||
if (isNotBlank(localModFile.getGameVersion()))
|
||||
message.append(", ").append(i18n("mods.game.version")).append(": ").append(localModFile.getGameVersion());
|
||||
if (isNotBlank(localModFile.getAuthors()))
|
||||
message.append(", ").append(i18n("archive.author")).append(": ").append(localModFile.getAuthors());
|
||||
this.message = message.toString();
|
||||
|
||||
this.mod = ModTranslations.MOD.getModById(localModFile.getId());
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return message;
|
||||
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
|
||||
}
|
||||
|
||||
LocalModFile getModInfo() {
|
||||
return localModFile;
|
||||
}
|
||||
|
||||
public ModTranslations.Mod getMod() {
|
||||
return mod;
|
||||
public @Nullable ModTranslations.Mod getModTranslations() {
|
||||
return modTranslations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ModListPageSkin.ModInfoObject o) {
|
||||
return localModFile.getFileName().toLowerCase().compareTo(o.localModFile.getFileName().toLowerCase());
|
||||
@FXThread
|
||||
private Image loadIcon() {
|
||||
List<String> iconPaths = new ArrayList<>();
|
||||
|
||||
if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) {
|
||||
iconPaths.add(this.localModFile.getLogoPath());
|
||||
}
|
||||
|
||||
iconPaths.addAll(List.of(
|
||||
"icon.png",
|
||||
"logo.png",
|
||||
"mod_logo.png",
|
||||
"pack.png",
|
||||
"logoFile.png",
|
||||
"assets/icon.png",
|
||||
"assets/logo.png",
|
||||
"assets/mod_icon.png",
|
||||
"assets/mod_logo.png",
|
||||
"META-INF/icon.png",
|
||||
"META-INF/logo.png",
|
||||
"META-INF/mod_icon.png",
|
||||
"textures/icon.png",
|
||||
"textures/logo.png",
|
||||
"textures/mod_icon.png",
|
||||
"resources/icon.png",
|
||||
"resources/logo.png",
|
||||
"resources/mod_icon.png"
|
||||
));
|
||||
|
||||
String modId = this.localModFile.getId();
|
||||
if (StringUtils.isNotBlank(modId)) {
|
||||
iconPaths.addAll(List.of(
|
||||
"assets/" + modId + "/icon.png",
|
||||
"assets/" + modId + "/logo.png",
|
||||
"assets/" + modId.replace("-", "") + "/icon.png",
|
||||
"assets/" + modId.replace("-", "") + "/logo.png",
|
||||
modId + ".png",
|
||||
modId + "-logo.png",
|
||||
modId + "-icon.png",
|
||||
modId + "_logo.png",
|
||||
modId + "_icon.png",
|
||||
"textures/" + modId + "/icon.png",
|
||||
"textures/" + modId + "/logo.png",
|
||||
"resources/" + modId + "/icon.png",
|
||||
"resources/" + modId + "/logo.png"
|
||||
));
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
|
||||
for (String path : iconPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
if (Files.exists(iconPath)) {
|
||||
Image image = FXUtils.loadImage(iconPath, 80, 80, true, true);
|
||||
if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageView.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageView.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
imageView.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
}
|
||||
|
||||
class ModInfoDialog extends JFXDialogLayout {
|
||||
final class ModInfoDialog extends JFXDialogLayout {
|
||||
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
Task.supplyAsync(() -> {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
|
||||
String logoPath = modInfo.getModInfo().getLogoPath();
|
||||
if (StringUtils.isNotBlank(logoPath)) {
|
||||
Path iconPath = fs.getPath(logoPath);
|
||||
if (Files.exists(iconPath)) {
|
||||
try {
|
||||
Image image = FXUtils.loadImage(iconPath, 40, 40, true, true);
|
||||
if (!image.isError() && image.getWidth() == image.getHeight())
|
||||
return image;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load image " + logoPath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> defaultPaths = new ArrayList<>(Arrays.asList(
|
||||
"icon.png",
|
||||
"logo.png",
|
||||
"mod_logo.png",
|
||||
"pack.png",
|
||||
"logoFile.png"
|
||||
));
|
||||
|
||||
String id = modInfo.getModInfo().getId();
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
defaultPaths.addAll(Arrays.asList(
|
||||
"assets/" + id + "/icon.png",
|
||||
"assets/" + id.replace("-", "") + "/icon.png",
|
||||
id + ".png",
|
||||
id + "-logo.png",
|
||||
id + "-icon.png",
|
||||
id + "_logo.png",
|
||||
id + "_icon.png"
|
||||
));
|
||||
}
|
||||
|
||||
for (String path : defaultPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
if (Files.exists(iconPath)) {
|
||||
Image image = FXUtils.loadImage(iconPath, 40, 40, true, true);
|
||||
if (!image.isError() && image.getWidth() == image.getHeight())
|
||||
return image;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load icon", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}).whenComplete(Schedulers.javafx(), (image, exception) -> {
|
||||
if (image != null) {
|
||||
imageView.setImage(image);
|
||||
} else {
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
|
||||
}
|
||||
}).start();
|
||||
FXUtils.limitSize(imageView, 40, 40);
|
||||
modInfo.loadIcon(imageView, null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
title.addTag(modInfo.getModInfo().getVersion());
|
||||
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
|
||||
title.setTitle(modInfo.getModTranslations().getDisplayName());
|
||||
else
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
|
||||
StringJoiner subtitle = new StringJoiner(" | ");
|
||||
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
|
||||
subtitle.add(modInfo.getModInfo().getGameVersion());
|
||||
}
|
||||
title.setSubtitle(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
subtitle.add(modInfo.getModInfo().getVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
|
||||
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
}
|
||||
title.setSubtitle(subtitle.toString());
|
||||
|
||||
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
|
||||
setHeading(titleContainer);
|
||||
|
||||
Label description = new Label(modInfo.getModInfo().getDescription().toString());
|
||||
description.setWrapText(true);
|
||||
FXUtils.copyOnDoubleClick(description);
|
||||
setBody(description);
|
||||
|
||||
ScrollPane descriptionPane = new ScrollPane(description);
|
||||
FXUtils.smoothScrolling(descriptionPane);
|
||||
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = stage.getHeight() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
setBody(descriptionPane);
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
|
||||
@@ -454,7 +503,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
Controllers.navigate(new DownloadPage(
|
||||
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteMod,
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getVersionId()),
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
|
||||
(profile, version, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
|
||||
));
|
||||
});
|
||||
@@ -477,7 +526,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
getActions().add(officialPageButton);
|
||||
}
|
||||
|
||||
if (modInfo.getMod() == null || StringUtils.isBlank(modInfo.getMod().getMcmod())) {
|
||||
if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
|
||||
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
|
||||
searchButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
@@ -492,7 +541,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
|
||||
mcmodButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getMod()));
|
||||
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
|
||||
});
|
||||
getActions().add(mcmodButton);
|
||||
}
|
||||
@@ -511,16 +560,23 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
ImageView imageView = new ImageView();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton restoreButton = new JFXButton();
|
||||
JFXButton infoButton = new JFXButton();
|
||||
JFXButton revealButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
Tooltip warningTooltip;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
|
||||
this.getStyleClass().add("mod-info-list-cell");
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -528,6 +584,11 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
imageView.setFitWidth(24);
|
||||
imageView.setFitHeight(24);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24), 24, 24));
|
||||
|
||||
@@ -539,7 +600,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.INFO.createIcon(Theme.blackFill(), 24), 24, 24));
|
||||
|
||||
container.getChildren().setAll(checkBox, content, restoreButton, revealButton, infoButton);
|
||||
container.getChildren().setAll(checkBox, imageView, content, restoreButton, revealButton, infoButton);
|
||||
|
||||
StackPane.setMargin(container, new Insets(8));
|
||||
getContainer().getChildren().setAll(container);
|
||||
@@ -547,50 +608,113 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||
pseudoClassStateChanged(WARNING, false);
|
||||
if (warningTooltip != null) {
|
||||
Tooltip.uninstall(this, warningTooltip);
|
||||
warningTooltip = null;
|
||||
}
|
||||
|
||||
if (empty) return;
|
||||
content.setTitle(dataItem.getTitle());
|
||||
|
||||
List<String> warning = new ArrayList<>();
|
||||
|
||||
content.getTags().clear();
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE:
|
||||
content.addTag(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTag(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTag(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
|
||||
LocalModFile modInfo = dataItem.getModInfo();
|
||||
ModTranslations.Mod modTranslations = dataItem.getModTranslations();
|
||||
|
||||
ModLoaderType modLoaderType = modInfo.getModLoaderType();
|
||||
|
||||
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
|
||||
|
||||
String displayName = modInfo.getName();
|
||||
if (modTranslations != null && I18n.isUseChinese()) {
|
||||
String chineseName = modTranslations.getName();
|
||||
if (StringUtils.containsChinese(chineseName)) {
|
||||
if (StringUtils.containsEmoji(chineseName)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
chineseName.codePoints().forEach(ch -> {
|
||||
if (ch < 0x1F300 || ch > 0x1FAFF)
|
||||
builder.appendCodePoint(ch);
|
||||
});
|
||||
|
||||
chineseName = builder.toString().trim();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) {
|
||||
displayName = displayName + " (" + chineseName + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dataItem.getMod() != null && I18n.isUseChinese()) {
|
||||
content.addTag(dataItem.getMod().getDisplayName());
|
||||
content.setTitle(displayName);
|
||||
|
||||
StringJoiner joiner = new StringJoiner(" | ");
|
||||
if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId()))
|
||||
joiner.add(modInfo.getId());
|
||||
|
||||
joiner.add(FileUtils.getName(modInfo.getFile()));
|
||||
|
||||
content.setSubtitle(joiner.toString());
|
||||
|
||||
if (modLoaderType == ModLoaderType.UNKNOWN) {
|
||||
content.addTagWarning(i18n("mods.unknown"));
|
||||
} else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE:
|
||||
content.addTagWarning(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTagWarning(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTagWarning(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
|
||||
String modVersion = modInfo.getVersion();
|
||||
if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) {
|
||||
content.addTag(modVersion);
|
||||
}
|
||||
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setOnAction(e -> {
|
||||
menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream()
|
||||
menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream()
|
||||
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(),
|
||||
() -> getSkinnable().rollback(dataItem.getModInfo(), localModFile),
|
||||
() -> getSkinnable().rollback(modInfo, localModFile),
|
||||
popup.get()))
|
||||
.collect(Collectors.toList())
|
||||
.toList()
|
||||
);
|
||||
|
||||
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
|
||||
});
|
||||
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(dataItem.getModInfo().getFile()));
|
||||
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile()));
|
||||
infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem)));
|
||||
|
||||
if (!warning.isEmpty()) {
|
||||
pseudoClassStateChanged(WARNING, true);
|
||||
|
||||
//noinspection ConstantValue
|
||||
this.warningTooltip = warning.size() == 1
|
||||
? new Tooltip(warning.get(0))
|
||||
: new Tooltip(String.join("\n", warning));
|
||||
FXUtils.installFastTooltip(this, warningTooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,13 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -57,54 +57,220 @@ public enum ModTranslations {
|
||||
};
|
||||
|
||||
public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) {
|
||||
switch (type) {
|
||||
case MOD:
|
||||
return MOD;
|
||||
case MODPACK:
|
||||
return MODPACK;
|
||||
default:
|
||||
return EMPTY;
|
||||
return switch (type) {
|
||||
case MOD -> MOD;
|
||||
case MODPACK -> MODPACK;
|
||||
default -> EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private static String cleanSubname(String subname) {
|
||||
if (StringUtils.isBlank(subname))
|
||||
return "";
|
||||
|
||||
StringBuilder builder = new StringBuilder(subname.length());
|
||||
for (int i = 0; i < subname.length(); ) {
|
||||
int ch = subname.codePointAt(i);
|
||||
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|
||||
|| ".+\\".indexOf(ch) >= 0) {
|
||||
builder.appendCodePoint(ch);
|
||||
} else if (Character.isWhitespace(ch)
|
||||
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0
|
||||
|| ch >= 0x1F300 && ch <= 0x1FAFF) {
|
||||
// Remove these unnecessary characters from subname
|
||||
} else {
|
||||
// The subname contains unsupported characters, so we do not use this subname to match the mod
|
||||
return "";
|
||||
}
|
||||
i += Character.charCount(ch);
|
||||
}
|
||||
return builder.length() == subname.length() ? subname : builder.toString();
|
||||
}
|
||||
|
||||
private final String resourceName;
|
||||
private List<Mod> mods;
|
||||
private Map<String, Mod> modIdMap; // mod id -> mod
|
||||
private Map<String, Mod> curseForgeMap; // curseforge id -> mod
|
||||
private List<Pair<String, Mod>> keywords;
|
||||
private int maxKeywordLength = -1;
|
||||
private volatile List<Mod> mods;
|
||||
private volatile Map<String, Mod> modIdMap; // mod id -> mod
|
||||
private volatile Map<String, Mod> subnameMap;
|
||||
private volatile Map<String, Mod> curseForgeMap; // curseforge id -> mod
|
||||
private volatile List<Pair<String, Mod>> keywords;
|
||||
private volatile int maxKeywordLength = -1;
|
||||
|
||||
ModTranslations(String resourceName) {
|
||||
this.resourceName = resourceName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Mod getModByCurseForgeId(String id) {
|
||||
if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null;
|
||||
public @NotNull List<Mod> getMods() {
|
||||
List<Mod> mods = this.mods;
|
||||
if (mods != null)
|
||||
return mods;
|
||||
|
||||
return curseForgeMap.get(id);
|
||||
synchronized (this) {
|
||||
mods = this.mods;
|
||||
if (mods != null)
|
||||
return mods;
|
||||
|
||||
if (StringUtils.isBlank(resourceName)) {
|
||||
return this.mods = List.of();
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) {
|
||||
return this.mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).toList();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load " + resourceName, e);
|
||||
return this.mods = List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Mod> getModIdMap() {
|
||||
Map<String, Mod> modIdMap = this.modIdMap;
|
||||
if (modIdMap != null)
|
||||
return modIdMap;
|
||||
synchronized (this) {
|
||||
modIdMap = this.modIdMap;
|
||||
if (modIdMap != null)
|
||||
return modIdMap;
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
modIdMap = new HashMap<>(mods.size());
|
||||
for (Mod mod : mods) {
|
||||
for (String id : mod.getModIds()) {
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
modIdMap.putIfAbsent(id, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.modIdMap = modIdMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Mod> getSubnameMap() {
|
||||
Map<String, Mod> subnameMap = this.subnameMap;
|
||||
if (subnameMap != null)
|
||||
return subnameMap;
|
||||
synchronized (this) {
|
||||
subnameMap = this.subnameMap;
|
||||
if (subnameMap != null)
|
||||
return subnameMap;
|
||||
|
||||
subnameMap = new HashMap<>();
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
for (Mod mod : mods) {
|
||||
String subname = cleanSubname(mod.getSubname());
|
||||
if (StringUtils.isNotBlank(subname)) {
|
||||
subnameMap.putIfAbsent(subname, mod);
|
||||
}
|
||||
}
|
||||
|
||||
return this.subnameMap = subnameMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Mod> getCurseForgeMap() {
|
||||
Map<String, Mod> curseForgeMap = this.curseForgeMap;
|
||||
if (curseForgeMap != null)
|
||||
return curseForgeMap;
|
||||
|
||||
synchronized (this) {
|
||||
curseForgeMap = this.curseForgeMap;
|
||||
if (curseForgeMap != null)
|
||||
return curseForgeMap;
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
curseForgeMap = new HashMap<>(mods.size());
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getCurseforge())) {
|
||||
curseForgeMap.putIfAbsent(mod.getCurseforge(), mod);
|
||||
}
|
||||
}
|
||||
|
||||
return this.curseForgeMap = curseForgeMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull List<Pair<String, Mod>> getKeywords() {
|
||||
List<Pair<String, Mod>> keywords = this.keywords;
|
||||
if (keywords != null)
|
||||
return keywords;
|
||||
|
||||
synchronized (this) {
|
||||
keywords = this.keywords;
|
||||
if (keywords != null)
|
||||
return keywords;
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
|
||||
keywords = new ArrayList<>();
|
||||
int maxKeywordLength = -1;
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getName())) {
|
||||
keywords.add(pair(mod.getName(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getSubname())) {
|
||||
keywords.add(pair(mod.getSubname(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getAbbr())) {
|
||||
keywords.add(pair(mod.getAbbr(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
|
||||
}
|
||||
}
|
||||
|
||||
this.maxKeywordLength = maxKeywordLength;
|
||||
return this.keywords = keywords;
|
||||
}
|
||||
}
|
||||
|
||||
private int getMaxKeywordLength() {
|
||||
int maxKeywordLength = this.maxKeywordLength;
|
||||
if (maxKeywordLength >= 0)
|
||||
return maxKeywordLength;
|
||||
|
||||
// Ensure maxKeywordLength is initialized
|
||||
getKeywords();
|
||||
return this.maxKeywordLength;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Mod getModById(String id) {
|
||||
if (StringUtils.isBlank(id) || !loadModIdMap()) return null;
|
||||
public Mod getModByCurseForgeId(String id) {
|
||||
if (StringUtils.isBlank(id)) return null;
|
||||
|
||||
return modIdMap.get(id);
|
||||
return getCurseForgeMap().get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Mod getMod(String id, String subname) {
|
||||
subname = cleanSubname(subname);
|
||||
if (StringUtils.isNotBlank(subname)) {
|
||||
Mod mod = getSubnameMap().get(subname);
|
||||
if (mod != null && (StringUtils.isBlank(id) || mod.getModIds().contains(id)))
|
||||
return mod;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(id))
|
||||
return getModIdMap().get(id);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract String getMcmodUrl(Mod mod);
|
||||
|
||||
public List<Mod> searchMod(String query) {
|
||||
if (!loadKeywords()) return Collections.emptyList();
|
||||
|
||||
StringBuilder newQuery = ((CharSequence) query).chars()
|
||||
StringBuilder newQuery = query.chars()
|
||||
.filter(ch -> !Character.isSpaceChar(ch))
|
||||
.collect(StringBuilder::new, (sb, value) -> sb.append((char) value), StringBuilder::append);
|
||||
query = newQuery.toString();
|
||||
|
||||
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength);
|
||||
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), getMaxKeywordLength());
|
||||
List<Pair<Integer, Mod>> modList = new ArrayList<>();
|
||||
for (Pair<String, Mod> keyword : keywords) {
|
||||
for (Pair<String, Mod> keyword : getKeywords()) {
|
||||
int value = lcs.calc(query, keyword.getKey());
|
||||
if (value >= Math.max(1, query.length() - 3)) {
|
||||
modList.add(pair(value, keyword.getValue()));
|
||||
@@ -113,92 +279,7 @@ public enum ModTranslations {
|
||||
return modList.stream()
|
||||
.sorted((a, b) -> -a.getKey().compareTo(b.getKey()))
|
||||
.map(Pair::getValue)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean loadFromResource() {
|
||||
if (mods != null) return true;
|
||||
if (StringUtils.isBlank(resourceName)) {
|
||||
mods = Collections.emptyList();
|
||||
return true;
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) {
|
||||
mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load " + resourceName, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadCurseForgeMap() {
|
||||
if (curseForgeMap != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mods == null) {
|
||||
if (!loadFromResource()) return false;
|
||||
}
|
||||
|
||||
curseForgeMap = new HashMap<>();
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getCurseforge())) {
|
||||
curseForgeMap.put(mod.getCurseforge(), mod);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean loadModIdMap() {
|
||||
if (modIdMap != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mods == null) {
|
||||
if (!loadFromResource()) return false;
|
||||
}
|
||||
|
||||
modIdMap = new HashMap<>();
|
||||
for (Mod mod : mods) {
|
||||
for (String id : mod.getModIds()) {
|
||||
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
|
||||
modIdMap.put(id, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean loadKeywords() {
|
||||
if (keywords != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mods == null) {
|
||||
if (!loadFromResource()) return false;
|
||||
}
|
||||
|
||||
keywords = new ArrayList<>();
|
||||
maxKeywordLength = -1;
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getName())) {
|
||||
keywords.add(pair(mod.getName(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getSubname())) {
|
||||
keywords.add(pair(mod.getSubname(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getAbbr())) {
|
||||
keywords.add(pair(mod.getAbbr(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static final class Mod {
|
||||
@@ -217,7 +298,7 @@ public enum ModTranslations {
|
||||
|
||||
curseforge = items[0];
|
||||
mcmod = items[1];
|
||||
modIds = Collections.unmodifiableList(Arrays.asList(items[2].split(",")));
|
||||
modIds = List.of(items[2].split(","));
|
||||
name = items[3];
|
||||
subname = items[4];
|
||||
abbr = items[5];
|
||||
|
||||
Loaded 100 of 339 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user