Compare commits
145
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b44404c469 | ||
|
|
0d53d521bf | ||
|
|
a9c14bfa0b | ||
|
|
6ef823b5ba | ||
|
|
d577e1fe30 | ||
|
|
db1ef60add | ||
|
|
ab6e8692ad | ||
|
|
97ada51489 | ||
|
|
8e9c2f27a1 | ||
|
|
8563db3654 | ||
|
|
2695b5c42f | ||
|
|
bed87425e7 | ||
|
|
d6ad1cbdf7 | ||
|
|
da5bbd2b22 | ||
|
|
5c726913e4 | ||
|
|
02bd9e9d2d | ||
|
|
458793f0c0 | ||
|
|
0543da31cc | ||
|
|
a74dc98ce6 | ||
|
|
9dfc6155c4 | ||
|
|
e4f10d7cd8 | ||
|
|
a21a8d5045 | ||
|
|
59994bdeed | ||
|
|
a95e4c65ee | ||
|
|
df7783c568 | ||
|
|
e5df2034fd | ||
|
|
f13d53d6da | ||
|
|
75c4fb6550 | ||
|
|
44869da20e | ||
|
|
756dce0d6f | ||
|
|
4a4fa07db4 | ||
|
|
17bc003698 | ||
|
|
80b2242e4d | ||
|
|
47ccda048c | ||
|
|
ae144aa7b8 | ||
|
|
343c09b6ff | ||
|
|
1bbc6ef777 | ||
|
|
f4c6a4f891 | ||
|
|
733b2c90c4 | ||
|
|
81b61523df | ||
|
|
d93f4d6bbb | ||
|
|
f0a16b0cd9 | ||
|
|
783cadcf77 | ||
|
|
cbb4b0c51b | ||
|
|
c4d9803177 | ||
|
|
2ce4fcabcb | ||
|
|
3a30e5b048 | ||
|
|
49b0a99ced | ||
|
|
8f2c860242 | ||
|
|
8c10775de9 | ||
|
|
b5bf7ce4d0 | ||
|
|
bd6a6fa9b6 | ||
|
|
1c0ae3918d | ||
|
|
c6ce2df731 | ||
|
|
18985c0b63 | ||
|
|
322e29642d | ||
|
|
a86e5d7bf5 | ||
|
|
c90eaa2937 | ||
|
|
defacb3554 | ||
|
|
0a66863272 | ||
|
|
d90df221bc | ||
|
|
fd7f909561 | ||
|
|
a921a11be6 | ||
|
|
7fbc99f3eb | ||
|
|
7e40c799e5 | ||
|
|
f2032ab7a8 | ||
|
|
d7c6a23dbe | ||
|
|
27e1e021d7 | ||
|
|
2516fb5d69 | ||
|
|
d66c080dea | ||
|
|
94ca81035b | ||
|
|
1ab414e702 | ||
|
|
16f6e89e45 | ||
|
|
22f6bfd845 | ||
|
|
c4823b3e55 | ||
|
|
e34ec9f801 | ||
|
|
d2e87ba024 | ||
|
|
ffb50423af | ||
|
|
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 |
No files matched your search
@@ -21,5 +21,9 @@ jobs:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
@@ -2,16 +2,26 @@ name: Check Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 * * * *'
|
||||
# schedule:
|
||||
# - cron: '30 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dev-check-update:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- channel: dev
|
||||
task: checkUpdateDev
|
||||
- channel: stable
|
||||
task: checkUpdateStable
|
||||
runs-on: ubuntu-latest
|
||||
name: check-update-${{ matrix.channel }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
@@ -22,24 +32,28 @@ jobs:
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Fetch last version
|
||||
run: ./gradlew checkUpdateDev --no-daemon --info --stacktrace
|
||||
run: ./gradlew ${{ matrix.task }} --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"
|
||||
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"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
if [ "${{ matrix.channel }}" = "stable" ]; then
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
fi
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/${{ matrix.channel }}.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
@@ -63,60 +77,3 @@ jobs:
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
prerelease: true
|
||||
stable-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
needs: dev-check-update
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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: Fetch last version
|
||||
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
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Sync to Gitee
|
||||
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
run:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to Gitee
|
||||
uses: Yikun/hub-mirror-action@v1.4
|
||||
with:
|
||||
src: github/HMCL-dev
|
||||
dst: gitee/huanghongxun
|
||||
static_list: 'HMCL'
|
||||
force_update: true
|
||||
debug: true
|
||||
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
|
||||
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
|
||||
cache_path: /github/workspace/hub-mirror-cache
|
||||
@@ -19,8 +19,12 @@ jobs:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
||||
run: ./gradlew build --no-daemon --parallel
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
@@ -47,4 +51,4 @@ jobs:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Mirror Repository
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
concurrency:
|
||||
group: mirror-repository
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- name: Gitee
|
||||
repo: gitee.com/huanghongxun/HMCL
|
||||
user: 'hmcl-sync'
|
||||
token: 'GITEE_SYNC_TOKEN'
|
||||
- name: CNB
|
||||
repo: cnb.cool/HMCL-dev/HMCL
|
||||
user: 'cnb'
|
||||
token: 'CNB_SYNC_TOKEN'
|
||||
name: Mirror to ${{ matrix.target.name }}
|
||||
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror GitHub to ${{ matrix.target.name }}
|
||||
run: |
|
||||
git clone --mirror "https://github.com/$GITHUB_REPOSITORY.git" -- repo
|
||||
cd repo
|
||||
git push -f --prune "https://${{ matrix.target.user }}:${{ secrets[matrix.target.token] }}@${{ matrix.target.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
|
||||
@@ -16,6 +16,11 @@ NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
/.java/
|
||||
/.local/
|
||||
/.cache/
|
||||
|
||||
# IANA Language Subtag Registry
|
||||
language-subtag-registry
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
|
||||
+45
-35
@@ -1,8 +1,11 @@
|
||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
@@ -16,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
|
||||
|
||||
@@ -50,7 +59,7 @@ dependencies {
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
|
||||
if (launcherExe == null) {
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
@@ -112,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)
|
||||
}
|
||||
@@ -140,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"])
|
||||
@@ -195,7 +205,7 @@ tasks.shadowJar {
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
if (launcherExe != null) {
|
||||
if (launcherExe.isNotBlank()) {
|
||||
into("assets") {
|
||||
from(file(launcherExe))
|
||||
}
|
||||
@@ -325,7 +335,7 @@ tasks.register<JavaExec>("run") {
|
||||
classpath = files(jarPath)
|
||||
workingDir = rootProject.rootDir
|
||||
|
||||
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
|
||||
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS") ?: "-Xmx1g")
|
||||
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
|
||||
vmOptions += "-Dhmcl.offline.auth.restricted=false"
|
||||
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
* 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.controls;
|
||||
|
||||
import com.jfoenix.controls.events.JFXDialogEvent;
|
||||
import com.jfoenix.converters.DialogTransitionConverter;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ObjectPropertyBase;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleableProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/// Note: for JFXDialog to work properly, the root node **MUST**
|
||||
/// be of type [StackPane]
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 1.0
|
||||
/// @since 2016-03-09
|
||||
@DefaultProperty(value = "content")
|
||||
public class JFXDialog extends StackPane {
|
||||
|
||||
private static final double INITIAL_SCALE = 0.8;
|
||||
|
||||
// public static enum JFXDialogLayout{PLAIN, HEADING, ACTIONS, BACKDROP};
|
||||
public enum DialogTransition {
|
||||
CENTER, NONE
|
||||
}
|
||||
|
||||
private StackPane contentHolder;
|
||||
|
||||
private double offsetX = 0;
|
||||
private double offsetY = 0;
|
||||
|
||||
private StackPane dialogContainer;
|
||||
private Region content;
|
||||
private Transition animation;
|
||||
|
||||
private final EventHandler<? super MouseEvent> closeHandler = e -> close();
|
||||
|
||||
/// creates empty JFXDialog control with CENTER animation type
|
||||
public JFXDialog() {
|
||||
this(null, null, DialogTransition.CENTER);
|
||||
}
|
||||
|
||||
/// creates empty JFXDialog control with a specified animation type
|
||||
public JFXDialog(DialogTransition transition) {
|
||||
this(null, null, transition);
|
||||
}
|
||||
|
||||
/// creates JFXDialog control with a specified animation type, the animation type
|
||||
/// can be one of the following:
|
||||
///
|
||||
/// - CENTER
|
||||
/// - TOP
|
||||
/// - RIGHT
|
||||
/// - BOTTOM
|
||||
/// - LEFT
|
||||
///
|
||||
/// @param dialogContainer is the parent of the dialog, it
|
||||
/// @param content the content of dialog
|
||||
/// @param transitionType the animation type
|
||||
public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType) {
|
||||
initialize();
|
||||
setContent(content);
|
||||
setDialogContainer(dialogContainer);
|
||||
this.transitionType.set(transitionType);
|
||||
// init change listeners
|
||||
initChangeListeners();
|
||||
}
|
||||
|
||||
/// creates JFXDialog control with a specified animation type that
|
||||
/// is closed when clicking on the overlay, the animation type
|
||||
/// can be one of the following:
|
||||
///
|
||||
/// - CENTER
|
||||
/// - TOP
|
||||
/// - RIGHT
|
||||
/// - BOTTOM
|
||||
/// - LEFT
|
||||
public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType, boolean overlayClose) {
|
||||
setOverlayClose(overlayClose);
|
||||
initialize();
|
||||
setContent(content);
|
||||
setDialogContainer(dialogContainer);
|
||||
this.transitionType.set(transitionType);
|
||||
// init change listeners
|
||||
initChangeListeners();
|
||||
}
|
||||
|
||||
private void initChangeListeners() {
|
||||
overlayCloseProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
|
||||
} else {
|
||||
this.removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.setVisible(false);
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.transitionType.addListener((o, oldVal, newVal) -> {
|
||||
animation = getShowAnimation(transitionType.get());
|
||||
});
|
||||
|
||||
contentHolder = new StackPane();
|
||||
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
|
||||
JFXDepthManager.setDepth(contentHolder, 4);
|
||||
contentHolder.setPickOnBounds(false);
|
||||
// ensure stackpane is never resized beyond it's preferred size
|
||||
contentHolder.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
|
||||
this.getChildren().add(contentHolder);
|
||||
this.getStyleClass().add("jfx-dialog-overlay-pane");
|
||||
StackPane.setAlignment(contentHolder, Pos.CENTER);
|
||||
this.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null)));
|
||||
// close the dialog if clicked on the overlay pane
|
||||
if (overlayClose.get()) {
|
||||
this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler);
|
||||
}
|
||||
// prevent propagating the events to overlay pane
|
||||
contentHolder.addEventHandler(MouseEvent.ANY, Event::consume);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Setters / Getters *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// @return the dialog container
|
||||
public StackPane getDialogContainer() {
|
||||
return dialogContainer;
|
||||
}
|
||||
|
||||
/// set the dialog container
|
||||
/// Note: the dialog container must be StackPane, its the container for the dialog to be shown in.
|
||||
public void setDialogContainer(StackPane dialogContainer) {
|
||||
if (dialogContainer != null) {
|
||||
this.dialogContainer = dialogContainer;
|
||||
// FIXME: need to be improved to consider only the parent boundary
|
||||
offsetX = dialogContainer.getBoundsInLocal().getWidth();
|
||||
offsetY = dialogContainer.getBoundsInLocal().getHeight();
|
||||
animation = getShowAnimation(transitionType.get());
|
||||
}
|
||||
}
|
||||
|
||||
/// @return dialog content node
|
||||
public Region getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
/// set the content of the dialog
|
||||
public void setContent(Region content) {
|
||||
if (content != null) {
|
||||
this.content = content;
|
||||
this.content.setPickOnBounds(false);
|
||||
contentHolder.getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
/// indicates whether the dialog will close when clicking on the overlay or not
|
||||
private final BooleanProperty overlayClose = new SimpleBooleanProperty(true);
|
||||
|
||||
public final BooleanProperty overlayCloseProperty() {
|
||||
return this.overlayClose;
|
||||
}
|
||||
|
||||
public final boolean isOverlayClose() {
|
||||
return this.overlayCloseProperty().get();
|
||||
}
|
||||
|
||||
public final void setOverlayClose(final boolean overlayClose) {
|
||||
this.overlayCloseProperty().set(overlayClose);
|
||||
}
|
||||
|
||||
/// if sets to true, the content of dialog container will be cached and replaced with an image
|
||||
/// when displaying the dialog (better performance).
|
||||
/// this is recommended if the content behind the dialog will not change during the showing
|
||||
/// period
|
||||
private final BooleanProperty cacheContainer = new SimpleBooleanProperty(false);
|
||||
|
||||
public boolean isCacheContainer() {
|
||||
return cacheContainer.get();
|
||||
}
|
||||
|
||||
public BooleanProperty cacheContainerProperty() {
|
||||
return cacheContainer;
|
||||
}
|
||||
|
||||
public void setCacheContainer(boolean cacheContainer) {
|
||||
this.cacheContainer.set(cacheContainer);
|
||||
}
|
||||
|
||||
/// it will show the dialog in the specified container
|
||||
public void show(StackPane dialogContainer) {
|
||||
this.setDialogContainer(dialogContainer);
|
||||
showDialog();
|
||||
}
|
||||
|
||||
private ArrayList<Node> tempContent;
|
||||
|
||||
/**
|
||||
* show the dialog inside its parent container
|
||||
*/
|
||||
public void show() {
|
||||
this.setDialogContainer(dialogContainer);
|
||||
showDialog();
|
||||
}
|
||||
|
||||
private void showDialog() {
|
||||
if (dialogContainer == null) {
|
||||
throw new RuntimeException("ERROR: JFXDialog container is not set!");
|
||||
}
|
||||
if (isCacheContainer()) {
|
||||
tempContent = new ArrayList<>(dialogContainer.getChildren());
|
||||
|
||||
SnapshotParameters snapShotparams = new SnapshotParameters();
|
||||
snapShotparams.setFill(Color.TRANSPARENT);
|
||||
WritableImage temp = dialogContainer.snapshot(snapShotparams,
|
||||
new WritableImage((int) dialogContainer.getWidth(),
|
||||
(int) dialogContainer.getHeight()));
|
||||
ImageView tempImage = new ImageView(temp);
|
||||
tempImage.setCache(true);
|
||||
tempImage.setCacheHint(CacheHint.SPEED);
|
||||
dialogContainer.getChildren().setAll(tempImage, this);
|
||||
} else {
|
||||
//prevent error if opening an already opened dialog
|
||||
dialogContainer.getChildren().remove(this);
|
||||
tempContent = null;
|
||||
dialogContainer.getChildren().add(this);
|
||||
}
|
||||
|
||||
if (animation != null) {
|
||||
animation.play();
|
||||
} else {
|
||||
setVisible(true);
|
||||
setOpacity(1);
|
||||
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* close the dialog
|
||||
*/
|
||||
public void close() {
|
||||
if (animation != null) {
|
||||
animation.setRate(-2);
|
||||
animation.play();
|
||||
animation.setOnFinished(e -> {
|
||||
closeDialog();
|
||||
});
|
||||
} else {
|
||||
setOpacity(0);
|
||||
setVisible(false);
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDialog() {
|
||||
resetProperties();
|
||||
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.CLOSED));
|
||||
if (tempContent == null) {
|
||||
dialogContainer.getChildren().remove(this);
|
||||
} else {
|
||||
dialogContainer.getChildren().setAll(tempContent);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Transitions *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private Transition getShowAnimation(DialogTransition transitionType) {
|
||||
Transition animation = null;
|
||||
if (contentHolder != null) {
|
||||
animation = switch (transitionType) {
|
||||
case CENTER -> {
|
||||
contentHolder.setScaleX(INITIAL_SCALE);
|
||||
contentHolder.setScaleY(INITIAL_SCALE);
|
||||
yield new CenterTransition();
|
||||
}
|
||||
case NONE -> {
|
||||
contentHolder.setScaleX(1);
|
||||
contentHolder.setScaleY(1);
|
||||
contentHolder.setTranslateX(0);
|
||||
contentHolder.setTranslateY(0);
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
}
|
||||
if (animation != null) {
|
||||
animation.setOnFinished(finish ->
|
||||
Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED)));
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
private void resetProperties() {
|
||||
this.setVisible(false);
|
||||
contentHolder.setTranslateX(0);
|
||||
contentHolder.setTranslateY(0);
|
||||
contentHolder.setScaleX(1);
|
||||
contentHolder.setScaleY(1);
|
||||
}
|
||||
|
||||
private final class CenterTransition extends CachedTransition {
|
||||
private static final Interpolator INTERPOLATOR = Motion.EMPHASIZED_DECELERATE;
|
||||
|
||||
CenterTransition() {
|
||||
super(contentHolder, new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(contentHolder.scaleXProperty(), INITIAL_SCALE, INTERPOLATOR),
|
||||
new KeyValue(contentHolder.scaleYProperty(), INITIAL_SCALE, INTERPOLATOR),
|
||||
new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR)
|
||||
),
|
||||
new KeyFrame(Duration.millis(10),
|
||||
new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR),
|
||||
new KeyValue(JFXDialog.this.opacityProperty(), 0, INTERPOLATOR)
|
||||
),
|
||||
new KeyFrame(Motion.EXTRA_LONG4,
|
||||
new KeyValue(contentHolder.scaleXProperty(), 1, INTERPOLATOR),
|
||||
new KeyValue(contentHolder.scaleYProperty(), 1, INTERPOLATOR),
|
||||
new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR),
|
||||
new KeyValue(JFXDialog.this.opacityProperty(), 1, INTERPOLATOR)
|
||||
))
|
||||
);
|
||||
// reduce the number to increase the shifting , increase number to reduce shifting
|
||||
setCycleDuration(Duration.seconds(0.4));
|
||||
setDelay(Duration.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Stylesheet Handling *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
/// Initialize the style class to 'jfx-dialog'.
|
||||
///
|
||||
/// This is the selector class from which CSS can be used to style
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-dialog";
|
||||
|
||||
/// dialog transition type property, it can be one of the following:
|
||||
///
|
||||
/// - CENTER
|
||||
/// - TOP
|
||||
/// - RIGHT
|
||||
/// - BOTTOM
|
||||
/// - LEFT
|
||||
/// - NONE
|
||||
private final StyleableObjectProperty<DialogTransition> transitionType = new SimpleStyleableObjectProperty<>(
|
||||
StyleableProperties.DIALOG_TRANSITION,
|
||||
JFXDialog.this,
|
||||
"dialogTransition",
|
||||
DialogTransition.CENTER);
|
||||
|
||||
public DialogTransition getTransitionType() {
|
||||
return transitionType == null ? DialogTransition.CENTER : transitionType.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<DialogTransition> transitionTypeProperty() {
|
||||
return this.transitionType;
|
||||
}
|
||||
|
||||
public void setTransitionType(DialogTransition transition) {
|
||||
this.transitionType.set(transition);
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
private static final CssMetaData<JFXDialog, DialogTransition> DIALOG_TRANSITION =
|
||||
new CssMetaData<JFXDialog, DialogTransition>("-jfx-dialog-transition",
|
||||
DialogTransitionConverter.getInstance(),
|
||||
DialogTransition.CENTER) {
|
||||
@Override
|
||||
public boolean isSettable(JFXDialog control) {
|
||||
return control.transitionType == null || !control.transitionType.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableProperty<DialogTransition> getStyleableProperty(JFXDialog control) {
|
||||
return control.transitionTypeProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(StackPane.getClassCssMetaData());
|
||||
Collections.addAll(styleables,
|
||||
DIALOG_TRANSITION
|
||||
);
|
||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.CHILD_STYLEABLES;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Custom Events *
|
||||
* *
|
||||
**************************************************************************/
|
||||
|
||||
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty = new ObjectPropertyBase<EventHandler<? super JFXDialogEvent>>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(JFXDialogEvent.CLOSED, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return JFXDialog.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onClosed";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a function to be called when the dialog is closed.
|
||||
* Note: it will be triggered after the close animation is finished.
|
||||
*/
|
||||
public ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty() {
|
||||
return onDialogClosedProperty;
|
||||
}
|
||||
|
||||
public void setOnDialogClosed(EventHandler<? super JFXDialogEvent> handler) {
|
||||
onDialogClosedProperty().set(handler);
|
||||
}
|
||||
|
||||
public EventHandler<? super JFXDialogEvent> getOnDialogClosed() {
|
||||
return onDialogClosedProperty().get();
|
||||
}
|
||||
|
||||
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty = new ObjectPropertyBase<>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(JFXDialogEvent.OPENED, get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return JFXDialog.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "onOpened";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a function to be called when the dialog is opened.
|
||||
* Note: it will be triggered after the show animation is finished.
|
||||
*/
|
||||
public ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty() {
|
||||
return onDialogOpenedProperty;
|
||||
}
|
||||
|
||||
public void setOnDialogOpened(EventHandler<? super JFXDialogEvent> handler) {
|
||||
onDialogOpenedProperty().set(handler);
|
||||
}
|
||||
|
||||
public EventHandler<? super JFXDialogEvent> getOnDialogOpened() {
|
||||
return onDialogOpenedProperty().get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
// 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));
|
||||
FXUtils.smoothScrolling(flow);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -28,6 +30,7 @@ import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -46,24 +49,10 @@ 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)
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && !isInsideMacAppBundle())
|
||||
initIcon();
|
||||
|
||||
checkJavaFX();
|
||||
@@ -80,6 +69,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 {
|
||||
@@ -108,6 +162,25 @@ public final class EntryPoint {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInsideMacAppBundle() {
|
||||
Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar == null)
|
||||
return false;
|
||||
|
||||
for (Path current = thisJar.getParent();
|
||||
current != null && current.getParent() != null;
|
||||
current = current.getParent()
|
||||
) {
|
||||
if ("Contents".equals(FileUtils.getName(current))
|
||||
&& FileUtils.getName(current.getParent()).endsWith(".app")
|
||||
&& Files.exists(current.resolve("Info.plist"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
try {
|
||||
if (java.awt.Taskbar.isTaskbarSupported()) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -318,23 +319,32 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
|
||||
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
|
||||
return VersionIconType.FABRIC.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
|
||||
return VersionIconType.FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
|
||||
return VersionIconType.CLEANROOM.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
|
||||
return VersionIconType.NEO_FORGE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
|
||||
return VersionIconType.QUILT.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
|
||||
return VersionIconType.CHICKEN.getIcon();
|
||||
else
|
||||
return VersionIconType.FURNACE.getIcon();
|
||||
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
|
||||
return VersionIconType.OPTIFINE.getIcon();
|
||||
}
|
||||
|
||||
return VersionIconType.DEFAULT.getIcon();
|
||||
String gameVersion = getGameVersion(version).orElse(null);
|
||||
if (gameVersion != null) {
|
||||
GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion);
|
||||
if (versionNumber.isAprilFools()) {
|
||||
return VersionIconType.APRIL_FOOLS.getIcon();
|
||||
} else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) {
|
||||
return VersionIconType.COMMAND.getIcon();
|
||||
} else if (versionNumber instanceof GameVersionNumber.Old) {
|
||||
return VersionIconType.CRAFT_TABLE.getIcon();
|
||||
}
|
||||
}
|
||||
return VersionIconType.GRASS.getIcon();
|
||||
} else {
|
||||
return iconType.getIcon();
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ 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;
|
||||
|
||||
@@ -190,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,
|
||||
@@ -706,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;
|
||||
@@ -849,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -888,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,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())
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.setting;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -38,18 +37,15 @@ 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.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(value = Config.Adapter.class)
|
||||
public final class Config implements Observable {
|
||||
public final class Config extends ObservableSetting {
|
||||
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
@@ -67,54 +63,15 @@ public final class Config implements Observable {
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.create();
|
||||
|
||||
private static final List<ObservableField<Config>> FIELDS;
|
||||
|
||||
static {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
Field[] fields = Config.class.getDeclaredFields();
|
||||
|
||||
var configFields = new ArrayList<ObservableField<Config>>(fields.length);
|
||||
for (Field field : fields) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers))
|
||||
continue;
|
||||
|
||||
configFields.add(ObservableField.of(lookup, field));
|
||||
}
|
||||
FIELDS = List.copyOf(configFields);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Config fromJson(String json) throws JsonParseException {
|
||||
return CONFIG_GSON.fromJson(json, Config.class);
|
||||
}
|
||||
|
||||
private transient final ObservableHelper helper = new ObservableHelper(this);
|
||||
private transient final DirtyTracker tracker = new DirtyTracker();
|
||||
private transient final Map<String, JsonElement> unknownFields = new HashMap<>();
|
||||
|
||||
public Config() {
|
||||
var shouldBeWrite = Collections.<Observable>newSetFromMap(new IdentityHashMap<>());
|
||||
Collections.addAll(shouldBeWrite, configVersion, uiVersion);
|
||||
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(this);
|
||||
if (shouldBeWrite.contains(observable))
|
||||
tracker.markDirty(observable);
|
||||
else
|
||||
tracker.track(observable);
|
||||
observable.addListener(helper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
tracker.markDirty(configVersion);
|
||||
tracker.markDirty(uiVersion);
|
||||
register();
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
@@ -250,6 +207,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();
|
||||
|
||||
@@ -760,53 +732,10 @@ public final class Config implements Observable {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public static final class Adapter implements JsonSerializer<Config>, JsonDeserializer<Config> {
|
||||
|
||||
public static final class Adapter extends ObservableSetting.Adapter<Config> {
|
||||
@Override
|
||||
public JsonElement serialize(Config config, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (config == null)
|
||||
return JsonNull.INSTANCE;
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(config);
|
||||
if (config.tracker.isDirty(observable)) {
|
||||
field.serialize(result, config, context);
|
||||
}
|
||||
}
|
||||
config.unknownFields.forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json.isJsonNull())
|
||||
return null;
|
||||
|
||||
if (!json.isJsonObject())
|
||||
throw new JsonParseException("Config is not an object: " + json);
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
config.unknownFields.putAll(values);
|
||||
return config;
|
||||
protected Config createInstance() {
|
||||
return new Config();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -22,13 +22,19 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
@@ -51,8 +57,23 @@ public final class StyleSheets {
|
||||
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
|
||||
}
|
||||
|
||||
private static String toStyleSheetUri(String styleSheet) {
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
private static String toStyleSheetUri(String styleSheet, String fallback) {
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
Files.writeString(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
@@ -102,7 +123,7 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString());
|
||||
return toStyleSheetUri(builder.toString(), defaultCss);
|
||||
}
|
||||
|
||||
private static String rgba(Color color, double opacity) {
|
||||
@@ -129,7 +150,7 @@ public final class StyleSheets {
|
||||
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
|
||||
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
|
||||
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
|
||||
'}');
|
||||
'}', blueCss);
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
@@ -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) {
|
||||
|
||||
@@ -771,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());
|
||||
@@ -841,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
-13
@@ -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,18 +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.ui.animation;
|
||||
package org.jackhuang.hmcl.terracotta.profile;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.util.Duration;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public interface AnimationHandler {
|
||||
Duration getDuration();
|
||||
|
||||
Pane getCurrentRoot();
|
||||
|
||||
Node getPreviousNode();
|
||||
|
||||
Node getCurrentNode();
|
||||
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;
|
||||
@@ -48,6 +49,8 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
@@ -55,12 +58,15 @@ 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.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -86,7 +92,7 @@ public final class Controllers {
|
||||
|
||||
private static Scene scene;
|
||||
private static Stage stage;
|
||||
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
|
||||
private static VersionPage versionPage;
|
||||
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
|
||||
GameListPage gameListPage = new GameListPage();
|
||||
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
@@ -99,7 +105,7 @@ public final class Controllers {
|
||||
});
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
private static DecoratorController decorator;
|
||||
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
|
||||
private static DownloadPage downloadPage;
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||
@@ -107,7 +113,8 @@ public final class Controllers {
|
||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
private static LauncherSettingsPage settingsPage;
|
||||
private static Lazy<TerracottaPage> terracottaPage = new Lazy<>(TerracottaPage::new);
|
||||
|
||||
private Controllers() {
|
||||
}
|
||||
@@ -122,7 +129,18 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static VersionPage getVersionPage() {
|
||||
return versionPage.get();
|
||||
if (versionPage == null) {
|
||||
versionPage = new VersionPage();
|
||||
}
|
||||
return versionPage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareVersionPage() {
|
||||
if (versionPage == null) {
|
||||
LOG.info("Prepare the version page");
|
||||
versionPage = FXUtils.prepareNode(new VersionPage());
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -137,7 +155,18 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static LauncherSettingsPage getSettingsPage() {
|
||||
return settingsPage.get();
|
||||
if (settingsPage == null) {
|
||||
settingsPage = new LauncherSettingsPage();
|
||||
}
|
||||
return settingsPage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareSettingsPage() {
|
||||
if (settingsPage == null) {
|
||||
LOG.info("Prepare the settings page");
|
||||
settingsPage = FXUtils.prepareNode(new LauncherSettingsPage());
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -147,7 +176,23 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static DownloadPage getDownloadPage() {
|
||||
return downloadPage.get();
|
||||
if (downloadPage == null) {
|
||||
downloadPage = new DownloadPage();
|
||||
}
|
||||
return downloadPage;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void prepareDownloadPage() {
|
||||
if (downloadPage == null) {
|
||||
LOG.info("Prepare the download page");
|
||||
downloadPage = FXUtils.prepareNode(new DownloadPage());
|
||||
}
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static Node getTerracottaPage() {
|
||||
return terracottaPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
@@ -296,16 +341,16 @@ public final class Controllers {
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, FXUtils.EASE)
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(600),
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, FXUtils.EASE)
|
||||
new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.play();
|
||||
@@ -424,6 +469,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, "");
|
||||
}
|
||||
@@ -456,7 +533,11 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void navigate(Node node) {
|
||||
decorator.navigate(node);
|
||||
decorator.navigate(node, ContainerAnimations.NAVIGATION, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
public static void navigateForward(Node node) {
|
||||
decorator.navigate(node, ContainerAnimations.FORWARD, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
public static void showToast(String content) {
|
||||
@@ -470,6 +551,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);
|
||||
@@ -487,6 +572,7 @@ public final class Controllers {
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
terracottaPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
|
||||
@@ -30,16 +30,19 @@ import javafx.beans.value.*;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
@@ -53,6 +56,7 @@ import javafx.stage.*;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -391,6 +395,11 @@ public final class FXUtils {
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public static void smoothScrolling(VirtualFlow<?> virtualFlow) {
|
||||
if (AnimationUtils.isAnimationEnabled())
|
||||
ScrollUtils.addSmoothScrolling(virtualFlow);
|
||||
}
|
||||
|
||||
/// If the current environment is JavaFX 23 or higher, this method returns [Labeled#textTruncatedProperty()];
|
||||
/// Otherwise, it returns `null`.
|
||||
public static @Nullable ReadOnlyBooleanProperty textTruncatedProperty(Labeled labeled) {
|
||||
@@ -434,28 +443,12 @@ public final class FXUtils {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
|
||||
animationKey = "FXUTILS.ANIMATION." + animationKey;
|
||||
Object oldTimeline = node.getProperties().get(animationKey);
|
||||
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
if (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
|
||||
public static <T> Animation playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> property, T from, T to, Interpolator interpolator) {
|
||||
if (from == null) from = property.getValue();
|
||||
if (duration == null || Objects.equals(duration, Duration.ZERO) || Objects.equals(from, to)) {
|
||||
playAnimation(node, animationKey, null);
|
||||
property.setValue(to);
|
||||
return null;
|
||||
} else {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)),
|
||||
new KeyFrame(duration, new KeyValue(property, to, interpolator))
|
||||
);
|
||||
playAnimation(node, animationKey, timeline);
|
||||
return timeline;
|
||||
}
|
||||
public static void playAnimation(Node node, String animationKey, Animation animation) {
|
||||
animationKey = "hmcl.animations." + animationKey;
|
||||
if (node.getProperties().get(animationKey) instanceof Animation oldAnimation)
|
||||
oldAnimation.stop();
|
||||
animation.play();
|
||||
node.getProperties().put(animationKey, animation);
|
||||
}
|
||||
|
||||
public static void openFolder(Path file) {
|
||||
@@ -1346,8 +1339,6 @@ public final class FXUtils {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Interpolator EASE = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
|
||||
|
||||
public static void onEscPressed(Node node, Runnable action) {
|
||||
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
@@ -1366,6 +1357,24 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static <N extends Parent> N prepareNode(N node) {
|
||||
Scene dummyScene = new Scene(node);
|
||||
StyleSheets.init(dummyScene);
|
||||
node.applyCss();
|
||||
node.layout();
|
||||
return node;
|
||||
}
|
||||
|
||||
public static void prepareOnMouseEnter(Node node, Runnable action) {
|
||||
node.addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(MouseEvent e) {
|
||||
node.removeEventFilter(MouseEvent.MOUSE_ENTERED, this);
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> void onScroll(Node node, List<T> list,
|
||||
ToIntFunction<List<T>> finder,
|
||||
Consumer<T> updater
|
||||
@@ -1387,6 +1396,16 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1400,12 +1419,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;
|
||||
@@ -283,7 +286,7 @@ public class GameCrashWindow extends Stage {
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
});
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
|
||||
@@ -38,6 +38,7 @@ public enum SVG {
|
||||
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
|
||||
ARCHIVE("M12 18 16 14 14.6 12.6 13 14.2V10H11V14.2L9.4 12.6 8 14 12 18ZM5 8V19H19V8H5ZM5 21Q4.175 21 3.5875 20.4125T3 19V6.525Q3 6.175 3.1125 5.85T3.45 5.25L4.7 3.725Q4.975 3.375 5.3875 3.1875T6.25 3H17.75Q18.2 3 18.6125 3.1875T19.3 3.725L20.55 5.25Q20.775 5.525 20.8875 5.85T21 6.525V19Q21 19.825 20.4125 20.4125T19 21H5ZM5.4 6H18.6L17.75 5H6.25L5.4 6ZM12 13.5Z"),
|
||||
ARCHIVE_FILL("M12 18l4-4-1.4-1.4L13 14.2V10H11v4.2L9.4 12.6 8 14l4 4ZM5 21q-.825 0-1.4125-.5875T3 19V6.525q0-.35.1125-.675t.3375-.6L4.7 3.725q.275-.35.6875-.5375T6.25 3h11.5q.45 0 .8625.1875T19.3 3.725L20.55 5.25q.225.275.3375.6T21 6.525V19q0 .825-.5875 1.4125T19 21H5ZM5.4 6H18.6l-.85-1H6.25L5.4 6Z"),
|
||||
ARROW_BACK("M7.825 13 13.425 18.6 12 20 4 12 12 4 13.425 5.4 7.825 11H20V13H7.825Z"),
|
||||
ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"),
|
||||
ARROW_DROP_UP("M7 14 12 9 17 14H7Z"),
|
||||
@@ -54,13 +55,16 @@ public enum SVG {
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
|
||||
DEPLOYED_CODE_FILL("M11 21.725 4 17.7q-.475-.275-.7375-.725T3 15.975V8.025q0-.55.2625-1T4 6.3l7-4.025Q11.475 2 12 2t1 .275L20 6.3q.475.275.7375.725t.2625 1v7.95q0 .55-.2625 1T20 17.7l-7 4.025Q12.525 22 12 22t-1-.275Zm0-9.15v6.85L12 20l1-.575v-6.85L19 9.1V8.05l-1.075-.625L12 10.85 6.075 7.425 5 8.05V9.1l6 3.475Z"),
|
||||
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
|
||||
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
|
||||
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
|
||||
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
|
||||
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
|
||||
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
|
||||
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
|
||||
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
|
||||
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
|
||||
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
|
||||
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
|
||||
@@ -69,16 +73,20 @@ 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"),
|
||||
HELP_FILL("M11.95 18q.525 0 .8875-.3625T13.2 16.75q0-.525-.3625-.8875T11.95 15.5q-.525 0-.8875.3625T10.7 16.75q0 .525.3625.8875T11.95 18Zm-.9-3.85H12.9q0-.825.1875-1.3t1.0625-1.3q.65-.65 1.025-1.2375T15.55 8.9q0-1.4-1.025-2.15T12.1 6q-1.425 0-2.3125.75T8.55 8.55l1.65.65q.125-.45.5625-.975T12.1 7.7q.8 0 1.2.4375t.4.9625q0 .5-.3.9375t-.75.8125q-1.1.975-1.35 1.475t-.25 1.825ZM12 22q-2.075 0-3.9-.7875T4.925 19.075q-1.35-1.35-2.1375-3.175T2 12q0-2.075.7875-3.9T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2q2.075 0 3.9.7875T19.075 4.925q1.35 1.35 2.1375 3.175T22 12q0 2.075-.7875 3.9T19.075 19.075q-1.35 1.35-3.175 2.1375T12 22Z"),
|
||||
HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"),
|
||||
HOST("M4 21Q3.175 21 2.5875 20.4125T2 19V5Q2 4.175 2.5875 3.5875T4 3H9Q9.825 3 10.4125 3.5875T11 5V19Q11 19.825 10.4125 20.4125T9 21H4ZM15 21Q14.175 21 13.5875 20.4125T13 19V5Q13 4.175 13.5875 3.5875T15 3H20Q20.825 3 21.4125 3.5875T22 5V19Q22 19.825 21.4125 20.4125T20 21H15ZM4 19H9V5H4V19ZM15 19H20V5H15V19ZM5 15H8V13H5V15ZM16 15H19V13H16V15ZM5 12H8V10H5V12ZM16 12H19V10H16V12ZM5 9H8V7H5V9ZM16 9H19V7H16V9ZM4 19H9 4ZM15 19H20 15Z"),
|
||||
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
INFO_FILL("M12 22q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22ZM12 9q-.425 0-.7125-.2875T11 8t.2875-.7125T12 7q.425-0 .7125.2875T13 8t-.2875.7125T12 9Zm-1 8V11h2v6H11Z"),
|
||||
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
|
||||
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
|
||||
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
|
||||
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
|
||||
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
|
||||
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
|
||||
LOCAL_CAFE_FILL("M4 21V19H20v2H4Zm4-4q-1.65 0-2.825-1.175T4 13V3H20q.825 0 1.4125.5875T22 5V8q0 .825-.5875 1.4125T20 10H18v3q0 1.65-1.175 2.825T14 17H8ZM18 8h2V5H18V8Z"),
|
||||
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
|
||||
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
|
||||
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
|
||||
@@ -90,6 +98,7 @@ public enum SVG {
|
||||
OUTPUT("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V7H19V5H5V19H19V17H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM17 17 15.6 15.6 18.175 13H9V11H18.175L15.6 8.4 17 7 22 12 17 17Z"),
|
||||
PACKAGE("M10 9.75 12 8.75 14 9.75V5H10V9.75ZM7 17V15H12V17H7ZM5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V19Q21 19.825 20.4125 20.4125T19 21H5ZM5 5V19 5ZM5 19H19V5H16V13L12 11 8 13V5H5V19Z"),
|
||||
PACKAGE2("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM11 21.725 4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725ZM16 8.525 17.925 7.425 12 4 10.05 5.125 16 8.525ZM12 10.85 13.95 9.725 8.025 6.3 6.075 7.425 12 10.85Z"),
|
||||
PACKAGE2_FILL("M11 21.725v-9.15L3 7.95v8.025q0 .55.2625 1T4 17.7l7 4.025Zm2 0L20 17.7q.475-.275.7375-.725t.2625-1V7.95l-8 4.625v9.15Zm3.975-13.75 2.95-1.725L13 2.275Q12.525 2 12 2t-1 .275L9.025 3.4l7.95 4.575ZM12 10.85l2.975-1.7L7.05 4.55l-3 1.725L12 10.85Z"),
|
||||
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
|
||||
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
|
||||
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
|
||||
@@ -97,13 +106,17 @@ public enum SVG {
|
||||
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
|
||||
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
|
||||
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
|
||||
SCHEMA_FILL("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Z"),
|
||||
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
|
||||
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
|
||||
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
|
||||
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
|
||||
SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons
|
||||
SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons
|
||||
STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"),
|
||||
STADIA_CONTROLLER_FILL("M4.725 20q-1.5 0-2.5625-1.075T1.05 16.325q0-.225.025-.45t.075-.45l2.1-8.4q.35-1.35 1.425-2.1875T7.125 4h9.75q1.375 0 2.45.8375T20.75 7.025l2.1 8.4q.05.225.0875.4625t.0375.4625q0 1.525-1.0875 2.5875T19.275 20q-1.05 0-1.95-.55t-1.35-1.5l-.7-1.45q-.125-.25-.375-.375T14.375 16H9.625q-.275 0-.525.125t-.375.375l-.7 1.45q-.45.95-1.35 1.5T4.725 20ZM13.5 11q.425 0 .7125-.2875T14.5 10t-.2875-.7125T13.5 9t-.7125.2875T12.5 10t.2875.7125T13.5 11Zm2-2q.425 0 .7125-.2875T16.5 8q0-.425-.2875-.7125T15.5 7q-.425 0-.7125.2875T14.5 8t.2875.7125T15.5 9Zm0 4q.425 0 .7125-.2875T16.5 12q0-.425-.2875-.7125T15.5 11q-.425 0-.7125.2875T14.5 12t.2875.7125T15.5 13Zm2-2q.425 0 .7125-.2875T18.5 10q0-.425-.2875-.7125T17.5 9q-.425 0-.7125.2875T16.5 10q0 .425.2875.7125T17.5 11Zm-9 1.5q.325 0 .5375-.2125T9.25 11.75v-1h1q.325 0 .5375-.2125T11 10t-.2125-.5375T10.25 9.25h-1v-1q0-.325-.2125-.5375T8.5 7.5q-.325 0-.5375.2125T7.75 8.25v1h-1q-.325 0-.5375.2125T6 10q0 .325.2125.5375T6.75 10.75h1v1q0 .325.2125.5375T8.5 12.5Z"),
|
||||
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
|
||||
STYLE_FILL("M3.975 19.8l-.85-.35q-.775-.325-1.0375-1.125T2.175 16.75l1.8-3.9V19.8Zm4 2.2q-.825 0-1.4125-.5875T5.975 20V14l2.65 7.35q.075.175.15.3375t.2.3125h-1Zm5.15-.1q-.8.3-1.55-.075t-1.05-1.175L6.075 8.45q-.3-.8.05-1.5625T7.275 5.85l7.55-2.75q.8-.3 1.55.075t1.05 1.175l4.45 12.2q.3.8-.05 1.5625T20.675 19.15l-7.55 2.75ZM10.975 10q.425 0 .7125-.2875T11.975 9q0-.425-.2875-.7125T10.975 8t-.7125.2875T9.975 9t.2875.7125T10.975 10Z"),
|
||||
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
|
||||
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
|
||||
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
|
||||
@@ -112,6 +125,7 @@ public enum SVG {
|
||||
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
|
||||
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
|
||||
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
|
||||
WB_SUNNY_FILL("M11 4V1h2V4H11Zm0 19V20h2v3H11Zm9-10V11h3v2H20ZM1 13V11H4v2H1ZM18.7 6.7 17.3 5.3l1.75-1.8L20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3l1.4 1.4-1.75 1.8Zm14.1 0-1.75-1.8 1.4-1.4 1.8 1.75-1.45 1.45ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18q-2.5 0-4.25-1.75T6 12 7.75 7.75 12 6t4.25 1.75T18 12t-1.75 4.25T12 18Z"),
|
||||
;
|
||||
|
||||
public static final double DEFAULT_SIZE = 24;
|
||||
|
||||
@@ -25,7 +25,9 @@ import javafx.animation.Animation.Status;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.IndexedCell;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.util.Duration;
|
||||
@@ -139,6 +141,21 @@ final class ScrollUtils {
|
||||
smoothScroll(scrollPane, speed, trackPadAdjustment);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow) {
|
||||
addSmoothScrolling(virtualFlow, 1);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow, double speed) {
|
||||
addSmoothScrolling(virtualFlow, speed, 7);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
public static void addSmoothScrolling(VirtualFlow<?> virtualFlow, double speed, double trackPadAdjustment) {
|
||||
smoothScroll(virtualFlow, speed, trackPadAdjustment);
|
||||
}
|
||||
|
||||
private static final double[] FRICTIONS = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
|
||||
private static final Duration DURATION = Duration.millis(3);
|
||||
|
||||
@@ -207,4 +224,58 @@ final class ScrollUtils {
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
|
||||
/// @author Glavo
|
||||
private static void smoothScroll(VirtualFlow<?> virtualFlow, double speed, double trackPadAdjustment) {
|
||||
if (!virtualFlow.isVertical())
|
||||
return;
|
||||
|
||||
final double[] derivatives = new double[FRICTIONS.length];
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
Holder<ScrollDirection> scrollDirectionHolder = new Holder<>();
|
||||
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
ScrollDirection scrollDirection = determineScrollDirection(event);
|
||||
if (scrollDirection == ScrollDirection.LEFT || scrollDirection == ScrollDirection.RIGHT) {
|
||||
return;
|
||||
}
|
||||
scrollDirectionHolder.value = scrollDirection;
|
||||
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
|
||||
|
||||
derivatives[0] += scrollDirection.intDirection * currentSpeed;
|
||||
if (timeline.getStatus() == Status.STOPPED) {
|
||||
timeline.play();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
virtualFlow.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
virtualFlow.addEventFilter(ScrollEvent.ANY, scrollHandler);
|
||||
|
||||
timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> {
|
||||
for (int i = 0; i < derivatives.length; i++) {
|
||||
derivatives[i] *= FRICTIONS[i];
|
||||
}
|
||||
for (int i = 1; i < derivatives.length; i++) {
|
||||
derivatives[i] += derivatives[i - 1];
|
||||
}
|
||||
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
|
||||
int cellCount = virtualFlow.getCellCount();
|
||||
IndexedCell<?> firstVisibleCell = virtualFlow.getFirstVisibleCell();
|
||||
double height = firstVisibleCell != null ? firstVisibleCell.getHeight() * cellCount : 0.0;
|
||||
|
||||
double delta = height > 0.0
|
||||
? dy / height
|
||||
: (scrollDirectionHolder.value == ScrollDirection.DOWN ? 0.001 : -0.001);
|
||||
virtualFlow.setPosition(Math.min(Math.max(virtualFlow.getPosition() + delta, 0), 1));
|
||||
|
||||
if (Math.abs(dy) < 0.001) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,18 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Node;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
@@ -40,6 +42,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
|
||||
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
|
||||
@@ -48,11 +51,21 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
setBody(new ProgressIndicator());
|
||||
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
VersionNumber targetVersion = VersionNumber.asVersion(remoteVersion.getVersion());
|
||||
VersionNumber currentVersion = VersionNumber.asVersion(Metadata.VERSION);
|
||||
if (targetVersion.compareTo(currentVersion) <= 0)
|
||||
// Downgrade update, no need to display changelog
|
||||
return null;
|
||||
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("#nowchange");
|
||||
if (node == null || !"h1".equals(node.nodeName()))
|
||||
throw new IOException("Cannot find #nowchange in document");
|
||||
Node node = document.selectFirst("h1[data-version=\"%s\"]".formatted(targetVersion));
|
||||
|
||||
if (node == null || !"h1".equals(node.nodeName())) {
|
||||
LOG.warning("Changelog not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
LOG.info("Open link: " + uri);
|
||||
@@ -60,8 +73,11 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
});
|
||||
|
||||
do {
|
||||
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
|
||||
break;
|
||||
if ("h1".equals(node.nodeName())) {
|
||||
String changelogVersion = node.attr("data-version");
|
||||
if (StringUtils.isBlank(changelogVersion) || currentVersion.compareTo(changelogVersion) >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
renderer.appendNode(node);
|
||||
node = node.nextSibling();
|
||||
@@ -71,10 +87,14 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
return renderer.render();
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
ScrollPane scrollPane = new ScrollPane(result);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setBody(scrollPane);
|
||||
if (result != null) {
|
||||
ScrollPane scrollPane = new ScrollPane(result);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
setBody(scrollPane);
|
||||
} else {
|
||||
setBody();
|
||||
}
|
||||
} else {
|
||||
LOG.warning("Failed to load update log, trying to open it in browser");
|
||||
FXUtils.openLink(url);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
@@ -46,4 +47,12 @@ public final class AnimationUtils {
|
||||
public static boolean playWindowAnimation() {
|
||||
return PLAY_WINDOW_ANIMATION;
|
||||
}
|
||||
|
||||
public static void reset(Node node, boolean opaque) {
|
||||
node.setTranslateX(0);
|
||||
node.setTranslateY(0);
|
||||
node.setScaleX(1);
|
||||
node.setScaleY(1);
|
||||
node.setOpacity(opaque ? 1 : 0);
|
||||
}
|
||||
}
|
||||
@@ -17,36 +17,30 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.*;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public enum ContainerAnimations implements AnimationProducer {
|
||||
public enum ContainerAnimations implements TransitionPane.AnimationProducer {
|
||||
NONE {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getPreviousNode().setTranslateY(0);
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
AnimationUtils.reset(previousNode, false);
|
||||
AnimationUtils.reset(nextNode, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Collections.emptyList();
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return this;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -55,151 +49,50 @@ public enum ContainerAnimations implements AnimationProducer {
|
||||
*/
|
||||
FADE {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getPreviousNode().setTranslateY(0);
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(0);
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH)));
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return this;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A fade between the old and new view
|
||||
*/
|
||||
FADE_IN {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A fade between the old and new view
|
||||
*/
|
||||
FADE_OUT {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* A zoom effect
|
||||
*/
|
||||
ZOOM_IN {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getPreviousNode().setTranslateY(0);
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getPreviousNode().scaleXProperty(), 4, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().scaleYProperty(), 4, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* A zoom effect
|
||||
*/
|
||||
ZOOM_OUT {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getPreviousNode().setTranslateY(0);
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(0);
|
||||
c.getCurrentNode().setTranslateY(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getPreviousNode().scaleXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().scaleYProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* A swipe effect
|
||||
*/
|
||||
SWIPE_LEFT {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(0);
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
AnimationUtils.reset(previousNode, true);
|
||||
AnimationUtils.reset(nextNode, true);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)));
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), container.getWidth(), interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), -container.getWidth(), interpolator)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return SWIPE_RIGHT;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -208,105 +101,154 @@ public enum ContainerAnimations implements AnimationProducer {
|
||||
*/
|
||||
SWIPE_RIGHT {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(0);
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth());
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
AnimationUtils.reset(previousNode, true);
|
||||
AnimationUtils.reset(nextNode, true);
|
||||
nextNode.setTranslateX(-container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH)));
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), -container.getWidth(), interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), container.getWidth(), interpolator)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return SWIPE_LEFT;
|
||||
}
|
||||
},
|
||||
|
||||
SWIPE_LEFT_FADE_SHORT {
|
||||
/// @see <a href="https://m3.material.io/styles/motion/transitions/transition-patterns">Transitions - Material Design 3</a>
|
||||
FORWARD {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(0);
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator)),
|
||||
new KeyFrame(duration.multiply(0.5),
|
||||
new KeyValue(previousNode.translateXProperty(), -offset, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator),
|
||||
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.translateXProperty(), offset, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return BACKWARD;
|
||||
}
|
||||
},
|
||||
|
||||
SWIPE_RIGHT_FADE_SHORT {
|
||||
/// @see <a href="https://m3.material.io/styles/motion/transitions/transition-patterns">Transitions - Material Design 3</a>
|
||||
BACKWARD {
|
||||
@Override
|
||||
public void init(AnimationHandler c) {
|
||||
c.getPreviousNode().setScaleX(1);
|
||||
c.getPreviousNode().setScaleY(1);
|
||||
c.getPreviousNode().setOpacity(0);
|
||||
c.getPreviousNode().setTranslateX(0);
|
||||
c.getCurrentNode().setScaleX(1);
|
||||
c.getCurrentNode().setScaleY(1);
|
||||
c.getCurrentNode().setOpacity(1);
|
||||
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth());
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator)),
|
||||
new KeyFrame(duration.multiply(0.5),
|
||||
new KeyValue(previousNode.translateXProperty(), offset, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator),
|
||||
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.translateXProperty(), -offset, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler c) {
|
||||
return Arrays.asList(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)),
|
||||
new KeyFrame(c.getDuration(),
|
||||
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH),
|
||||
new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return FORWARD;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
private ContainerAnimations opposite;
|
||||
/// Imitates the animation when switching tabs in the Windows 11 Settings interface
|
||||
SLIDE_UP_FADE_IN {
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
double offset = container.getHeight() > 0 ? container.getHeight() * 0.2 : 50;
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(previousNode.translateYProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.translateYProperty(), offset, interpolator)),
|
||||
new KeyFrame(duration.multiply(0.5),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(nextNode.translateYProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
static {
|
||||
NONE.opposite = NONE;
|
||||
FADE.opposite = FADE;
|
||||
SWIPE_LEFT.opposite = SWIPE_RIGHT;
|
||||
SWIPE_RIGHT.opposite = SWIPE_LEFT;
|
||||
FADE_IN.opposite = FADE_OUT;
|
||||
FADE_OUT.opposite = FADE_IN;
|
||||
ZOOM_IN.opposite = ZOOM_OUT;
|
||||
ZOOM_OUT.opposite = ZOOM_IN;
|
||||
}
|
||||
NAVIGATION {
|
||||
@Override
|
||||
public Animation animate(Pane container, Node previousNode, Node nextNode, Duration duration, Interpolator interpolator) {
|
||||
Timeline timeline = new Timeline();
|
||||
Duration halfDuration = duration.divide(2);
|
||||
|
||||
@Override
|
||||
public abstract void init(AnimationHandler handler);
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)));
|
||||
timeline.getKeyFrames().add(new KeyFrame(halfDuration,
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator)));
|
||||
if (previousNode instanceof DecoratorAnimatedPage prevPage) {
|
||||
Node left = prevPage.getLeft();
|
||||
Node center = prevPage.getCenter();
|
||||
|
||||
@Override
|
||||
public abstract List<KeyFrame> animate(AnimationHandler handler);
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(left.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(center.translateXProperty(), 0, interpolator)));
|
||||
timeline.getKeyFrames().add(new KeyFrame(halfDuration,
|
||||
new KeyValue(left.translateXProperty(), -30, interpolator),
|
||||
new KeyValue(center.translateXProperty(), 30, interpolator)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ContainerAnimations opposite() {
|
||||
return opposite;
|
||||
}
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator)));
|
||||
timeline.getKeyFrames().add(new KeyFrame(halfDuration,
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator)));
|
||||
timeline.getKeyFrames().add(new KeyFrame(duration,
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator)));
|
||||
if (nextNode instanceof DecoratorAnimatedPage nextPage) {
|
||||
Node left = nextPage.getLeft();
|
||||
Node center = nextPage.getCenter();
|
||||
|
||||
timeline.getKeyFrames().add(new KeyFrame(halfDuration,
|
||||
new KeyValue(left.translateXProperty(), -30, interpolator),
|
||||
new KeyValue(center.translateXProperty(), 30, interpolator)));
|
||||
timeline.getKeyFrames().add(new KeyFrame(duration,
|
||||
new KeyValue(left.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(center.translateXProperty(), 0, interpolator)));
|
||||
}
|
||||
|
||||
return timeline;
|
||||
}
|
||||
},
|
||||
;
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// @author Glavo
|
||||
/// @see <a href="https://api.flutter.dev/flutter/animation/Curves-class.html">Flutter Curves</a>
|
||||
public final class Motion {
|
||||
|
||||
//region Curves
|
||||
|
||||
/// A linear animation curve.
|
||||
///
|
||||
/// This is the identity map over the unit interval: its [Interpolator#curve(double)]
|
||||
/// method returns its input unmodified. This is useful as a default curve for
|
||||
/// cases where a [Interpolator] is required but no actual curve is desired.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear.mp4">curve_linear.mp4</a>
|
||||
public static final Interpolator LINEAR = Interpolator.LINEAR;
|
||||
|
||||
/// The emphasizedAccelerate easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static final Interpolator EMPHASIZED_ACCELERATE = new Cubic(0.3, 0.0, 0.8, 0.15);
|
||||
|
||||
/// The emphasizedDecelerate easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static final Interpolator EMPHASIZED_DECELERATE = new Cubic(0.05, 0.7, 0.1, 1.0);
|
||||
|
||||
/// The standard easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static final Interpolator STANDARD = new Cubic(0.2, 0.0, 0.0, 1.0);
|
||||
|
||||
/// The standardAccelerate easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static final Interpolator STANDARD_ACCELERATE = new Cubic(0.3, 0.0, 1.0, 1.0);
|
||||
|
||||
/// The standardDecelerate easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static Interpolator STANDARD_DECELERATE = new Cubic(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
/// The legacyDecelerate easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static Interpolator LEGACY_DECELERATE = new Cubic(0.0, 0.0, 0.2, 1.0);
|
||||
|
||||
/// The legacyAccelerate easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static Interpolator LEGACY_ACCELERATE = new Cubic(0.4, 0.0, 1.0, 1.0);
|
||||
|
||||
/// The legacy easing curve in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee)
|
||||
/// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration)
|
||||
public static Interpolator LEGACY = new Cubic(0.4, 0.0, 0.2, 1.0);
|
||||
|
||||
/// A cubic animation curve that speeds up quickly and ends slowly.
|
||||
///
|
||||
/// This is the same as the CSS easing function `ease`.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4">curve_ease.mp4</a>
|
||||
public static final Interpolator EASE = new Cubic(0.25, 0.1, 0.25, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly.
|
||||
///
|
||||
/// This is the same as the CSS easing function `ease-in`.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4">curve_ease_in.mp4</a>
|
||||
public static final Interpolator EASE_IN = new Cubic(0.42, 0.0, 1.0, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends linearly.
|
||||
///
|
||||
/// The symmetric animation to [#LINEAR_TO_EASE_OUT].
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_to_linear.mp4">curve_ease_in_to_linear.mp4</a>
|
||||
public static final Interpolator EASE_IN_TO_LINEAR = new Cubic(0.67, 0.03, 0.65, 0.09);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This is
|
||||
/// similar to [#EASE_IN], but with sinusoidal easing for a slightly less
|
||||
/// abrupt beginning and end. Nonetheless, the result is quite gentle and is
|
||||
/// hard to distinguish from [#linear] at a glance.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_sine.mp4">curve_ease_in_sine.mp4</a>
|
||||
public static final Interpolator EASE_IN_SINE = new Cubic(0.47, 0.0, 0.745, 0.715);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. Based on a
|
||||
/// quadratic equation where `f(t) = t²`, this is effectively the inverse of
|
||||
/// [#decelerate].
|
||||
///
|
||||
/// Compared to [#EASE_IN_SINE], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quad.mp4">curve_ease_in_quad.mp4</a>
|
||||
public static final Interpolator EASE_IN_QUAD = new Cubic(0.55, 0.085, 0.68, 0.53);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This curve is
|
||||
/// based on a cubic equation where `f(t) = t³`. The result is a safe sweet
|
||||
/// spot when choosing a curve for widgets animating off the viewport.
|
||||
///
|
||||
/// Compared to [#EASE_IN_QUAD], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_cubic.mp4">curve_ease_in_cubic.mp4</a>
|
||||
public static final Interpolator EASE_IN_CUBIC = new Cubic(0.55, 0.055, 0.675, 0.19);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This curve is
|
||||
/// based on a quartic equation where `f(t) = t⁴`.
|
||||
///
|
||||
/// Animations using this curve or steeper curves will benefit from a longer
|
||||
/// duration to avoid motion feeling unnatural.
|
||||
///
|
||||
/// Compared to [#EASE_IN_CUBIC], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quart.mp4">curve_ease_in_quart.mp4</a>
|
||||
public static final Interpolator EASE_IN_QUART = new Cubic(0.895, 0.03, 0.685, 0.22);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This curve is
|
||||
/// based on a quintic equation where `f(t) = t⁵`.
|
||||
///
|
||||
/// Compared to [#EASE_IN_QUART], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quint.mp4">curve_ease_in_quint.mp4</a>
|
||||
public static final Interpolator EASE_IN_QUINT = new Cubic(0.755, 0.05, 0.855, 0.06);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This curve is
|
||||
/// based on an exponential equation where `f(t) = 2¹⁰⁽ᵗ⁻¹⁾`.
|
||||
///
|
||||
/// Using this curve can give your animations extra flare, but a longer
|
||||
/// duration may need to be used to compensate for the steepness of the curve.
|
||||
///
|
||||
/// Compared to [#EASE_IN_QUINT], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_expo.mp4">curve_ease_in_expo.mp4</a>
|
||||
public static final Interpolator EASE_IN_EXPO = new Cubic(0.95, 0.05, 0.795, 0.035);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This curve is
|
||||
/// effectively the bottom-right quarter of a circle.
|
||||
///
|
||||
/// Like [#EASE_IN_EXPO], this curve is fairly dramatic and will reduce
|
||||
/// the clarity of an animation if not given a longer duration.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_circ.mp4">curve_ease_in_circ.mp4</a>
|
||||
public static final Interpolator EASE_IN_CIRC = new Cubic(0.6, 0.04, 0.98, 0.335);
|
||||
|
||||
/// A cubic animation curve that starts slowly and ends quickly. This curve
|
||||
/// is similar to [#elasticIn] in that it overshoots its bounds before
|
||||
/// reaching its end. Instead of repeated swinging motions before ascending,
|
||||
/// though, this curve overshoots once, then continues to ascend.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_back.mp4">curve_ease_in_back.mp4</a>
|
||||
public static final Interpolator EASE_IN_BACK = new Cubic(0.6, -0.28, 0.735, 0.045);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly.
|
||||
///
|
||||
/// This is the same as the CSS easing function `ease-out`.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4">curve_ease_out.mp4</a>
|
||||
public static final Interpolator EASE_OUT = new Cubic(0.0, 0.0, 0.58, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts linearly and ends slowly.
|
||||
///
|
||||
/// A symmetric animation to [#EASE_IN_TO_LINEAR].
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear_to_ease_out.mp4">curve_linear_to_ease_out.mp4</a>
|
||||
public static final Interpolator LINEAR_TO_EASE_OUT = new Cubic(0.35, 0.91, 0.33, 0.97);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This is
|
||||
/// similar to [#EASE_OUT], but with sinusoidal easing for a slightly
|
||||
/// less abrupt beginning and end. Nonetheless, the result is quite gentle and
|
||||
/// is hard to distinguish from [#linear] at a glance.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_sine.mp4">curve_ease_out_sine.mp4</a>
|
||||
public static final Interpolator EASE_OUT_SINE = new Cubic(0.39, 0.575, 0.565, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This is
|
||||
/// effectively the same as [#decelerate], only simulated using a cubic
|
||||
/// bezier function.
|
||||
///
|
||||
/// Compared to [#EASE_OUT_SINE], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quad.mp4">curve_ease_out_quad.mp4</a>
|
||||
public static final Interpolator EASE_OUT_QUAD = new Cubic(0.25, 0.46, 0.45, 0.94);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This curve is
|
||||
/// a flipped version of [#EASE_IN_CUBIC].
|
||||
///
|
||||
/// The result is a safe sweet spot when choosing a curve for animating a
|
||||
/// widget's position entering or already inside the viewport.
|
||||
///
|
||||
/// Compared to [#EASE_OUT_QUAD], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_cubic.mp4">curve_ease_out_cubic.mp4</a>
|
||||
public static final Interpolator EASE_OUT_CUBIC = new Cubic(0.215, 0.61, 0.355, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This curve is
|
||||
/// a flipped version of [#EASE_IN_QUART].
|
||||
///
|
||||
/// Animations using this curve or steeper curves will benefit from a longer
|
||||
/// duration to avoid motion feeling unnatural.
|
||||
///
|
||||
/// Compared to [#EASE_OUT_CUBIC], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quart.mp4">curve_ease_out_quart.mp4</a>
|
||||
public static final Interpolator EASE_OUT_QUART = new Cubic(0.165, 0.84, 0.44, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This curve is
|
||||
/// a flipped version of [#EASE_IN_QUINT].
|
||||
///
|
||||
/// Compared to [#EASE_OUT_QUART], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quint.mp4">curve_ease_out_quint.mp4</a>
|
||||
public static final Interpolator EASE_OUT_QUINT = new Cubic(0.23, 1.0, 0.32, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This curve is
|
||||
/// a flipped version of [#EASE_IN_EXPO]. Using this curve can give your
|
||||
/// animations extra flare, but a longer duration may need to be used to
|
||||
/// compensate for the steepness of the curve.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_expo.mp4">curve_ease_out_expo.mp4</a>
|
||||
public static final Interpolator EASE_OUT_EXPO = new Cubic(0.19, 1.0, 0.22, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This curve is
|
||||
/// effectively the top-left quarter of a circle.
|
||||
///
|
||||
/// Like [#EASE_OUT_EXPO], this curve is fairly dramatic and will reduce
|
||||
/// the clarity of an animation if not given a longer duration.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_circ.mp4">curve_ease_out_circ.mp4</a>
|
||||
public static final Interpolator EASE_OUT_CIRC = new Cubic(0.075, 0.82, 0.165, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly and ends slowly. This curve is
|
||||
/// similar to [#elasticOut] in that it overshoots its bounds before
|
||||
/// reaching its end. Instead of repeated swinging motions after ascending,
|
||||
/// though, this curve only overshoots once.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_back.mp4">curve_ease_out_back.mp4</a>
|
||||
public static final Interpolator EASE_OUT_BACK = new Cubic(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly.
|
||||
///
|
||||
/// This is the same as the CSS easing function `ease-in-out`.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out.mp4">curve_ease_in_out.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT = new Cubic(0.42, 0.0, 0.58, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This is similar to [#EASE_IN_OUT], but with sinusoidal easing
|
||||
/// for a slightly less abrupt beginning and end.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_sine.mp4">curve_ease_in_out_sine.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_SINE = new Cubic(0.445, 0.05, 0.55, 0.95);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This curve can be imagined as [#EASE_IN_QUAD] as the first
|
||||
/// half, and [#EASE_OUT_QUAD] as the second.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_SINE], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quad.mp4">curve_ease_in_out_quad.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_QUAD = new Cubic(0.455, 0.03, 0.515, 0.955);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This curve can be imagined as [#EASE_IN_CUBIC] as the first
|
||||
/// half, and [#EASE_OUT_CUBIC] as the second.
|
||||
///
|
||||
/// The result is a safe sweet spot when choosing a curve for a widget whose
|
||||
/// initial and final positions are both within the viewport.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_QUAD], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic.mp4">curve_ease_in_out_cubic.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_CUBIC = new Cubic(0.645, 0.045, 0.355, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This curve can be imagined as [#EASE_IN_QUART] as the first
|
||||
/// half, and [#EASE_OUT_QUART] as the second.
|
||||
///
|
||||
/// Animations using this curve or steeper curves will benefit from a longer
|
||||
/// duration to avoid motion feeling unnatural.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_CUBIC], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quart.mp4">curve_ease_in_out_quart.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_QUART = new Cubic(0.77, 0.0, 0.175, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This curve can be imagined as [#EASE_IN_QUINT] as the first
|
||||
/// half, and [#EASE_OUT_QUINT] as the second.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_QUART], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quint.mp4">curve_ease_in_out_quint.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_QUINT = new Cubic(0.86, 0.0, 0.07, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly.
|
||||
///
|
||||
/// Since this curve is arrived at with an exponential function, the midpoint
|
||||
/// is exceptionally steep. Extra consideration should be taken when designing
|
||||
/// an animation using this.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_QUINT], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_expo.mp4">curve_ease_in_out_expo.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_EXPO = new Cubic(1.0, 0.0, 0.0, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This curve can be imagined as [#EASE_IN_CIRC] as the first
|
||||
/// half, and [#EASE_OUT_CIRC] as the second.
|
||||
///
|
||||
/// Like [#EASE_IN_OUT_EXPO], this curve is fairly dramatic and will reduce
|
||||
/// the clarity of an animation if not given a longer duration.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_EXPO], this curve is slightly steeper.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_circ.mp4">curve_ease_in_out_circ.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_CIRC = new Cubic(0.785, 0.135, 0.15, 0.86);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up shortly thereafter,
|
||||
/// and then ends slowly. This curve can be imagined as a steeper version of
|
||||
/// [#EASE_IN_OUT_CUBIC].
|
||||
///
|
||||
/// The result is a more emphasized eased curve when choosing a curve for a
|
||||
/// widget whose initial and final positions are both within the viewport.
|
||||
///
|
||||
/// Compared to [#EASE_IN_OUT_CUBIC], this curve is slightly steeper.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic_emphasized.mp4">curve_ease_in_out_cubic_emphasized.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_CUBIC_EMPHASIZED = new ThreePointCubic(
|
||||
new Offset(0.05, 0),
|
||||
new Offset(0.133333, 0.06),
|
||||
new Offset(0.166666, 0.4),
|
||||
new Offset(0.208333, 0.82),
|
||||
new Offset(0.25, 1)
|
||||
);
|
||||
|
||||
/// A cubic animation curve that starts slowly, speeds up, and then ends
|
||||
/// slowly. This curve can be imagined as [#EASE_IN_BACK] as the first
|
||||
/// half, and [#EASE_OUT_BACK] as the second.
|
||||
///
|
||||
/// Since two curves are used as a basis for this curve, the resulting
|
||||
/// animation will overshoot its bounds twice before reaching its end - first
|
||||
/// by exceeding its lower bound, then exceeding its upper bound and finally
|
||||
/// descending to its final position.
|
||||
///
|
||||
/// Derived from Robert Penner’s easing functions.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_back.mp4">curve_ease_in_out_back.mp4</a>
|
||||
public static final Interpolator EASE_IN_OUT_BACK = new Cubic(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
/// A curve that starts quickly and eases into its final position.
|
||||
///
|
||||
/// Over the course of the animation, the object spends more time near its
|
||||
/// final destination. As a result, the user isn’t left waiting for the
|
||||
/// animation to finish, and the negative effects of motion are minimized.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4">curve_fast_out_slow_in.mp4</a>
|
||||
public static final Interpolator FAST_OUT_SLOW_IN = new Cubic(0.4, 0.0, 0.2, 1.0);
|
||||
|
||||
/// A cubic animation curve that starts quickly, slows down, and then ends
|
||||
/// quickly.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_slow_middle.mp4">curve_slow_middle.mp4</a>
|
||||
public static final Interpolator SLOW_MIDDLE = new Cubic(0.15, 0.85, 0.85, 0.15);
|
||||
|
||||
private static double bounce(double t) {
|
||||
if (t < 1.0 / 2.75) {
|
||||
return 7.5625 * t * t;
|
||||
} else if (t < 2 / 2.75) {
|
||||
t -= 1.5 / 2.75;
|
||||
return 7.5625 * t * t + 0.75;
|
||||
} else if (t < 2.5 / 2.75) {
|
||||
t -= 2.25 / 2.75;
|
||||
return 7.5625 * t * t + 0.9375;
|
||||
}
|
||||
t -= 2.625 / 2.75;
|
||||
return 7.5625 * t * t + 0.984375;
|
||||
}
|
||||
|
||||
/// An oscillating curve that grows in magnitude.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4">curve_bounce_in.mp4</a>
|
||||
|
||||
public static final Interpolator BOUNCE_IN = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
return 1.0 - bounce(1.0 - t);
|
||||
}
|
||||
};
|
||||
|
||||
/// An oscillating curve that first grows and then shrink in magnitude.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_out.mp4">curve_bounce_out.mp4</a>
|
||||
public static final Interpolator BOUNCE_OUT = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
return bounce(t);
|
||||
}
|
||||
};
|
||||
|
||||
/// An oscillating curve that first grows and then shrink in magnitude.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in_out.mp4">curve_bounce_in_out.mp4</a>
|
||||
public static final Interpolator BOUNCE_IN_OUT = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
if (t < 0.5) {
|
||||
return (1.0 - bounce(1.0 - t * 2.0)) * 0.5;
|
||||
} else {
|
||||
return bounce(t * 2.0 - 1.0) * 0.5 + 0.5;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static final double PERIOD = 0.4;
|
||||
|
||||
/// An oscillating curve that grows in magnitude while overshooting its bounds.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in.mp4">curve_elastic_in.mp4</a>
|
||||
public static final Interpolator ELASTIC_IN = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
final double s = PERIOD / 4.0;
|
||||
t = t - 1.0;
|
||||
return -Math.pow(2.0, 10.0 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD);
|
||||
}
|
||||
};
|
||||
|
||||
/// An oscillating curve that shrinks in magnitude while overshooting its bounds.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_out.mp4">curve_elastic_out.mp4</a>
|
||||
public static Interpolator ELASTIC_OUT = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
final double s = PERIOD / 4.0;
|
||||
return Math.pow(2.0, -10 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD) + 1.0;
|
||||
}
|
||||
};
|
||||
|
||||
/// An oscillating curve that grows and then shrinks in magnitude while overshooting its bounds.
|
||||
///
|
||||
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in_out.mp4">curve_elastic_in_out.mp4</a>
|
||||
public static Interpolator ELASTIC_IN_OUT = new Interpolator() {
|
||||
@Override
|
||||
@SuppressWarnings("DuplicateExpressions")
|
||||
protected double curve(double t) {
|
||||
final double s = PERIOD / 4.0;
|
||||
t = 2.0 * t - 1.0;
|
||||
if (t < 0.0) {
|
||||
return -0.5 * Math.pow(2.0, 10.0 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD);
|
||||
} else {
|
||||
return Math.pow(2.0, -10.0 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD) * 0.5 + 1.0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// A cubic polynomial mapping of the unit interval.
|
||||
private static final class Cubic extends Interpolator {
|
||||
private static final double CUBIC_ERROR_BOUND = 0.001;
|
||||
|
||||
/// The x coordinate of the first control point.
|
||||
///
|
||||
/// The line through the point (0, 0) and the first control point is tangent
|
||||
/// to the curve at the point (0, 0).
|
||||
private final double a;
|
||||
|
||||
/// The y coordinate of the first control point.
|
||||
///
|
||||
/// The line through the point (0, 0) and the first control point is tangent
|
||||
/// to the curve at the point (0, 0).
|
||||
private final double b;
|
||||
|
||||
/// The x coordinate of the second control point.
|
||||
///
|
||||
/// The line through the point (1, 1) and the second control point is tangent
|
||||
/// to the curve at the point (1, 1).
|
||||
private final double c;
|
||||
|
||||
/// The y coordinate of the second control point.
|
||||
///
|
||||
/// The line through the point (1, 1) and the second control point is tangent
|
||||
/// to the curve at the point (1, 1).
|
||||
private final double d;
|
||||
|
||||
private Cubic(double a, double b, double c, double d) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.c = c;
|
||||
this.d = d;
|
||||
}
|
||||
|
||||
double _evaluateCubic(double a, double b, double m) {
|
||||
return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
double start = 0.0;
|
||||
double end = 1.0;
|
||||
while (true) {
|
||||
final double midpoint = (start + end) / 2;
|
||||
final double estimate = _evaluateCubic(a, c, midpoint);
|
||||
if (Math.abs(t - estimate) < CUBIC_ERROR_BOUND) {
|
||||
return _evaluateCubic(b, d, midpoint);
|
||||
}
|
||||
if (estimate < t) {
|
||||
start = midpoint;
|
||||
} else {
|
||||
end = midpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof Cubic cubic
|
||||
&& this.a == cubic.a
|
||||
&& this.b == cubic.b
|
||||
&& this.c == cubic.c
|
||||
&& this.d == cubic.d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(a, b, c, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Cubic[a=%s, b=%s, c=%s, d=%s]".formatted(a, b, c, d);
|
||||
}
|
||||
}
|
||||
|
||||
private record Offset(double dx, double dy) {
|
||||
}
|
||||
|
||||
private static final class ThreePointCubic extends Interpolator {
|
||||
|
||||
/// The coordinates of the first control point of the first curve.
|
||||
///
|
||||
/// The line through the point (0, 0) and this control point is tangent to the
|
||||
/// curve at the point (0, 0).
|
||||
private final Offset a1;
|
||||
|
||||
/// The coordinates of the second control point of the first curve.
|
||||
///
|
||||
/// The line through the [#midpoint] and this control point is tangent to the
|
||||
/// curve approaching the [#midpoint].
|
||||
private final Offset b1;
|
||||
|
||||
/// The coordinates of the middle shared point.
|
||||
///
|
||||
/// The curve will go through this point. If the control points surrounding
|
||||
/// this middle point ([#b1], and [#a2]) are not colinear with this point, then
|
||||
/// the curve's derivative will have a discontinuity (a cusp) at this point.
|
||||
private final Offset midpoint;
|
||||
|
||||
/// The coordinates of the first control point of the second curve.
|
||||
///
|
||||
/// The line through the [#midpoint] and this control point is tangent to the
|
||||
/// curve approaching the [#midpoint].
|
||||
private final Offset a2;
|
||||
|
||||
/// The coordinates of the second control point of the second curve.
|
||||
///
|
||||
/// The line through the point (1, 1) and this control point is tangent to the
|
||||
/// curve at (1, 1).
|
||||
private final Offset b2;
|
||||
|
||||
/// Creates two cubic curves that share a common control point.
|
||||
///
|
||||
/// Rather than creating a new instance, consider using one of the common
|
||||
/// three-point cubic curves in [Interpolator].
|
||||
///
|
||||
/// The arguments correspond to the control points for the two curves,
|
||||
/// including the [#midpoint], but do not include the two implied end points at
|
||||
/// (0,0) and (1,1), which are fixed.
|
||||
private ThreePointCubic(Offset a1, Offset b1, Offset midpoint, Offset a2, Offset b2) {
|
||||
this.a1 = a1;
|
||||
this.b1 = b1;
|
||||
this.midpoint = midpoint;
|
||||
this.a2 = a2;
|
||||
this.b2 = b2;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
final boolean firstCurve = t < midpoint.dx;
|
||||
final double scaleX = firstCurve ? midpoint.dx : 1.0 - midpoint.dx;
|
||||
final double scaleY = firstCurve ? midpoint.dy : 1.0 - midpoint.dy;
|
||||
final double scaledT = (t - (firstCurve ? 0.0 : midpoint.dx)) / scaleX;
|
||||
if (firstCurve) {
|
||||
return new Cubic(
|
||||
a1.dx / scaleX,
|
||||
a1.dy / scaleY,
|
||||
b1.dx / scaleX,
|
||||
b1.dy / scaleY
|
||||
).curve(scaledT) *
|
||||
scaleY;
|
||||
} else {
|
||||
return new Cubic(
|
||||
(a2.dx - midpoint.dx) / scaleX,
|
||||
(a2.dy - midpoint.dy) / scaleY,
|
||||
(b2.dx - midpoint.dx) / scaleX,
|
||||
(b2.dy - midpoint.dy) / scaleY
|
||||
).curve(scaledT) *
|
||||
scaleY +
|
||||
midpoint.dy;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof ThreePointCubic that
|
||||
&& a1.equals(that.a1)
|
||||
&& b1.equals(that.b1)
|
||||
&& midpoint.equals(that.midpoint)
|
||||
&& a2.equals(that.a2)
|
||||
&& b2.equals(that.b2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(a1, b1, midpoint, a2, b2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ThreePointCubic[a1=%s, b1=%s, midpoint=%s, a2=%s, b2=%s]".formatted(a1, b1, midpoint, a2, b2);
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Curves
|
||||
|
||||
// region Durations
|
||||
|
||||
/// The short1 duration (50ms) in the Material specification.
|
||||
public static final Duration SHORT1 = Duration.millis(50);
|
||||
|
||||
/// The short2 duration (100ms) in the Material specification.
|
||||
public static final Duration SHORT2 = Duration.millis(100);
|
||||
|
||||
/// The short3 duration (150ms) in the Material specification.
|
||||
public static final Duration SHORT3 = Duration.millis(150);
|
||||
|
||||
/// The short4 duration (200ms) in the Material specification.
|
||||
public static final Duration SHORT4 = Duration.millis(200);
|
||||
|
||||
/// The medium1 duration (250ms) in the Material specification.
|
||||
public static final Duration MEDIUM1 = Duration.millis(250);
|
||||
|
||||
/// The medium2 duration (300ms) in the Material specification.
|
||||
public static final Duration MEDIUM2 = Duration.millis(300);
|
||||
|
||||
/// The medium3 duration (350ms) in the Material specification.
|
||||
public static final Duration MEDIUM3 = Duration.millis(350);
|
||||
|
||||
/// The medium4 duration (400ms) in the Material specification.
|
||||
public static final Duration MEDIUM4 = Duration.millis(400);
|
||||
|
||||
/// The long1 duration (450ms) in the Material specification.
|
||||
public static final Duration LONG1 = Duration.millis(450);
|
||||
|
||||
/// The long2 duration (500ms) in the Material specification.
|
||||
public static final Duration LONG2 = Duration.millis(500);
|
||||
|
||||
/// The long3 duration (550ms) in the Material specification.
|
||||
public static final Duration LONG3 = Duration.millis(550);
|
||||
|
||||
/// The long4 duration (600ms) in the Material specification.
|
||||
public static final Duration LONG4 = Duration.millis(600);
|
||||
|
||||
/// The extralong1 duration (700ms) in the Material specification.
|
||||
public static final Duration EXTRA_LONG1 = Duration.millis(700);
|
||||
|
||||
/// The extralong2 duration (800ms) in the Material specification.
|
||||
public static final Duration EXTRA_LONG2 = Duration.millis(800);
|
||||
|
||||
/// The extralong3 duration (900ms) in the Material specification.
|
||||
public static final Duration EXTRA_LONG3 = Duration.millis(900);
|
||||
|
||||
/// The extralong4 duration (1000ms) in the Material specification.
|
||||
public static final Duration EXTRA_LONG4 = Duration.millis(1000);
|
||||
|
||||
// endregion Durations
|
||||
|
||||
private Motion() {
|
||||
}
|
||||
}
|
||||
@@ -17,98 +17,103 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.CacheHint;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
private static final Duration DEFAULT_DURATION = Duration.millis(200);
|
||||
public class TransitionPane extends StackPane {
|
||||
|
||||
private Duration duration;
|
||||
private Node previousNode, currentNode;
|
||||
private Node currentNode;
|
||||
|
||||
public TransitionPane() {
|
||||
FXUtils.setOverflowHidden(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getPreviousNode() {
|
||||
return previousNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getCurrentNode() {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StackPane getCurrentRoot() {
|
||||
return this;
|
||||
public final void setContent(Node newView, AnimationProducer transition) {
|
||||
setContent(newView, transition, Motion.SHORT4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getDuration() {
|
||||
return duration;
|
||||
public final void setContent(Node newView, AnimationProducer transition, Duration duration) {
|
||||
setContent(newView, transition, duration, Motion.EASE);
|
||||
}
|
||||
|
||||
public void setContent(Node newView, AnimationProducer transition) {
|
||||
setContent(newView, transition, DEFAULT_DURATION);
|
||||
}
|
||||
public void setContent(Node newView, AnimationProducer transition,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
Node previousNode = currentNode != newView && getWidth() > 0 && getHeight() > 0 ? currentNode : null;
|
||||
currentNode = newView;
|
||||
|
||||
public void setContent(Node newView, AnimationProducer transition, Duration duration) {
|
||||
this.duration = duration;
|
||||
|
||||
updateContent(newView);
|
||||
|
||||
if (previousNode == EMPTY_PANE) {
|
||||
if (!AnimationUtils.isAnimationEnabled() || previousNode == null || transition == ContainerAnimations.NONE) {
|
||||
AnimationUtils.reset(newView, true);
|
||||
getChildren().setAll(newView);
|
||||
return;
|
||||
}
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled() && transition != ContainerAnimations.NONE) {
|
||||
setMouseTransparent(true);
|
||||
transition.init(this);
|
||||
getChildren().setAll(previousNode, newView);
|
||||
|
||||
// runLater or "init" will not work
|
||||
Platform.runLater(() -> {
|
||||
Timeline newAnimation = new Timeline();
|
||||
newAnimation.getKeyFrames().setAll(transition.animate(this));
|
||||
newAnimation.setOnFinished(e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
});
|
||||
FXUtils.playAnimation(this, "transition_pane", newAnimation);
|
||||
setMouseTransparent(true);
|
||||
transition.init(this, previousNode, newView);
|
||||
|
||||
CacheHint cacheHint = newView instanceof Cacheable cacheable
|
||||
? cacheable.getCacheHint(transition)
|
||||
: null;
|
||||
|
||||
if (cacheHint != null) {
|
||||
newView.setCache(true);
|
||||
newView.setCacheHint(cacheHint);
|
||||
}
|
||||
|
||||
// runLater or "init" will not work
|
||||
Platform.runLater(() -> {
|
||||
Animation newAnimation = transition.animate(
|
||||
this,
|
||||
previousNode,
|
||||
newView,
|
||||
duration, interpolator);
|
||||
newAnimation.setOnFinished(e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
|
||||
if (cacheHint != null) {
|
||||
newView.setCache(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getChildren().remove(previousNode);
|
||||
FXUtils.playAnimation(this, "transition_pane", newAnimation);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public interface AnimationProducer {
|
||||
default void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
AnimationUtils.reset(previousNode, true);
|
||||
AnimationUtils.reset(nextNode, false);
|
||||
}
|
||||
|
||||
Animation animate(Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator);
|
||||
|
||||
default @Nullable TransitionPane.AnimationProducer opposite() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateContent(Node newView) {
|
||||
if (getWidth() > 0 && getHeight() > 0) {
|
||||
previousNode = currentNode;
|
||||
if (previousNode == null) {
|
||||
if (getChildren().isEmpty())
|
||||
previousNode = EMPTY_PANE;
|
||||
else
|
||||
previousNode = getChildren().get(0);
|
||||
}
|
||||
} else
|
||||
previousNode = EMPTY_PANE;
|
||||
|
||||
if (previousNode == newView)
|
||||
previousNode = EMPTY_PANE;
|
||||
|
||||
currentNode = newView;
|
||||
|
||||
getChildren().setAll(previousNode, currentNode);
|
||||
}
|
||||
|
||||
private final EmptyPane EMPTY_PANE = new EmptyPane();
|
||||
|
||||
public static class EmptyPane extends StackPane {
|
||||
/// Marks a node as cacheable as a bitmap during animation.
|
||||
public interface Cacheable {
|
||||
/// @return the [cache hint][CacheHint] to use when caching this node during the given animation,
|
||||
/// or `null` to not cache it.
|
||||
default @Nullable CacheHint getCacheHint(AnimationProducer animationProducer) {
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4789
|
||||
return animationProducer == ContainerAnimations.SLIDE_UP_FADE_IN ? CacheHint.SPEED : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,19 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
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;
|
||||
import javafx.scene.paint.Paint;
|
||||
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.versions.VersionPage;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
@@ -98,6 +103,28 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return add(item);
|
||||
}
|
||||
|
||||
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
|
||||
SVG unselectedGraphic, SVG selectedGraphic) {
|
||||
AdvancedListItem item = createNavigationDrawerItem(title, null);
|
||||
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
|
||||
item.setOnAction(e -> tabHeader.select(tab));
|
||||
|
||||
Node unselectedIcon = unselectedGraphic.createIcon((Paint) null, 20);
|
||||
Node selectedIcon = selectedGraphic.createIcon((Paint) null, 20);
|
||||
|
||||
TransitionPane leftGraphic = new TransitionPane();
|
||||
leftGraphic.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(leftGraphic, 30);
|
||||
FXUtils.setLimitHeight(leftGraphic, 20);
|
||||
leftGraphic.setPadding(Insets.EMPTY);
|
||||
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
|
||||
FXUtils.onChange(item.activeProperty(), active ->
|
||||
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
|
||||
|
||||
item.setLeftGraphic(leftGraphic);
|
||||
return add(item);
|
||||
}
|
||||
|
||||
public AdvancedListBox add(int index, Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(index, child);
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -29,6 +25,8 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
@@ -36,6 +34,7 @@ import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -71,10 +70,8 @@ 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");
|
||||
|
||||
@@ -83,9 +80,8 @@ final class ComponentListCell extends StackPane {
|
||||
VBox groupNode = new VBox();
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(Theme.blackFill(), 20);
|
||||
JFXButton expandButton = new JFXButton();
|
||||
expandButton.setGraphic(expandIcon);
|
||||
expandButton.getStyleClass().add("options-list-item-expand-button");
|
||||
expandIcon.setMouseTransparent(true);
|
||||
HBox.setMargin(expandIcon, new Insets(0, 8, 0, 8));
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
@@ -124,19 +120,27 @@ final class ComponentListCell extends StackPane {
|
||||
if (rightNode != null)
|
||||
header.getChildren().add(rightNode);
|
||||
}
|
||||
header.getChildren().add(expandButton);
|
||||
header.getChildren().add(expandIcon);
|
||||
|
||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||
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);
|
||||
groupNode.getChildren().add(container);
|
||||
|
||||
Runnable onExpand = () -> {
|
||||
headerRippler.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
event.consume();
|
||||
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
@@ -149,19 +153,27 @@ 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;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (expanded) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
||||
));
|
||||
double currentRotate = expandIcon.getRotate();
|
||||
Duration duration = Motion.LONG2.multiply(Math.abs(currentRotate - targetRotate) / 180.0);
|
||||
Interpolator interpolator = Motion.EASE_IN_OUT_CUBIC_EMPHASIZED;
|
||||
|
||||
expandAnimation = new Timeline(
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, interpolator),
|
||||
new KeyValue(expandIcon.rotateProperty(), targetRotate, interpolator))
|
||||
);
|
||||
|
||||
if (!expanded) {
|
||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||
@@ -171,18 +183,14 @@ final class ComponentListCell extends StackPane {
|
||||
} else {
|
||||
container.setMinHeight(contentHeight);
|
||||
container.setMaxHeight(contentHeight);
|
||||
expandIcon.setRotate(targetRotate);
|
||||
|
||||
if (!expanded) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
FXUtils.onClicked(headerRippler, onExpand);
|
||||
expandButton.setOnAction(e -> onExpand.run());
|
||||
|
||||
expandedProperty().addListener((a, b, newValue) -> expandIcon.setRotate(newValue ? 180 : 0));
|
||||
});
|
||||
|
||||
getChildren().setAll(groupNode);
|
||||
} else {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -26,12 +27,14 @@ import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
|
||||
@@ -47,7 +50,7 @@ public class Navigator extends TransitionPane {
|
||||
public void init(Node init) {
|
||||
stack.push(init);
|
||||
backable.set(canGoBack());
|
||||
getChildren().setAll(init);
|
||||
setContent(init, ContainerAnimations.NONE);
|
||||
|
||||
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
|
||||
if (init instanceof PageAware) ((PageAware) init).onPageShown();
|
||||
@@ -56,6 +59,10 @@ public class Navigator extends TransitionPane {
|
||||
}
|
||||
|
||||
public void navigate(Node node, AnimationProducer animationProducer) {
|
||||
navigate(node, animationProducer, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
public void navigate(Node node, AnimationProducer animationProducer, Duration duration, Interpolator interpolator) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
if (!initialized)
|
||||
@@ -75,7 +82,7 @@ public class Navigator extends TransitionPane {
|
||||
node.fireEvent(navigating);
|
||||
|
||||
node.getProperties().put("hmcl.navigator.animation", animationProducer);
|
||||
setContent(node, animationProducer);
|
||||
setContent(node, animationProducer, duration, interpolator);
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
|
||||
node.fireEvent(navigated);
|
||||
@@ -113,7 +120,7 @@ public class Navigator extends TransitionPane {
|
||||
Node poppedNode = stack.pop();
|
||||
NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED);
|
||||
poppedNode.fireEvent(exited);
|
||||
if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden();
|
||||
if (poppedNode instanceof PageAware pageAware) pageAware.onPageHidden();
|
||||
|
||||
backable.set(canGoBack());
|
||||
Node node = stack.peek();
|
||||
@@ -123,8 +130,8 @@ public class Navigator extends TransitionPane {
|
||||
node.fireEvent(navigating);
|
||||
|
||||
Object obj = from.getProperties().get("hmcl.navigator.animation");
|
||||
if (obj instanceof AnimationProducer) {
|
||||
setContent(node, (AnimationProducer) obj);
|
||||
if (obj instanceof AnimationProducer animationProducer) {
|
||||
setContent(node, Objects.requireNonNullElse(animationProducer.opposite(), animationProducer));
|
||||
} else {
|
||||
setContent(node, ContainerAnimations.NONE);
|
||||
}
|
||||
@@ -160,12 +167,13 @@ public class Navigator extends TransitionPane {
|
||||
return stack.size();
|
||||
}
|
||||
|
||||
public void setContent(Node content, AnimationProducer animationProducer) {
|
||||
super.setContent(content, animationProducer);
|
||||
@Override
|
||||
public void setContent(Node newView, AnimationProducer transition, Duration duration, Interpolator interpolator) {
|
||||
super.setContent(newView, transition, duration, interpolator);
|
||||
|
||||
if (content instanceof Region) {
|
||||
((Region) content).setMinSize(0, 0);
|
||||
FXUtils.setOverflowHidden((Region) content);
|
||||
if (newView instanceof Region region) {
|
||||
region.setMinSize(0, 0);
|
||||
FXUtils.setOverflowHidden(region);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +189,7 @@ public class Navigator extends TransitionPane {
|
||||
this.onNavigated.set(onNavigated);
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<NavigationEvent>> onNavigated = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigated") {
|
||||
private final ObjectProperty<EventHandler<NavigationEvent>> onNavigated = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigated") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(NavigationEvent.NAVIGATED, get());
|
||||
@@ -200,14 +208,14 @@ public class Navigator extends TransitionPane {
|
||||
this.onNavigating.set(onNavigating);
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<NavigationEvent>> onNavigating = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigating") {
|
||||
private final ObjectProperty<EventHandler<NavigationEvent>> onNavigating = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigating") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(NavigationEvent.NAVIGATING, get());
|
||||
}
|
||||
};
|
||||
|
||||
public static class NavigationEvent extends Event {
|
||||
public static final class NavigationEvent extends Event {
|
||||
public static final EventType<NavigationEvent> EXITED = new EventType<>("EXITED");
|
||||
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
|
||||
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXRippler;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
@@ -40,6 +39,7 @@ import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.List;
|
||||
@@ -109,7 +109,7 @@ public class RipplerContainer extends StackPane {
|
||||
setOnMouseEntered(e -> new Transition() {
|
||||
{
|
||||
setCycleDuration(DURATION);
|
||||
setInterpolator(Interpolator.EASE_IN);
|
||||
setInterpolator(Motion.EASE_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,7 +121,7 @@ public class RipplerContainer extends StackPane {
|
||||
setOnMouseExited(e -> new Transition() {
|
||||
{
|
||||
setCycleDuration(DURATION);
|
||||
setInterpolator(Interpolator.EASE_OUT);
|
||||
setInterpolator(Motion.EASE_OUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -174,7 +174,7 @@ public interface TabControl {
|
||||
}
|
||||
}
|
||||
|
||||
class Tab<T extends Node> {
|
||||
final class Tab<T extends Node> {
|
||||
private final StringProperty id = new SimpleStringProperty(this, "id");
|
||||
private final StringProperty text = new SimpleStringProperty(this, "text");
|
||||
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
|
||||
|
||||
@@ -36,54 +36,73 @@ import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class TabHeader extends Control implements TabControl, PageAware {
|
||||
|
||||
private final TransitionPane contentPane;
|
||||
|
||||
public TabHeader(Tab<?>... tabs) {
|
||||
this(null, tabs);
|
||||
}
|
||||
|
||||
public TabHeader(@Nullable TransitionPane contentPane, Tab<?>... tabs) {
|
||||
this.contentPane = contentPane;
|
||||
|
||||
getStyleClass().setAll("tab-header");
|
||||
if (tabs != null) {
|
||||
getTabs().addAll(tabs);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableList<Tab<?>> tabs = FXCollections.observableArrayList();
|
||||
private ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP);
|
||||
private final ObservableList<Tab<?>> tabs = FXCollections.observableArrayList();
|
||||
|
||||
@Override
|
||||
public ObservableList<Tab<?>> getTabs() {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
private final ObjectProperty<SingleSelectionModel<Tab<?>>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControlSelectionModel(this));
|
||||
private final SingleSelectionModel<Tab<?>> selectionModel = new TabControlSelectionModel(this);
|
||||
|
||||
public SingleSelectionModel<Tab<?>> getSelectionModel() {
|
||||
return selectionModel.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<SingleSelectionModel<Tab<?>>> selectionModelProperty() {
|
||||
return selectionModel;
|
||||
}
|
||||
|
||||
public void setSelectionModel(SingleSelectionModel<Tab<?>> selectionModel) {
|
||||
this.selectionModel.set(selectionModel);
|
||||
public void select(Tab<?> tab) {
|
||||
select(tab, true);
|
||||
}
|
||||
|
||||
public void select(Tab<?> tab) {
|
||||
public void select(Tab<?> tab, boolean playAnimation) {
|
||||
Tab<?> oldTab = getSelectionModel().getSelectedItem();
|
||||
if (oldTab != null) {
|
||||
if (oldTab.getNode() instanceof PageAware) {
|
||||
((PageAware) oldTab.getNode()).onPageHidden();
|
||||
if (oldTab.getNode() instanceof PageAware pageAware) {
|
||||
pageAware.onPageHidden();
|
||||
}
|
||||
}
|
||||
|
||||
tab.initializeIfNeeded();
|
||||
if (tab.getNode() instanceof PageAware) {
|
||||
((PageAware) tab.getNode()).onPageShown();
|
||||
if (tab.getNode() instanceof PageAware pageAware) {
|
||||
pageAware.onPageShown();
|
||||
}
|
||||
|
||||
getSelectionModel().select(tab);
|
||||
|
||||
if (contentPane != null) {
|
||||
if (playAnimation && contentPane.getCurrentNode() != null) {
|
||||
contentPane.setContent(tab.getNode(),
|
||||
ContainerAnimations.SLIDE_UP_FADE_IN,
|
||||
Motion.MEDIUM4,
|
||||
Motion.EASE_IN_OUT_CUBIC_EMPHASIZED
|
||||
);
|
||||
} else {
|
||||
contentPane.setContent(tab.getNode(), ContainerAnimations.NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,12 +125,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The position to place the tabs.
|
||||
*/
|
||||
public Side getSide() {
|
||||
return side.get();
|
||||
}
|
||||
private final ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP);
|
||||
|
||||
/**
|
||||
* The position of the tabs.
|
||||
@@ -120,6 +134,13 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
return side;
|
||||
}
|
||||
|
||||
/**
|
||||
* The position to place the tabs.
|
||||
*/
|
||||
public Side getSide() {
|
||||
return side.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The position the place the tabs in this TabHeader.
|
||||
*/
|
||||
@@ -170,14 +191,14 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
this.selectedTab = control.getSelectionModel().getSelectedItem();
|
||||
}
|
||||
|
||||
protected class HeaderContainer extends StackPane {
|
||||
protected final class HeaderContainer extends StackPane {
|
||||
private Timeline timeline;
|
||||
private StackPane selectedTabLine;
|
||||
private HeadersRegion headersRegion;
|
||||
private Scale scale = new Scale(1, 1, 0, 0);
|
||||
private Rotate rotate = new Rotate(0, 0, 1);
|
||||
private double selectedTabLineOffset;
|
||||
private ObservableList<Node> binding;
|
||||
private final StackPane selectedTabLine;
|
||||
private final HeadersRegion headersRegion;
|
||||
private final Scale scale = new Scale(1, 1, 0, 0);
|
||||
private final Rotate rotate = new Rotate(0, 0, 1);
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final ObservableList<Node> binding;
|
||||
|
||||
public HeaderContainer() {
|
||||
getStyleClass().add("tab-header-area");
|
||||
@@ -232,13 +253,12 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
protected void invalidated() {
|
||||
super.invalidated();
|
||||
|
||||
switch (get()) {
|
||||
case TOP: action = new Top(); break;
|
||||
case BOTTOM: action = new Bottom(); break;
|
||||
case LEFT: action = new Left(); break;
|
||||
case RIGHT: action = new Right(); break;
|
||||
default: throw new InternalError();
|
||||
}
|
||||
action = switch (get()) {
|
||||
case TOP -> new Top();
|
||||
case BOTTOM -> new Bottom();
|
||||
case LEFT -> new Left();
|
||||
case RIGHT -> new Right();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,7 +289,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
action.layoutChildren();
|
||||
}
|
||||
|
||||
protected void animateSelectionLine() {
|
||||
private void animateSelectionLine() {
|
||||
action.animateSelectionLine();
|
||||
}
|
||||
|
||||
@@ -325,7 +345,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
double oldWidth = lineWidth * oldScaleX;
|
||||
double oldTransX = selectedTabLine.getTranslateX();
|
||||
double newScaleX = newWidth * oldScaleX / oldWidth;
|
||||
selectedTabLineOffset = newTransX;
|
||||
// newTransX += offsetStart * (double)this.direction;
|
||||
double transDiff = newTransX - oldTransX;
|
||||
if (transDiff < 0.0D) {
|
||||
@@ -465,7 +484,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
|
||||
double oldHeight = lineHeight * oldScaleY;
|
||||
double oldTransY = selectedTabLine.getTranslateY();
|
||||
double newScaleY = newHeight * oldScaleY / oldHeight;
|
||||
selectedTabLineOffset = newTransY;
|
||||
// newTransY += offsetStart * (double)this.direction;
|
||||
double transDiff = newTransY - oldTransY;
|
||||
if (transDiff < 0.0D) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -40,6 +41,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,22 +86,10 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
}
|
||||
});
|
||||
|
||||
speedEventHandler = speedEvent -> {
|
||||
String unit = "B/s";
|
||||
double speed = speedEvent.getSpeed();
|
||||
if (speed > 1024) {
|
||||
speed /= 1024;
|
||||
unit = "KiB/s";
|
||||
}
|
||||
if (speed > 1024) {
|
||||
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);
|
||||
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
|
||||
String message = I18n.formatSpeed(speedEvent.getSpeed());
|
||||
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() {
|
||||
|
||||
@@ -41,7 +41,9 @@ import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
public class Decorator extends Control {
|
||||
private final ListProperty<Node> drawer = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||
@@ -81,19 +83,19 @@ public class Decorator extends Control {
|
||||
if (playRestoreMinimizeAnimation && !iconified) {
|
||||
playRestoreMinimizeAnimation = false;
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(this.opacityProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(this.translateYProperty(), 200, FXUtils.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE)
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 200, Motion.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 0.4, Motion.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 0.4, Motion.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 0.4, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(200),
|
||||
new KeyValue(this.opacityProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE)
|
||||
new KeyFrame(Motion.SHORT4,
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 1, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.play();
|
||||
@@ -273,22 +275,22 @@ public class Decorator extends Control {
|
||||
}
|
||||
|
||||
public void minimize() {
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
if (AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
|
||||
playRestoreMinimizeAnimation = true;
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(this.opacityProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE)
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(this.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 0, Motion.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 1, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(200),
|
||||
new KeyValue(this.opacityProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(this.translateYProperty(), 200, FXUtils.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE)
|
||||
new KeyFrame(Motion.SHORT4,
|
||||
new KeyValue(this.opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(this.translateYProperty(), 200, Motion.EASE),
|
||||
new KeyValue(this.scaleXProperty(), 0.4, Motion.EASE),
|
||||
new KeyValue(this.scaleYProperty(), 0.4, Motion.EASE),
|
||||
new KeyValue(this.scaleZProperty(), 0.4, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.setOnFinished(event -> primaryStage.setIconified(true));
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.scene.Node;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationHandler;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DecoratorAnimationProducer implements AnimationProducer {
|
||||
@Override
|
||||
public void init(AnimationHandler handler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KeyFrame> animate(AnimationHandler handler) {
|
||||
Node prev = handler.getPreviousNode();
|
||||
Node next = handler.getCurrentNode();
|
||||
if (prev instanceof TransitionPane.EmptyPane) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Duration halfDuration = handler.getDuration().divide(2);
|
||||
|
||||
List<KeyFrame> keyFrames = new ArrayList<>();
|
||||
|
||||
keyFrames.add(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(prev.opacityProperty(), 1, Interpolator.EASE_BOTH)));
|
||||
keyFrames.add(new KeyFrame(halfDuration,
|
||||
new KeyValue(prev.opacityProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
if (prev instanceof DecoratorAnimatedPage) {
|
||||
Node left = ((DecoratorAnimatedPage) prev).getLeft();
|
||||
Node center = ((DecoratorAnimatedPage) prev).getCenter();
|
||||
|
||||
keyFrames.add(new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(left.translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(center.translateXProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
keyFrames.add(new KeyFrame(halfDuration,
|
||||
new KeyValue(left.translateXProperty(), -30, Interpolator.EASE_BOTH),
|
||||
new KeyValue(center.translateXProperty(), 30, Interpolator.EASE_BOTH)));
|
||||
}
|
||||
|
||||
keyFrames.add(new KeyFrame(halfDuration,
|
||||
new KeyValue(next.opacityProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
keyFrames.add(new KeyFrame(handler.getDuration(),
|
||||
new KeyValue(next.opacityProperty(), 1, Interpolator.EASE_BOTH)));
|
||||
if (next instanceof DecoratorAnimatedPage) {
|
||||
Node left = ((DecoratorAnimatedPage) next).getLeft();
|
||||
Node center = ((DecoratorAnimatedPage) next).getCenter();
|
||||
|
||||
keyFrames.add(new KeyFrame(halfDuration,
|
||||
new KeyValue(left.translateXProperty(), -30, Interpolator.EASE_BOTH),
|
||||
new KeyValue(center.translateXProperty(), 30, Interpolator.EASE_BOTH)));
|
||||
keyFrames.add(new KeyFrame(handler.getDuration(),
|
||||
new KeyValue(left.translateXProperty(), 0, Interpolator.EASE_BOTH),
|
||||
new KeyValue(center.translateXProperty(), 0, Interpolator.EASE_BOTH)));
|
||||
}
|
||||
|
||||
return keyFrames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable AnimationProducer opposite() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
@@ -49,8 +50,8 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.*;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
@@ -90,16 +91,16 @@ public class DecoratorController {
|
||||
if (AnimationUtils.playWindowAnimation()) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.millis(0),
|
||||
new KeyValue(decorator.opacityProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.scaleXProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.scaleYProperty(), 1, FXUtils.EASE),
|
||||
new KeyValue(decorator.scaleZProperty(), 0.3, FXUtils.EASE)
|
||||
new KeyValue(decorator.opacityProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(decorator.scaleZProperty(), 0.3, Motion.EASE)
|
||||
),
|
||||
new KeyFrame(Duration.millis(200),
|
||||
new KeyValue(decorator.opacityProperty(), 0, FXUtils.EASE),
|
||||
new KeyValue(decorator.scaleXProperty(), 0.8, FXUtils.EASE),
|
||||
new KeyValue(decorator.scaleYProperty(), 0.8, FXUtils.EASE),
|
||||
new KeyValue(decorator.scaleZProperty(), 0.8, FXUtils.EASE)
|
||||
new KeyValue(decorator.opacityProperty(), 0, Motion.EASE),
|
||||
new KeyValue(decorator.scaleXProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.scaleYProperty(), 0.8, Motion.EASE),
|
||||
new KeyValue(decorator.scaleZProperty(), 0.8, Motion.EASE)
|
||||
)
|
||||
);
|
||||
timeline.setOnFinished(event -> Launcher.stopApplication());
|
||||
@@ -364,10 +365,8 @@ public class DecoratorController {
|
||||
|
||||
// ==== Navigation ====
|
||||
|
||||
private static final DecoratorAnimationProducer animation = new DecoratorAnimationProducer();
|
||||
|
||||
public void navigate(Node node) {
|
||||
navigator.navigate(node, animation);
|
||||
public void navigate(Node node, AnimationProducer animationProducer, Duration duration, Interpolator interpolator) {
|
||||
navigator.navigate(node, animationProducer, duration, interpolator);
|
||||
}
|
||||
|
||||
private void close() {
|
||||
@@ -458,7 +457,9 @@ public class DecoratorController {
|
||||
Platform.runLater(() -> showDialog(node));
|
||||
return;
|
||||
}
|
||||
dialog = new JFXDialog();
|
||||
dialog = new JFXDialog(AnimationUtils.isAnimationEnabled()
|
||||
? JFXDialog.DialogTransition.CENTER
|
||||
: JFXDialog.DialogTransition.NONE);
|
||||
dialogPane = new JFXDialogPane();
|
||||
|
||||
dialog.setContent(dialogPane);
|
||||
@@ -537,7 +538,8 @@ public class DecoratorController {
|
||||
public void startWizard(WizardProvider wizardProvider, String category) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE);
|
||||
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category),
|
||||
ContainerAnimations.FORWARD, Motion.SHORT4, Motion.EASE);
|
||||
}
|
||||
|
||||
// ==== Authlib Injector DnD ====
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
|
||||
public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage {
|
||||
protected final Navigator navigator = new Navigator();
|
||||
|
||||
{
|
||||
this.navigator.setOnNavigating(this::onNavigating);
|
||||
this.navigator.setOnNavigated(this::onNavigated);
|
||||
backableProperty().bind(navigator.backableProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void navigate(Node page, AnimationProducer animationProducer) {
|
||||
navigator.navigate(page, animationProducer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean back() {
|
||||
if (navigator.canGoBack()) {
|
||||
navigator.close();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onNavigating(Navigator.NavigationEvent event) {
|
||||
if (event.getSource() != this.navigator) return;
|
||||
onNavigating(event.getNode());
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (event.getSource() != this.navigator) return;
|
||||
onNavigated(event.getNode());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -39,12 +43,13 @@ import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -205,16 +210,13 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
|
||||
if (s.isAnimate()) {
|
||||
AnimationProducer animation;
|
||||
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) {
|
||||
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT;
|
||||
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) {
|
||||
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT;
|
||||
} else {
|
||||
animation = ContainerAnimations.FADE;
|
||||
}
|
||||
TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
default -> ContainerAnimations.FADE;
|
||||
};
|
||||
skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
|
||||
navBarPane.setContent(node, animation);
|
||||
navBarPane.setContent(node, animation, Motion.SHORT4);
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(node);
|
||||
}
|
||||
@@ -500,4 +502,66 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NavBarAnimations implements TransitionPane.AnimationProducer {
|
||||
NEXT {
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline animate(
|
||||
Pane container, Node previousNode, Node nextNode,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), 50, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), -50, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return NEXT;
|
||||
}
|
||||
},
|
||||
|
||||
PREVIOUS {
|
||||
@Override
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline animate(Pane container, Node previousNode, Node nextNode, Duration duration, Interpolator interpolator) {
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.translateXProperty(), -50, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 1, interpolator)),
|
||||
new KeyFrame(duration,
|
||||
new KeyValue(nextNode.translateXProperty(), 0, interpolator),
|
||||
new KeyValue(previousNode.translateXProperty(), 50, interpolator),
|
||||
new KeyValue(nextNode.opacityProperty(), 1, interpolator),
|
||||
new KeyValue(previousNode.opacityProperty(), 0, interpolator))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransitionPane.AnimationProducer opposite() {
|
||||
return PREVIOUS;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.SingleSelectionModel;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.TabControl;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
|
||||
public abstract class DecoratorTabPage extends DecoratorTransitionPage implements TabControl {
|
||||
|
||||
public DecoratorTabPage() {
|
||||
getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
newValue.initializeIfNeeded();
|
||||
if (newValue.getNode() != null) {
|
||||
onNavigating(getCurrentPage());
|
||||
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING));
|
||||
navigate(newValue.getNode(), ContainerAnimations.FADE);
|
||||
onNavigated(getCurrentPage());
|
||||
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public DecoratorTabPage(TabHeader.Tab<?>... tabs) {
|
||||
this();
|
||||
if (tabs != null) {
|
||||
getTabs().addAll(tabs);
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableList<TabHeader.Tab<?>> tabs = FXCollections.observableArrayList();
|
||||
|
||||
@Override
|
||||
public ObservableList<TabHeader.Tab<?>> getTabs() {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
private final ObjectProperty<SingleSelectionModel<TabHeader.Tab<?>>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControl.TabControlSelectionModel(this));
|
||||
|
||||
public SingleSelectionModel<TabHeader.Tab<?>> getSelectionModel() {
|
||||
return selectionModel.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<SingleSelectionModel<TabHeader.Tab<?>>> selectionModelProperty() {
|
||||
return selectionModel;
|
||||
}
|
||||
|
||||
public void setSelectionModel(SingleSelectionModel<TabHeader.Tab<?>> selectionModel) {
|
||||
this.selectionModel.set(selectionModel);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.Node;
|
||||
@@ -24,7 +25,7 @@ import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
|
||||
@@ -36,8 +37,8 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
|
||||
private Node currentPage;
|
||||
protected final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
protected void navigate(Node page, AnimationProducer animation) {
|
||||
transitionPane.setContent(currentPage = page, animation);
|
||||
protected void navigate(Node page, TransitionPane.AnimationProducer animation, Duration duration, Interpolator interpolator) {
|
||||
transitionPane.setContent(currentPage = page, animation, duration, interpolator);
|
||||
}
|
||||
|
||||
protected void onNavigating(Node from) {
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.decorator;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.wizard.*;
|
||||
@@ -74,7 +75,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
|
||||
@Override
|
||||
public void navigateTo(Node page, Navigation.NavigationDirection nav) {
|
||||
displayer.navigateTo(page, nav);
|
||||
navigate(page, nav.getAnimation());
|
||||
navigate(page, nav.getAnimation(), Motion.SHORT4, Motion.EASE);
|
||||
|
||||
String prefix = category == null ? "" : category + " - ";
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
@@ -60,7 +61,16 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
}
|
||||
|
||||
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
|
||||
controller.onNext(
|
||||
new VersionsPage(
|
||||
controller,
|
||||
i18n("install.installer.choose", i18n("install.installer." + libraryId)),
|
||||
gameVersion,
|
||||
downloadProvider,
|
||||
libraryId,
|
||||
() -> controller.onPrev(false, Navigation.NavigationDirection.NEXT)
|
||||
), Navigation.NavigationDirection.NEXT
|
||||
);
|
||||
});
|
||||
library.setOnRemove(() -> {
|
||||
controller.getSettings().remove(libraryId);
|
||||
|
||||
@@ -36,11 +36,9 @@ 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.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TabControl;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -100,23 +98,20 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, (profile, version, file) -> download(profile, version, file, "shaderpacks"), true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
|
||||
tab.select(newGameTab);
|
||||
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.startCategory(i18n("download.game").toUpperCase(Locale.ROOT))
|
||||
.addNavigationDrawerTab(tab, newGameTab, i18n("game"), SVG.STADIA_CONTROLLER)
|
||||
.addNavigationDrawerTab(tab, modpackTab, i18n("modpack"), SVG.PACKAGE2)
|
||||
.addNavigationDrawerTab(tab, newGameTab, i18n("game"), SVG.STADIA_CONTROLLER, SVG.STADIA_CONTROLLER_FILL)
|
||||
.addNavigationDrawerTab(tab, modpackTab, i18n("modpack"), SVG.PACKAGE2, SVG.PACKAGE2_FILL)
|
||||
.startCategory(i18n("download.content").toUpperCase(Locale.ROOT))
|
||||
.addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION)
|
||||
.addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION, SVG.EXTENSION_FILL)
|
||||
.addNavigationDrawerTab(tab, resourcePackTab, i18n("resourcepack"), SVG.TEXTURE)
|
||||
.addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY)
|
||||
.addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL)
|
||||
.addNavigationDrawerTab(tab, worldTab, i18n("world"), SVG.PUBLIC);
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -124,13 +119,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
private void selectTabIfCurseForgeAvailable(TabControl.Tab<?> newTab) {
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
tab.select(newTab);
|
||||
else
|
||||
Controllers.dialog(i18n("download.curseforge.unavailable"));
|
||||
}
|
||||
|
||||
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
@@ -205,20 +193,20 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
public void showGameDownloads() {
|
||||
tab.select(newGameTab);
|
||||
tab.select(newGameTab, false);
|
||||
}
|
||||
|
||||
public void showModpackDownloads() {
|
||||
tab.select(modpackTab);
|
||||
tab.select(modpackTab, false);
|
||||
}
|
||||
|
||||
public DownloadListPage showModDownloads() {
|
||||
tab.select(modTab);
|
||||
tab.select(modTab, false);
|
||||
return modTab.getNode();
|
||||
}
|
||||
|
||||
public void showWorldDownloads() {
|
||||
tab.select(worldTab);
|
||||
tab.select(worldTab, false);
|
||||
}
|
||||
|
||||
private static final class DownloadNavigator implements Navigation {
|
||||
|
||||
@@ -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,14 +50,20 @@ 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;
|
||||
@@ -65,8 +72,8 @@ 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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -223,7 +233,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
twoLineListItem.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
|
||||
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
} else {
|
||||
@@ -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 SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
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,17 @@ 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
|
||||
|| versionType == RemoteVersion.Type.UNOBFUSCATED;
|
||||
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();
|
||||
|
||||
+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");
|
||||
}
|
||||
}
|
||||
@@ -138,8 +138,11 @@ public final class AboutPage extends StackPane {
|
||||
else if (obj.has("subtitleLocalized"))
|
||||
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
|
||||
|
||||
if (obj.has("externalLink"))
|
||||
item.setExternalLink(obj.get("externalLink").getAsString());
|
||||
if (obj.has("externalLink")) {
|
||||
String link = obj.get("externalLink").getAsString();
|
||||
item.setExternalLink(link);
|
||||
FXUtils.installFastTooltip(item.getExternalLinkButton(), link);
|
||||
}
|
||||
|
||||
componentList.getContent().add(item);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
|
||||
}
|
||||
|
||||
void onShowRestoreJavaPage() {
|
||||
Controllers.navigate(new JavaRestorePage(ConfigHolder.globalConfig().getDisabledJava()));
|
||||
Controllers.navigateForward(new JavaRestorePage(ConfigHolder.globalConfig().getDisabledJava()));
|
||||
}
|
||||
|
||||
private void onAddJavaBinary(Path file) {
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
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.decorator.DecoratorAnimatedPage;
|
||||
@@ -56,26 +55,22 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
helpTab.setNodeSupplier(HelpPage::new);
|
||||
feedbackTab.setNodeSupplier(FeedbackPage::new);
|
||||
aboutTab.setNodeSupplier(AboutPage::new);
|
||||
tab = new TabHeader(gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
|
||||
tab = new TabHeader(transitionPane, gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
|
||||
|
||||
tab.select(gameTab);
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null));
|
||||
transitionPane.setContent(gameTab.getNode(), ContainerAnimations.NONE);
|
||||
FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER)
|
||||
.addNavigationDrawerTab(tab, javaManagementTab, i18n("java.management"), SVG.LOCAL_CAFE)
|
||||
.addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER, SVG.STADIA_CONTROLLER_FILL)
|
||||
.addNavigationDrawerTab(tab, javaManagementTab, i18n("java.management"), SVG.LOCAL_CAFE, SVG.LOCAL_CAFE_FILL)
|
||||
.startCategory(i18n("launcher").toUpperCase(Locale.ROOT))
|
||||
.addNavigationDrawerTab(tab, settingsTab, i18n("settings.launcher.general"), SVG.TUNE)
|
||||
.addNavigationDrawerTab(tab, personalizationTab, i18n("settings.launcher.appearance"), SVG.STYLE)
|
||||
.addNavigationDrawerTab(tab, personalizationTab, i18n("settings.launcher.appearance"), SVG.STYLE, SVG.STYLE_FILL)
|
||||
.addNavigationDrawerTab(tab, downloadTab, i18n("download"), SVG.DOWNLOAD)
|
||||
.startCategory(i18n("help").toUpperCase(Locale.ROOT))
|
||||
.addNavigationDrawerTab(tab, helpTab, i18n("help"), SVG.HELP)
|
||||
.addNavigationDrawerTab(tab, feedbackTab, i18n("feedback"), SVG.FEEDBACK)
|
||||
.addNavigationDrawerTab(tab, aboutTab, i18n("about"), SVG.INFO);
|
||||
.addNavigationDrawerTab(tab, helpTab, i18n("help"), SVG.HELP, SVG.HELP_FILL)
|
||||
.addNavigationDrawerTab(tab, feedbackTab, i18n("feedback"), SVG.FEEDBACK, SVG.FEEDBACK_FILL)
|
||||
.addNavigationDrawerTab(tab, aboutTab, i18n("about"), SVG.INFO, SVG.INFO_FILL);
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
|
||||
@@ -94,11 +89,11 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
|
||||
public void showGameSettings(Profile profile) {
|
||||
gameTab.getNode().loadVersion(profile, null);
|
||||
tab.select(gameTab);
|
||||
tab.select(gameTab, false);
|
||||
}
|
||||
|
||||
public void showFeedback() {
|
||||
tab.select(feedbackTab);
|
||||
tab.select(feedbackTab, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,10 +30,12 @@ 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;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -49,6 +51,7 @@ 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.nio.file.Files;
|
||||
@@ -158,6 +161,9 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
String currentId = getSkinnable().getMainPage().getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, it -> getSkinnable().getMainPage().getProfile().setSelectedVersion(it.getId()));
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareVersionPage);
|
||||
}
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
@@ -171,22 +177,51 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
downloadItem.setLeftGraphic(wrap(SVG.DOWNLOAD));
|
||||
downloadItem.setActionButtonVisible(false);
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
downloadItem.setOnAction(e -> {
|
||||
Controllers.getDownloadPage().showGameDownloads();
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
});
|
||||
FXUtils.installFastTooltip(downloadItem, i18n("download.hint"));
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
FXUtils.prepareOnMouseEnter(downloadItem, Controllers::prepareDownloadPage);
|
||||
}
|
||||
|
||||
// fifth item in left sidebar
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS));
|
||||
launcherSettingsItem.setActionButtonVisible(false);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
launcherSettingsItem.setOnAction(e -> {
|
||||
Controllers.getSettingsPage().showGameSettings(Profiles.getSelectedProfile());
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
});
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
FXUtils.prepareOnMouseEnter(launcherSettingsItem, Controllers::prepareSettingsPage);
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -198,7 +233,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);
|
||||
|
||||
@@ -114,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);
|
||||
// ====
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,712 @@
|
||||
/*
|
||||
* 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.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, () -> {
|
||||
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);
|
||||
}, () -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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.SLIDE_UP_FADE_IN);
|
||||
};
|
||||
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.SLIDE_UP_FADE_IN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
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.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
private static final int TERRACOTTA_AGREEMENT_VERSION = 2;
|
||||
|
||||
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(transitionPane, statusPage);
|
||||
tab.select(statusPage);
|
||||
|
||||
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();
|
||||
|
||||
if (globalConfig().getTerracottaAgreementVersion() < TERRACOTTA_AGREEMENT_VERSION) {
|
||||
Controllers.confirmWithCountdown(i18n("terracotta.confirm.desc"), i18n("terracotta.confirm.title"), 5, MessageDialogPane.MessageType.INFO, () -> {
|
||||
globalConfig().setTerracottaAgreementVersion(TERRACOTTA_AGREEMENT_VERSION);
|
||||
}, () -> fireEvent(new PageCloseEvent()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
tab.onPageHidden();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,18 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.game.NativesDirectoryType;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.game.Renderer;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -250,8 +252,8 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
if (versionSetting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
String nativesDirName = "natives-" + Platform.SYSTEM_PLATFORM;
|
||||
if (versionId == null) {
|
||||
return String.format("%s/%s/%s",
|
||||
profile.getRepository().getBaseDirectory().resolve("versions").toAbsolutePath().normalize(),
|
||||
return String.join(FileSystems.getDefault().getSeparator(),
|
||||
FileUtils.getAbsolutePath(profile.getRepository().getBaseDirectory().resolve("versions")),
|
||||
i18n("settings.advanced.natives_directory.default.version_id"),
|
||||
nativesDirName
|
||||
);
|
||||
|
||||
@@ -26,16 +26,21 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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 DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
|
||||
private final Path worldDir;
|
||||
@@ -47,7 +52,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
datapack = new Datapack(worldDir.resolve("datapacks"));
|
||||
datapack.loadFromDir();
|
||||
|
||||
setItems(MappedObservableList.create(datapack.getInfo(), DatapackListPageSkin.DatapackInfoObject::new));
|
||||
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
|
||||
|
||||
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
|
||||
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
|
||||
@@ -55,9 +60,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
|
||||
private void installSingleDatapack(Path datapack) {
|
||||
try {
|
||||
Datapack zip = new Datapack(datapack);
|
||||
zip.loadFromZip();
|
||||
zip.installTo(worldDir);
|
||||
this.datapack.installPack(datapack);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
LOG.warning("Unable to parse datapack file " + datapack, e);
|
||||
}
|
||||
@@ -83,8 +86,9 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
|
||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||
|
||||
if (res != null)
|
||||
if (res != null) {
|
||||
res.forEach(this::installSingleDatapack);
|
||||
}
|
||||
|
||||
datapack.loadFromDir();
|
||||
}
|
||||
@@ -97,7 +101,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
datapack.deletePack(pack);
|
||||
} catch (IOException e) {
|
||||
// Fail to remove mods if the game is running or the datapack is absent.
|
||||
LOG.warning("Failed to delete datapack " + pack);
|
||||
LOG.warning("Failed to delete datapack \"" + pack.getId() + "\"", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -105,12 +109,41 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
void enableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
|
||||
.forEach(info -> info.setActive(true));
|
||||
.forEach(pack -> pack.setActive(true));
|
||||
}
|
||||
|
||||
void disableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
|
||||
.forEach(info -> info.setActive(false));
|
||||
.forEach(pack -> pack.setActive(false));
|
||||
}
|
||||
|
||||
void openDataPackFolder() {
|
||||
FXUtils.openFolder(datapack.getPath());
|
||||
}
|
||||
|
||||
@NotNull Predicate<DatapackListPageSkin.DatapackInfoObject> updateSearchPredicate(String queryString) {
|
||||
if (queryString.isBlank()) {
|
||||
return dataPack -> true;
|
||||
}
|
||||
|
||||
final Predicate<String> stringPredicate;
|
||||
if (queryString.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(StringUtils.substringAfter(queryString, "regex:"));
|
||||
stringPredicate = s -> s != null && pattern.matcher(s).find();
|
||||
} catch (Exception e) {
|
||||
return dataPack -> false;
|
||||
}
|
||||
} else {
|
||||
String lowerCaseFilter = queryString.toLowerCase(Locale.ROOT);
|
||||
stringPredicate = s -> s != null && s.toLowerCase(Locale.ROOT).contains(lowerCaseFilter);
|
||||
}
|
||||
|
||||
return dataPack -> {
|
||||
String id = dataPack.getPackInfo().getId();
|
||||
String description = dataPack.getPackInfo().getDescription().toString();
|
||||
return stringPredicate.test(id) || stringPredicate.test(description);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,33 +17,79 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.mod.Datapack;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
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.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
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.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
private final HBox searchBar;
|
||||
private final HBox normalToolbar;
|
||||
private final HBox selectingToolbar;
|
||||
InvalidationListener updateBarByStateWeakListener;
|
||||
|
||||
private final JFXListView<DatapackInfoObject> listView;
|
||||
private final FilteredList<DatapackInfoObject> filteredList;
|
||||
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
|
||||
private final JFXTextField searchField;
|
||||
|
||||
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
|
||||
final Consumer<Integer> toggleSelect;
|
||||
|
||||
DatapackListPageSkin(DatapackListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@@ -53,22 +99,79 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
JFXListView<DatapackInfoObject> listView = new JFXListView<>();
|
||||
listView = new JFXListView<>();
|
||||
filteredList = new FilteredList<>(skinnable.getItems());
|
||||
|
||||
{
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getChildren().add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh));
|
||||
toolbar.getChildren().add(createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add));
|
||||
toolbar.getChildren().add(createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}));
|
||||
toolbar.getChildren().add(createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())));
|
||||
toolbar.getChildren().add(createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())));
|
||||
root.getContent().add(toolbar);
|
||||
toolbarPane = new TransitionPane();
|
||||
searchBar = new HBox();
|
||||
normalToolbar = new HBox();
|
||||
selectingToolbar = new HBox();
|
||||
|
||||
normalToolbar.getChildren().addAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("datapack.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openDataPackFolder),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
|
||||
);
|
||||
|
||||
selectingToolbar.getChildren().addAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 5));
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> filteredList.setPredicate(skinnable.updateSearchPredicate(searchField.getText())));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
isSearching.set(false);
|
||||
searchField.clear();
|
||||
});
|
||||
FXUtils.onEscPressed(searchField, closeSearchBar::fire);
|
||||
searchBar.getChildren().addAll(searchField, closeSearchBar);
|
||||
|
||||
root.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
if (listView.getSelectionModel().getSelectedItem() != null) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> isSelecting.set(selectedItem != null));
|
||||
root.getContent().add(toolbarPane);
|
||||
|
||||
updateBarByStateWeakListener = FXUtils.observeWeak(() -> {
|
||||
if (isSelecting.get()) {
|
||||
changeToolbar(selectingToolbar);
|
||||
} else if (!isSelecting.get() && !isSearching.get()) {
|
||||
changeToolbar(normalToolbar);
|
||||
} else {
|
||||
changeToolbar(searchBar);
|
||||
}
|
||||
}, isSearching, isSelecting);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -80,26 +183,48 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView, lastCell));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
this.listView.setItems(filteredList);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
toggleSelect = i -> {
|
||||
if (listView.getSelectionModel().isSelected(i)) {
|
||||
listView.getSelectionModel().clearSelection(i);
|
||||
} else {
|
||||
listView.getSelectionModel().select(i);
|
||||
}
|
||||
};
|
||||
|
||||
pane.getChildren().setAll(root);
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
|
||||
private void changeToolbar(HBox newToolbar) {
|
||||
Node oldToolbar = toolbarPane.getCurrentNode();
|
||||
if (newToolbar != oldToolbar) {
|
||||
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
if (newToolbar == searchBar) {
|
||||
// search button click will get focus while searchField request focus, this cause conflict.
|
||||
// Defer focus request to next pulse avoids this conflict.
|
||||
Platform.runLater(searchField::requestFocus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
|
||||
private final BooleanProperty active;
|
||||
private final BooleanProperty activeProperty;
|
||||
private final Datapack.Pack packInfo;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
|
||||
DatapackInfoObject(Datapack.Pack packInfo) {
|
||||
this.packInfo = packInfo;
|
||||
this.active = packInfo.activeProperty();
|
||||
this.activeProperty = packInfo.activeProperty();
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
@@ -107,16 +232,75 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
|
||||
return packInfo.getDescription().toString();
|
||||
}
|
||||
|
||||
Datapack.Pack getPackInfo() {
|
||||
return packInfo;
|
||||
}
|
||||
|
||||
Image loadIcon() {
|
||||
Image image = null;
|
||||
Path imagePath;
|
||||
if (this.getPackInfo().isDirectory()) {
|
||||
imagePath = getPackInfo().getPath().resolve("pack.png");
|
||||
try {
|
||||
image = FXUtils.loadImage(imagePath, 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e);
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
} else {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(getPackInfo().getPath())) {
|
||||
imagePath = fs.getPath("/pack.png");
|
||||
if (Files.exists(imagePath)) {
|
||||
image = FXUtils.loadImage(imagePath, 64, 64, true, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e);
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
}
|
||||
|
||||
if (image != null && !image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
} else {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
}
|
||||
|
||||
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> 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(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
imageView.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
|
||||
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
|
||||
final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
ImageView imageView = new ImageView();
|
||||
final TwoLineListItem content = new TwoLineListItem();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
@@ -130,9 +314,16 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
imageView.setFitWidth(32);
|
||||
imageView.setFitHeight(32);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
|
||||
|
||||
StackPane.setMargin(container, new Insets(8));
|
||||
container.getChildren().setAll(checkBox, content);
|
||||
container.getChildren().setAll(checkBox, imageView, content);
|
||||
getContainer().getChildren().setAll(container);
|
||||
|
||||
getContainer().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseEvent -> handleSelect(this, mouseEvent));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,7 +334,41 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.activeProperty);
|
||||
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
|
||||
}
|
||||
}
|
||||
|
||||
public void handleSelect(DatapackInfoListCell cell, MouseEvent mouseEvent) {
|
||||
if (cell.isEmpty()) {
|
||||
mouseEvent.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouseEvent.isShiftDown()) {
|
||||
int currentIndex = cell.getIndex();
|
||||
if (lastShiftClickIndex.get() == -1) {
|
||||
lastShiftClickIndex.set(currentIndex);
|
||||
toggleSelect.accept(cell.getIndex());
|
||||
} else if (listView.getItems().size() >= lastShiftClickIndex.get() && !(lastShiftClickIndex.get() < -1)) {
|
||||
if (cell.isSelected()) {
|
||||
IntStream.rangeClosed(
|
||||
Math.min(lastShiftClickIndex.get(), currentIndex),
|
||||
Math.max(lastShiftClickIndex.get(), currentIndex))
|
||||
.forEach(listView.getSelectionModel()::clearSelection);
|
||||
} else {
|
||||
listView.getSelectionModel().selectRange(lastShiftClickIndex.get(), currentIndex);
|
||||
listView.getSelectionModel().select(currentIndex);
|
||||
}
|
||||
lastShiftClickIndex.set(-1);
|
||||
} else {
|
||||
lastShiftClickIndex.set(currentIndex);
|
||||
listView.getSelectionModel().select(currentIndex);
|
||||
}
|
||||
} else {
|
||||
toggleSelect.accept(cell.getIndex());
|
||||
}
|
||||
cell.requestFocus();
|
||||
mouseEvent.consume();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -219,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));
|
||||
|
||||
@@ -341,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);
|
||||
@@ -358,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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -42,7 +43,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class InstallerListPage extends ListPageBase<InstallerItem> implements VersionPage.VersionLoadable {
|
||||
public class InstallerListPage extends ListPageBase<InstallerItem> implements VersionPage.VersionLoadable, TransitionPane.Cacheable {
|
||||
private Profile profile;
|
||||
private String versionId;
|
||||
private Version version;
|
||||
|
||||
@@ -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;
|
||||
@@ -34,6 +34,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
@@ -44,19 +45,22 @@ 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 {
|
||||
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware, TransitionPane.Cacheable {
|
||||
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 -> {
|
||||
@@ -83,34 +87,82 @@ 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() {
|
||||
@@ -148,7 +200,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 +212,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 +227,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void openModFolder() {
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(versionId).resolve("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());
|
||||
}
|
||||
@@ -193,13 +245,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
} else if (result.isEmpty()) {
|
||||
Controllers.dialog(i18n("mods.check_updates.empty"));
|
||||
} else {
|
||||
Controllers.navigate(new ModUpdatesPage(modManager, result));
|
||||
Controllers.navigateForward(new ModUpdatesPage(modManager, result));
|
||||
}
|
||||
})
|
||||
.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 +262,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 +291,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,18 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
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 +37,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 +47,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 +56,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 +94,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 +108,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();
|
||||
|
||||
@@ -142,7 +151,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
toolbarNormal.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("folder.mod"), SVG.FOLDER_OPEN, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("mods.check_updates"), SVG.UPDATE, skinnable::checkUpdates),
|
||||
createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
|
||||
@@ -233,7 +242,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
if (newToolbar != oldToolbar) {
|
||||
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
if (newToolbar == searchBar) {
|
||||
searchField.requestFocus();
|
||||
Platform.runLater(searchField::requestFocus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,139 +281,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 +504,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 +527,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 +542,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 +561,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 +585,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 +601,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 +609,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];
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -60,7 +61,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements VersionPage.VersionLoadable {
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements VersionPage.VersionLoadable, TransitionPane.Cacheable {
|
||||
|
||||
private static String translateAuthorName(String author) {
|
||||
if (I18n.isUseChinese() && "hsds".equals(author)) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -38,7 +37,6 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
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.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
@@ -73,16 +71,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
|
||||
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));
|
||||
|
||||
tab = new TabHeader(versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab);
|
||||
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab);
|
||||
tab.select(versionSettingsTab);
|
||||
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
|
||||
tab.select(versionSettingsTab);
|
||||
transitionPane.setContent(versionSettingsTab.getNode(), ContainerAnimations.NONE);
|
||||
FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
|
||||
}
|
||||
|
||||
@@ -112,6 +105,10 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
};
|
||||
}
|
||||
|
||||
public void showInstanceSettings() {
|
||||
tab.select(versionSettingsTab, false);
|
||||
}
|
||||
|
||||
public void setVersion(String version, Profile profile) {
|
||||
this.version.set(new Profile.ProfileVersion(profile, version));
|
||||
}
|
||||
@@ -238,56 +235,12 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
super(control);
|
||||
|
||||
{
|
||||
BorderPane left = new BorderPane();
|
||||
FXUtils.setLimitWidth(left, 200);
|
||||
setLeft(left);
|
||||
|
||||
AdvancedListItem versionSettingsItem = new AdvancedListItem();
|
||||
versionSettingsItem.getStyleClass().add("navigation-drawer-item");
|
||||
versionSettingsItem.setTitle(i18n("settings.game"));
|
||||
versionSettingsItem.setLeftGraphic(wrap(SVG.SETTINGS));
|
||||
versionSettingsItem.setActionButtonVisible(false);
|
||||
versionSettingsItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.versionSettingsTab));
|
||||
versionSettingsItem.setOnAction(e -> control.tab.select(control.versionSettingsTab));
|
||||
|
||||
AdvancedListItem installerListItem = new AdvancedListItem();
|
||||
installerListItem.getStyleClass().add("navigation-drawer-item");
|
||||
installerListItem.setTitle(i18n("settings.tabs.installers"));
|
||||
installerListItem.setLeftGraphic(wrap(SVG.DEPLOYED_CODE));
|
||||
installerListItem.setActionButtonVisible(false);
|
||||
installerListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.installerListTab));
|
||||
installerListItem.setOnAction(e -> control.tab.select(control.installerListTab));
|
||||
|
||||
AdvancedListItem modListItem = new AdvancedListItem();
|
||||
modListItem.getStyleClass().add("navigation-drawer-item");
|
||||
modListItem.setTitle(i18n("mods.manage"));
|
||||
modListItem.setLeftGraphic(wrap(SVG.EXTENSION));
|
||||
modListItem.setActionButtonVisible(false);
|
||||
modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab));
|
||||
modListItem.setOnAction(e -> control.tab.select(control.modListTab));
|
||||
|
||||
AdvancedListItem worldListItem = new AdvancedListItem();
|
||||
worldListItem.getStyleClass().add("navigation-drawer-item");
|
||||
worldListItem.setTitle(i18n("world.manage"));
|
||||
worldListItem.setLeftGraphic(wrap(SVG.PUBLIC));
|
||||
worldListItem.setActionButtonVisible(false);
|
||||
worldListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.worldListTab));
|
||||
worldListItem.setOnAction(e -> control.tab.select(control.worldListTab));
|
||||
|
||||
AdvancedListItem schematicsListItem = new AdvancedListItem();
|
||||
schematicsListItem.getStyleClass().add("navigation-drawer-item");
|
||||
schematicsListItem.setTitle(i18n("schematics.manage"));
|
||||
schematicsListItem.setLeftGraphic(wrap(SVG.SCHEMA));
|
||||
schematicsListItem.setActionButtonVisible(false);
|
||||
schematicsListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.schematicsTab));
|
||||
schematicsListItem.setOnAction(e -> control.tab.select(control.schematicsTab));
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.add(versionSettingsItem)
|
||||
.add(installerListItem)
|
||||
.add(modListItem)
|
||||
.add(worldListItem)
|
||||
.add(schematicsListItem);
|
||||
.addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL)
|
||||
.addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC)
|
||||
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL);
|
||||
VBox.setVgrow(sideBar, Priority.ALWAYS);
|
||||
|
||||
PopupMenu browseList = new PopupMenu();
|
||||
|
||||
@@ -461,7 +461,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
if (advancedVersionSettingPage == null)
|
||||
advancedVersionSettingPage = new AdvancedVersionSettingPage(profile, versionId, lastVersionSetting);
|
||||
|
||||
Controllers.navigate(advancedVersionSettingPage);
|
||||
Controllers.navigateForward(advancedVersionSettingPage);
|
||||
}
|
||||
});
|
||||
showAdvancedSettingPane.setRight(button);
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
@@ -112,14 +113,21 @@ public final class Versions {
|
||||
|
||||
JFXButton deleteButton = new JFXButton(i18n("button.delete"));
|
||||
deleteButton.getStyleClass().add("dialog-error");
|
||||
deleteButton.setOnAction(e -> profile.getRepository().removeVersionFromDisk(version));
|
||||
deleteButton.setOnAction(e -> {
|
||||
Task.supplyAsync(Schedulers.io(), () -> profile.getRepository().removeVersionFromDisk(version))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null || !Boolean.TRUE.equals(result)) {
|
||||
Controllers.dialog(i18n("version.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
|
||||
Controllers.confirmAction(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, deleteButton);
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
|
||||
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> {
|
||||
if (!FileUtils.isNameValid(newName)) {
|
||||
if (!HMCLGameRepository.isValidVersionId(newName)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
}
|
||||
@@ -150,6 +158,10 @@ public final class Versions {
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> {
|
||||
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
|
||||
if (!HMCLGameRepository.isValidVersionId(newVersionName)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
}
|
||||
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
@@ -157,13 +169,15 @@ public final class Versions {
|
||||
resolve.run();
|
||||
} else {
|
||||
reject.accept(StringUtils.getStackTrace(exception));
|
||||
profile.getRepository().removeVersionFromDisk(newVersionName);
|
||||
if (!profile.getRepository().versionIdConflicts(newVersionName)) {
|
||||
profile.getRepository().removeVersionFromDisk(newVersionName);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm")))
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version,
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().hasVersion(newVersionName))))
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName))))
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
@@ -222,9 +236,14 @@ public final class Versions {
|
||||
|
||||
private static boolean checkVersionForLaunching(Profile profile, String id) {
|
||||
if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) {
|
||||
Controllers.dialog(i18n("version.empty.launch"), i18n("launch.failed"), MessageDialogPane.MessageType.ERROR, () -> {
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
});
|
||||
JFXButton gotoDownload = new JFXButton(i18n("version.empty.launch.goto_download"));
|
||||
gotoDownload.getStyleClass().add("dialog-accept");
|
||||
gotoDownload.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
|
||||
Controllers.confirmAction(i18n("version.empty.launch"), i18n("launch.failed"),
|
||||
MessageDialogPane.MessageType.ERROR,
|
||||
gotoDownload,
|
||||
null);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@@ -268,6 +287,7 @@ public final class Versions {
|
||||
|
||||
public static void modifyGameSettings(Profile profile, String version) {
|
||||
Controllers.getVersionPage().setVersion(version, profile);
|
||||
Controllers.getVersionPage().showInstanceSettings();
|
||||
// VersionPage.loadVersion will be invoked after navigation
|
||||
Controllers.navigate(Controllers.getVersionPage());
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.*;
|
||||
@@ -246,7 +247,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
item.setSubtitle(formatDateTime(skinnable.getBackupTime()) + (skinnable.count == 0 ? "" : " (" + skinnable.count + ")"));
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
item.addTag(world.getGameVersion());
|
||||
item.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.scene.control.Skin;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -41,7 +42,8 @@ public class WorldExportPage extends WizardSinglePage {
|
||||
this.world = world;
|
||||
|
||||
path.set(export.toString());
|
||||
gameVersion.set(world.getGameVersion());
|
||||
if (world.getGameVersion() != null)
|
||||
gameVersion.set(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
worldName.set(world.getWorldName());
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,8 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
Label gameVersionLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(gameVersionLabel);
|
||||
gameVersionLabel.setText(world.getGameVersion());
|
||||
if (world.getGameVersion() != null)
|
||||
gameVersionLabel.setText(world.getGameVersion().toNormalizedString());
|
||||
BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT);
|
||||
gameVersionPane.setRight(gameVersionLabel);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -73,10 +73,10 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
item.setMouseTransparent(true);
|
||||
if (world.getWorldName() != null)
|
||||
item.setTitle(parseColorEscapes(world.getWorldName()));
|
||||
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
|
||||
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
item.addTag(world.getGameVersion());
|
||||
item.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||
if (world.isLocked())
|
||||
item.addTag(i18n("world.locked"));
|
||||
}
|
||||
@@ -127,7 +127,7 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||
);
|
||||
|
||||
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
|
||||
() -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
@@ -41,7 +43,7 @@ import java.util.stream.Stream;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class WorldListPage extends ListPageBase<WorldListItem> implements VersionPage.VersionLoadable {
|
||||
public final class WorldListPage extends ListPageBase<WorldListItem> implements VersionPage.VersionLoadable, TransitionPane.Cacheable {
|
||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||
|
||||
private Path savesDir;
|
||||
@@ -49,7 +51,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
private List<World> worlds;
|
||||
private Profile profile;
|
||||
private String id;
|
||||
private String gameVersion;
|
||||
private GameVersionNumber gameVersion;
|
||||
|
||||
public WorldListPage() {
|
||||
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
|
||||
@@ -87,7 +89,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
return;
|
||||
|
||||
setLoading(true);
|
||||
Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).orElse(null))
|
||||
Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null))
|
||||
.thenApplyAsync(unused -> {
|
||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
||||
return stream.parallel().collect(Collectors.toList());
|
||||
|
||||
@@ -28,13 +28,11 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
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.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
@@ -65,17 +63,14 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
this.world = world;
|
||||
this.backupsDir = backupsDir;
|
||||
|
||||
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
|
||||
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", world.getWorldName())));
|
||||
this.header = new TabHeader(worldInfoTab, worldBackupsTab);
|
||||
|
||||
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
|
||||
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
|
||||
header.select(worldInfoTab);
|
||||
transitionPane.setContent(worldInfoTab.getNode(), ContainerAnimations.NONE);
|
||||
FXUtils.onChange(header.getSelectionModel().selectedItemProperty(), newValue ->
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE));
|
||||
|
||||
setCenter(transitionPane);
|
||||
|
||||
BorderPane left = new BorderPane();
|
||||
@@ -84,13 +79,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
setLeft(left);
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO)
|
||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE);
|
||||
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||
|
||||
if (world.getGameVersion() != null && // old game will not write game version to level.dat
|
||||
GameVersionNumber.asGameVersion(world.getGameVersion()).isAtLeast("1.13", "17w43a")) {
|
||||
world.getGameVersion().isAtLeast("1.13", "17w43a")) {
|
||||
header.getTabs().add(datapackTab);
|
||||
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION);
|
||||
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||
}
|
||||
|
||||
left.setTop(sideBar);
|
||||
@@ -106,7 +101,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||
);
|
||||
|
||||
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
|
||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||
popupMenu.getContent().add(
|
||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||
}
|
||||
|
||||
@@ -40,11 +40,9 @@ public interface Navigation {
|
||||
|
||||
enum NavigationDirection {
|
||||
START(ContainerAnimations.NONE),
|
||||
PREVIOUS(ContainerAnimations.SWIPE_RIGHT),
|
||||
NEXT(ContainerAnimations.SWIPE_LEFT),
|
||||
FINISH(ContainerAnimations.SWIPE_LEFT),
|
||||
IN(ContainerAnimations.ZOOM_IN),
|
||||
OUT(ContainerAnimations.ZOOM_OUT);
|
||||
PREVIOUS(ContainerAnimations.BACKWARD),
|
||||
NEXT(ContainerAnimations.FORWARD),
|
||||
FINISH(ContainerAnimations.FORWARD);
|
||||
|
||||
private final ContainerAnimations animation;
|
||||
|
||||
|
||||
@@ -83,6 +83,10 @@ public class WizardController implements Navigation {
|
||||
}
|
||||
|
||||
public void onNext(Node page) {
|
||||
onNext(page, NavigationDirection.NEXT);
|
||||
}
|
||||
|
||||
public void onNext(Node page, NavigationDirection direction) {
|
||||
pages.push(page);
|
||||
|
||||
if (stopped) { // navigatingTo may stop this wizard.
|
||||
@@ -93,11 +97,15 @@ public class WizardController implements Navigation {
|
||||
((WizardPage) page).onNavigate(settings);
|
||||
|
||||
LOG.info("Navigating to " + page + ", pages: " + pages);
|
||||
displayer.navigateTo(page, NavigationDirection.NEXT);
|
||||
displayer.navigateTo(page, direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrev(boolean cleanUp) {
|
||||
onPrev(cleanUp, NavigationDirection.PREVIOUS);
|
||||
}
|
||||
|
||||
public void onPrev(boolean cleanUp, NavigationDirection direction) {
|
||||
if (!canPrev()) {
|
||||
if (provider.cancelIfCannotGoBack()) {
|
||||
onCancel();
|
||||
@@ -116,7 +124,7 @@ public class WizardController implements Navigation {
|
||||
((WizardPage) prevPage).onNavigate(settings);
|
||||
|
||||
LOG.info("Navigating to " + prevPage + ", pages: " + pages);
|
||||
displayer.navigateTo(prevPage, NavigationDirection.PREVIOUS);
|
||||
displayer.navigateTo(prevPage, direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.util.Optional;
|
||||
|
||||
public final class RemoteVersion {
|
||||
|
||||
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
|
||||
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
|
||||
try {
|
||||
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(url), JsonObject.class);
|
||||
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
|
||||
@@ -37,7 +37,7 @@ public final class RemoteVersion {
|
||||
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
if (jarUrl != null && jarHash != null) {
|
||||
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
|
||||
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), preview, force);
|
||||
} else {
|
||||
throw new IOException("No download url is available");
|
||||
}
|
||||
@@ -51,14 +51,16 @@ public final class RemoteVersion {
|
||||
private final String url;
|
||||
private final Type type;
|
||||
private final IntegrityCheck integrityCheck;
|
||||
private final boolean preview;
|
||||
private final boolean force;
|
||||
|
||||
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean force) {
|
||||
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
|
||||
this.channel = channel;
|
||||
this.version = version;
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.integrityCheck = integrityCheck;
|
||||
this.preview = preview;
|
||||
this.force = force;
|
||||
}
|
||||
|
||||
@@ -82,6 +84,10 @@ public final class RemoteVersion {
|
||||
return integrityCheck;
|
||||
}
|
||||
|
||||
public boolean isPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public boolean isForce() {
|
||||
return force;
|
||||
}
|
||||
|
||||
@@ -27,14 +27,15 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class UpdateChecker {
|
||||
private UpdateChecker() {}
|
||||
private UpdateChecker() {
|
||||
}
|
||||
|
||||
private static final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
|
||||
private static final BooleanBinding outdated = Bindings.createBooleanBinding(
|
||||
@@ -55,7 +56,7 @@ public final class UpdateChecker {
|
||||
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
|
||||
|
||||
public static void init() {
|
||||
requestCheckUpdate(UpdateChannel.getChannel());
|
||||
requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate());
|
||||
}
|
||||
|
||||
public static RemoteVersion getLatestVersion() {
|
||||
@@ -82,16 +83,17 @@ public final class UpdateChecker {
|
||||
return checkingUpdate.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
|
||||
private static RemoteVersion checkUpdate(UpdateChannel channel, boolean preview) throws IOException {
|
||||
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Self verification failed");
|
||||
}
|
||||
|
||||
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, mapOf(
|
||||
pair("version", Metadata.VERSION),
|
||||
pair("channel", channel.channelName)));
|
||||
var query = new LinkedHashMap<String, String>();
|
||||
query.put("version", Metadata.VERSION);
|
||||
query.put("channel", preview ? channel.channelName + "-preview" : channel.channelName);
|
||||
|
||||
return RemoteVersion.fetch(channel, url);
|
||||
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, query);
|
||||
return RemoteVersion.fetch(channel, preview, url);
|
||||
}
|
||||
|
||||
private static boolean isDevelopmentVersion(String version) {
|
||||
@@ -99,7 +101,7 @@ public final class UpdateChecker {
|
||||
version.contains("SNAPSHOT"); // eg. 3.5.SNAPSHOT
|
||||
}
|
||||
|
||||
public static void requestCheckUpdate(UpdateChannel channel) {
|
||||
public static void requestCheckUpdate(UpdateChannel channel, boolean preview) {
|
||||
Platform.runLater(() -> {
|
||||
if (isCheckingUpdate())
|
||||
return;
|
||||
@@ -108,9 +110,9 @@ public final class UpdateChecker {
|
||||
thread(() -> {
|
||||
RemoteVersion result = null;
|
||||
try {
|
||||
result = checkUpdate(channel);
|
||||
LOG.info("Latest version (" + channel + ") is " + result);
|
||||
} catch (IOException e) {
|
||||
result = checkUpdate(channel, preview);
|
||||
LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to check for update", e);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ public final class ChunkBaseApp {
|
||||
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
|
||||
|
||||
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
||||
"1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3",
|
||||
"1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12",
|
||||
"1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
|
||||
"1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13",
|
||||
"1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
|
||||
};
|
||||
|
||||
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
|
||||
@@ -49,7 +49,7 @@ public final class ChunkBaseApp {
|
||||
|
||||
public static boolean isSupported(@NotNull World world) {
|
||||
return world.getSeed() != null && world.getGameVersion() != null &&
|
||||
GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo(MIN_GAME_VERSION) >= 0;
|
||||
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
|
||||
}
|
||||
|
||||
public static ChunkBaseApp newBuilder(String app, long seed) {
|
||||
@@ -60,7 +60,7 @@ public final class ChunkBaseApp {
|
||||
assert isSupported(world);
|
||||
|
||||
newBuilder("seed-map", Objects.requireNonNull(world.getSeed()))
|
||||
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS)
|
||||
.addPlatform(world.getGameVersion(), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS)
|
||||
.open();
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public final class ChunkBaseApp {
|
||||
assert isSupported(world);
|
||||
|
||||
newBuilder("stronghold-finder", Objects.requireNonNull(world.getSeed()))
|
||||
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS)
|
||||
.addPlatform(world.getGameVersion(), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS)
|
||||
.open();
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public final class ChunkBaseApp {
|
||||
assert isSupported(world);
|
||||
|
||||
newBuilder("nether-fortress-finder", Objects.requireNonNull(world.getSeed()))
|
||||
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, NETHER_FORTRESS_GAME_VERSIONS)
|
||||
.addPlatform(world.getGameVersion(), false, NETHER_FORTRESS_GAME_VERSIONS)
|
||||
.open();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public final class ChunkBaseApp {
|
||||
assert isSupported(world);
|
||||
|
||||
newBuilder("endcity-finder", Objects.requireNonNull(world.getSeed()))
|
||||
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, END_CITY_GAME_VERSIONS)
|
||||
.addPlatform(world.getGameVersion(), false, END_CITY_GAME_VERSIONS)
|
||||
.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NativePatcher {
|
||||
private NativePatcher() {
|
||||
}
|
||||
|
||||
private static final Library NONEXISTENT_LIBRARY = new Library(null);
|
||||
|
||||
@@ -196,4 +194,85 @@ public final class NativePatcher {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static SupportStatus checkSupportedStatus(GameVersionNumber gameVersion, Platform platform,
|
||||
OSVersion systemVersion) {
|
||||
if (platform.equals(Platform.WINDOWS_X86_64)) {
|
||||
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_10) && gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
}
|
||||
|
||||
if (platform.equals(Platform.MACOS_X86_64) || platform.equals(Platform.LINUX_X86_64))
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
|
||||
if (platform.equals(Platform.WINDOWS_X86) || platform.equals(Platform.LINUX_X86)) {
|
||||
if (gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
else
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
}
|
||||
|
||||
if (platform.equals(Platform.WINDOWS_ARM64) || platform.equals(Platform.MACOS_ARM64)) {
|
||||
if (gameVersion.compareTo("1.19") >= 0)
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
|
||||
String minVersion = platform.getOperatingSystem() == OperatingSystem.WINDOWS
|
||||
? "1.8"
|
||||
: "1.6";
|
||||
|
||||
return gameVersion.compareTo(minVersion) >= 0
|
||||
? SupportStatus.LAUNCHER_SUPPORTED
|
||||
: SupportStatus.TRANSLATION_SUPPORTED;
|
||||
}
|
||||
|
||||
String minVersion = null;
|
||||
String maxVersion = null;
|
||||
|
||||
if (platform.equals(Platform.FREEBSD_X86_64)) {
|
||||
minVersion = "1.13";
|
||||
} else if (platform.equals(Platform.LINUX_ARM64)) {
|
||||
minVersion = "1.6";
|
||||
} else if (platform.equals(Platform.LINUX_RISCV64)) {
|
||||
minVersion = "1.8";
|
||||
maxVersion = "1.21.5";
|
||||
} else if (platform.equals(Platform.LINUX_LOONGARCH64)) {
|
||||
minVersion = "1.6";
|
||||
} else if (platform.equals(Platform.LINUX_LOONGARCH64_OW)) {
|
||||
minVersion = "1.6";
|
||||
maxVersion = "1.20.1";
|
||||
} else if (platform.equals(Platform.LINUX_MIPS64EL) || platform.equals(Platform.LINUX_ARM32)) {
|
||||
minVersion = "1.8";
|
||||
maxVersion = "1.20.1";
|
||||
}
|
||||
|
||||
if (minVersion != null) {
|
||||
if (gameVersion.compareTo(minVersion) >= 0) {
|
||||
if (maxVersion != null && gameVersion.compareTo(maxVersion) > 0)
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
|
||||
String[] defaultGameVersions = GameVersionNumber.getDefaultGameVersions();
|
||||
if (defaultGameVersions.length > 0 && gameVersion.compareTo(defaultGameVersions[0]) > 0) {
|
||||
return SupportStatus.UNTESTED;
|
||||
}
|
||||
return SupportStatus.LAUNCHER_SUPPORTED;
|
||||
} else {
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
return SupportStatus.UNTESTED;
|
||||
}
|
||||
|
||||
public enum SupportStatus {
|
||||
OFFICIAL_SUPPORTED,
|
||||
LAUNCHER_SUPPORTED,
|
||||
TRANSLATION_SUPPORTED,
|
||||
UNTESTED,
|
||||
UNSUPPORTED,
|
||||
}
|
||||
|
||||
private NativePatcher() {
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import javax.swing.*;
|
||||
@@ -62,7 +63,6 @@ import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
@@ -101,14 +101,14 @@ public final class SelfDependencyPatcher {
|
||||
|
||||
private static final class DependencyDescriptor {
|
||||
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
|
||||
private static final Path DEPENDENCIES_DIR_PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(Platform.getPlatform().toString()).resolve("openjfx");
|
||||
private static final Path DEPENDENCIES_DIR_PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(Platform.CURRENT_PLATFORM.toString()).resolve("openjfx");
|
||||
|
||||
static List<DependencyDescriptor> readDependencies() {
|
||||
//noinspection ConstantConditions
|
||||
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
|
||||
Map<String, Map<String, List<DependencyDescriptor>>> allDependencies =
|
||||
JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, mapTypeOf(String.class, listTypeOf(DependencyDescriptor.class))));
|
||||
Map<String, List<DependencyDescriptor>> platform = allDependencies.get(Platform.getPlatform().toString());
|
||||
Map<String, List<DependencyDescriptor>> platform = allDependencies.get(Platform.CURRENT_PLATFORM.toString());
|
||||
if (platform == null)
|
||||
return null;
|
||||
|
||||
@@ -253,14 +253,7 @@ public final class SelfDependencyPatcher {
|
||||
.map(DependencyDescriptor::localPath)
|
||||
.toArray(Path[]::new);
|
||||
|
||||
String addOpens = null;
|
||||
try (InputStream input = SelfDependencyPatcher.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
|
||||
if (input != null)
|
||||
addOpens = new Manifest(input).getMainAttributes().getValue("Add-Opens");
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read MANIFEST.MF file", e);
|
||||
}
|
||||
|
||||
String addOpens = JarUtils.getAttribute("hmcl.add-opens", null);
|
||||
JavaFXPatcher.patch(modules, jars, addOpens != null ? addOpens.split(" ") : new String[0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -43,6 +44,10 @@ public final class I18n {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public static boolean isUpsideDown() {
|
||||
return LocaleUtils.getScript(locale.getLocale()).equals("Qabs");
|
||||
}
|
||||
|
||||
public static boolean isUseChinese() {
|
||||
return LocaleUtils.isChinese(locale.getLocale());
|
||||
}
|
||||
@@ -51,6 +56,10 @@ public final class I18n {
|
||||
return locale.getResourceBundle();
|
||||
}
|
||||
|
||||
public static Translator getTranslator() {
|
||||
return locale.getTranslator();
|
||||
}
|
||||
|
||||
public static String i18n(String key, Object... formatArgs) {
|
||||
return locale.i18n(key, formatArgs);
|
||||
}
|
||||
@@ -60,22 +69,19 @@ public final class I18n {
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
return locale.formatDateTime(time);
|
||||
return getTranslator().formatDateTime(time);
|
||||
}
|
||||
|
||||
public static String getDisplaySelfVersion(RemoteVersion version) {
|
||||
if (locale.getLocale().getLanguage().equals("lzh")) {
|
||||
if (version instanceof GameRemoteVersion)
|
||||
return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion()));
|
||||
else
|
||||
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
|
||||
}
|
||||
public static String formatSpeed(long bytes) {
|
||||
return getTranslator().formatSpeed(bytes);
|
||||
}
|
||||
|
||||
if (LocaleUtils.isEnglish(locale.getLocale()) && "Qabs".equals(LocaleUtils.getScript(locale.getLocale()))) {
|
||||
return UpsideDownUtils.translate(version.getSelfVersion());
|
||||
}
|
||||
public static String getDisplayVersion(RemoteVersion version) {
|
||||
return getTranslator().getDisplayVersion(version);
|
||||
}
|
||||
|
||||
return version.getSelfVersion();
|
||||
public static String getDisplayVersion(GameVersionNumber version) {
|
||||
return getTranslator().getDisplayVersion(version);
|
||||
}
|
||||
|
||||
/// Find the builtin localized resource with given name and suffix.
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator_lzh;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -28,7 +30,7 @@ public final class MinecraftWiki {
|
||||
private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("^[0-9]{2}w[0-9]{2}.+$");
|
||||
|
||||
public static String getWikiLink(SupportedLocale locale, GameRemoteVersion version) {
|
||||
String wikiVersion = version.getSelfVersion();
|
||||
String wikiVersion = StringUtils.removeSuffix(version.getSelfVersion(), "_unobfuscated");
|
||||
var gameVersion = GameVersionNumber.asGameVersion(wikiVersion);
|
||||
|
||||
if (locale.getLocale().getLanguage().equals("lzh")) {
|
||||
@@ -36,9 +38,9 @@ public final class MinecraftWiki {
|
||||
if (wikiVersion.startsWith("2.0"))
|
||||
translatedVersion = "二點〇";
|
||||
else if (wikiVersion.startsWith("1.0.0-rc2"))
|
||||
translatedVersion = WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2"));
|
||||
translatedVersion = Translator_lzh.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2"));
|
||||
else
|
||||
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
|
||||
translatedVersion = Translator_lzh.translateGameVersion(gameVersion);
|
||||
|
||||
if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) {
|
||||
return getWikiLink(SupportedLocale.getLocale(LocaleUtils.LOCALE_ZH_HANT), version);
|
||||
@@ -70,7 +72,7 @@ public final class MinecraftWiki {
|
||||
else if (wikiVersion.startsWith("1.0.0-rc2"))
|
||||
wikiVersion = "1.0.0-rc2";
|
||||
}
|
||||
} else if (gameVersion instanceof GameVersionNumber.Snapshot) {
|
||||
} else if (gameVersion instanceof GameVersionNumber.LegacySnapshot) {
|
||||
return locale.i18n("wiki.version.game.snapshot", wikiVersion) + variantSuffix;
|
||||
} else {
|
||||
if (wikiVersion.length() >= 6 && wikiVersion.charAt(2) == 'w') {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
* 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.util.i18n;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
@@ -24,12 +23,13 @@ import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
@@ -72,10 +72,12 @@ public final class SupportedLocale {
|
||||
private final boolean isDefault;
|
||||
private final String name;
|
||||
private final Locale locale;
|
||||
private final TextDirection textDirection;
|
||||
|
||||
private ResourceBundle resourceBundle;
|
||||
private ResourceBundle localeNamesBundle;
|
||||
private DateTimeFormatter dateTimeFormatter;
|
||||
private List<Locale> candidateLocales;
|
||||
private Translator translator;
|
||||
|
||||
SupportedLocale() {
|
||||
this.isDefault = true;
|
||||
@@ -85,12 +87,14 @@ public final class SupportedLocale {
|
||||
this.locale = StringUtils.isBlank(language)
|
||||
? LocaleUtils.SYSTEM_DEFAULT
|
||||
: Locale.forLanguageTag(language);
|
||||
this.textDirection = LocaleUtils.getTextDirection(locale);
|
||||
}
|
||||
|
||||
SupportedLocale(Locale locale) {
|
||||
this.isDefault = false;
|
||||
this.name = locale.toLanguageTag();
|
||||
this.locale = locale;
|
||||
this.textDirection = LocaleUtils.getTextDirection(locale);
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
@@ -105,6 +109,10 @@ public final class SupportedLocale {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public TextDirection getTextDirection() {
|
||||
return textDirection;
|
||||
}
|
||||
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (isDefault()) {
|
||||
try {
|
||||
@@ -200,23 +208,6 @@ public final class SupportedLocale {
|
||||
}
|
||||
}
|
||||
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
DateTimeFormatter formatter = dateTimeFormatter;
|
||||
if (formatter == null) {
|
||||
if (LocaleUtils.isEnglish(locale) && "Qabs".equals(locale.getScript())) {
|
||||
return UpsideDownUtils.formatDateTime(time);
|
||||
}
|
||||
|
||||
if (locale.getLanguage().equals("lzh")) {
|
||||
return WenyanUtils.formatDateTime(time);
|
||||
}
|
||||
|
||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
|
||||
.withZone(ZoneId.systemDefault());
|
||||
}
|
||||
return formatter.format(time);
|
||||
}
|
||||
|
||||
public String getFcMatchPattern() {
|
||||
String language = locale.getLanguage();
|
||||
String region = locale.getCountry();
|
||||
@@ -252,6 +243,31 @@ public final class SupportedLocale {
|
||||
return region.isEmpty() ? language : language + "-" + region;
|
||||
}
|
||||
|
||||
public Translator getTranslator() {
|
||||
Translator translator = this.translator;
|
||||
if (translator != null)
|
||||
return translator;
|
||||
|
||||
List<Locale> candidateLocales = getCandidateLocales();
|
||||
|
||||
for (Locale candidateLocale : candidateLocales) {
|
||||
String className = DefaultResourceBundleControl.INSTANCE.toBundleName(Translator.class.getSimpleName(), candidateLocale);
|
||||
if (Translator.class.getResource(className + ".class") != null) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(Translator.class.getPackageName() + "." + className);
|
||||
|
||||
MethodHandle constructor = MethodHandles.publicLookup()
|
||||
.findConstructor(clazz, MethodType.methodType(void.class, SupportedLocale.class));
|
||||
|
||||
return this.translator = (Translator) constructor.invoke(this);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to create instance for " + className, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.translator = new Translator(this);
|
||||
}
|
||||
|
||||
public boolean isSameLanguage(SupportedLocale other) {
|
||||
return LocaleUtils.getRootLanguage(this.getLocale())
|
||||
.equals(LocaleUtils.getRootLanguage(other.getLocale()));
|
||||
|
||||
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user