Compare commits
278
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9706a799be | ||
|
|
ec37b32eaa | ||
|
|
f893eeea1f | ||
|
|
8d559dba02 | ||
|
|
d167c712a6 | ||
|
|
8b224cb240 | ||
|
|
651aedaa50 | ||
|
|
a36b0ebed2 | ||
|
|
5a9e8683e4 | ||
|
|
843b29eff0 | ||
|
|
7bad0be2c4 | ||
|
|
5561011122 | ||
|
|
192a0c987e | ||
|
|
85a9a21e57 | ||
|
|
610864d674 | ||
|
|
06d0fa6e22 | ||
|
|
fb9a6fd314 | ||
|
|
5cab8ad41e | ||
|
|
d9f2b964c9 | ||
|
|
ac78b69ec2 | ||
|
|
516975688f | ||
|
|
7f07439388 | ||
|
|
bf87c4ac52 | ||
|
|
272132af51 | ||
|
|
04dd179631 | ||
|
|
57554c8d4b | ||
|
|
74a20bf11d | ||
|
|
16c96419d3 | ||
|
|
0b444bc5a2 | ||
|
|
7846e9e196 | ||
|
|
a74b971b98 | ||
|
|
6707ce1ed9 | ||
|
|
de421f93b5 | ||
|
|
1762e78cb3 | ||
|
|
2631f3a007 | ||
|
|
f70ec8a090 | ||
|
|
a4f22671c6 | ||
|
|
71c23df971 | ||
|
|
1bb2593c5b | ||
|
|
45d5ef4602 | ||
|
|
7b5b2596b9 | ||
|
|
23a805d576 | ||
|
|
f8cf3c22a1 | ||
|
|
8e5725946c | ||
|
|
99bb0d7a93 | ||
|
|
9695ddf8de | ||
|
|
50cff0bcba | ||
|
|
1832cf5924 | ||
|
|
10fba0a2b2 | ||
|
|
e2050fd47a | ||
|
|
f72da5973f | ||
|
|
0ebeffac32 | ||
|
|
dcfe724803 | ||
|
|
821020e7b6 | ||
|
|
33efc38f4c | ||
|
|
f01c0408e1 | ||
|
|
365ae0ef8c | ||
|
|
a6e593e3a1 | ||
|
|
67d38d6333 | ||
|
|
513d101fc4 | ||
|
|
5e1304f699 | ||
|
|
955a06d689 | ||
|
|
59e1a470e3 | ||
|
|
16952f5695 | ||
|
|
67feca075b | ||
|
|
12368375cf | ||
|
|
284ffb7aa0 | ||
|
|
1aefb26575 | ||
|
|
670d0f8462 | ||
|
|
3bbe422421 | ||
|
|
d68e988c15 | ||
|
|
74fd35047c | ||
|
|
21a9d19105 | ||
|
|
f662e983a4 | ||
|
|
7db379492a | ||
|
|
4a82daccec | ||
|
|
21e44bbc90 | ||
|
|
42a40a844a | ||
|
|
cd7c515097 | ||
|
|
9595626a27 | ||
|
|
db105c90b5 | ||
|
|
74a305c32a | ||
|
|
3af115ce37 | ||
|
|
b4e7934396 | ||
|
|
0a54037460 | ||
|
|
dd8da57cfb | ||
|
|
9b6f2d00ca | ||
|
|
2852821d2c | ||
|
|
673bc75cd5 | ||
|
|
3fad76aae1 | ||
|
|
2cb70a41fb | ||
|
|
ef4f2ed791 | ||
|
|
c9a73a366d | ||
|
|
7973179ddf | ||
|
|
6b656bdc48 | ||
|
|
428c1a1799 | ||
|
|
8892e2b273 | ||
|
|
2e8e6dd39e | ||
|
|
3ac16b15c6 | ||
|
|
ec3dfb908c | ||
|
|
b5484bb13b | ||
|
|
c5a43efafb | ||
|
|
23c2c0689c | ||
|
|
948d64237e | ||
|
|
3433e88bb8 | ||
|
|
28278c091d | ||
|
|
04a68d0b95 | ||
|
|
9b0c0d1870 | ||
|
|
86734d2f5f | ||
|
|
7d5a73828d | ||
|
|
0fd8a78972 | ||
|
|
fca78cf419 | ||
|
|
5f98c8c8bb | ||
|
|
dc4a791a9e | ||
|
|
d89c8cc2d2 | ||
|
|
9b24c683c3 | ||
|
|
2acea40204 | ||
|
|
f014f59cf2 | ||
|
|
6221f4b616 | ||
|
|
32d557ee07 | ||
|
|
ba6eb17a38 | ||
|
|
935436ce49 | ||
|
|
877c5a827c | ||
|
|
eedcff785e | ||
|
|
60865163db | ||
|
|
c32a14a959 | ||
|
|
3ddab57ab5 | ||
|
|
20957106df | ||
|
|
f528f3bf40 | ||
|
|
14fd0d484e | ||
|
|
6fc60f1387 | ||
|
|
5ad46279c2 | ||
|
|
f4894ccb9d | ||
|
|
2b04292424 | ||
|
|
775b58d0fd | ||
|
|
6d09d0984e | ||
|
|
6ce5106271 | ||
|
|
fdbe38496a | ||
|
|
abae32fd70 | ||
|
|
e4a1fa5bb4 | ||
|
|
1b8cf6b1e5 | ||
|
|
135f15dd9f | ||
|
|
1cd4470187 | ||
|
|
3a2f5a77ef | ||
|
|
4201ac9203 | ||
|
|
6c3178a831 | ||
|
|
d48591a4cd | ||
|
|
271885a85e | ||
|
|
57e6765bfd | ||
|
|
9a46596b6c | ||
|
|
27f6ff74fc | ||
|
|
03df019638 | ||
|
|
23750e1222 | ||
|
|
ba0a7ddfa8 | ||
|
|
31327d685b | ||
|
|
bb6bd86e71 | ||
|
|
4ba897660b | ||
|
|
c3fc22c6a9 | ||
|
|
8748b8c277 | ||
|
|
e934d6c983 | ||
|
|
dfb2d3f2bf | ||
|
|
c2a87e2474 | ||
|
|
9229727544 | ||
|
|
cc8dd77d90 | ||
|
|
de9233fba5 | ||
|
|
05cda699a5 | ||
|
|
358a520305 | ||
|
|
57cb98f13a | ||
|
|
33aeb24dbe | ||
|
|
973510a3df | ||
|
|
ae8f67d507 | ||
|
|
a48336db4f | ||
|
|
e69ef0ce25 | ||
|
|
e01c31fb1d | ||
|
|
36d1096976 | ||
|
|
6901228a15 | ||
|
|
8100662eb5 | ||
|
|
ee4c03a427 | ||
|
|
adb2ed0614 | ||
|
|
e78fd8ba94 | ||
|
|
754ecadbb1 | ||
|
|
ee89815628 | ||
|
|
8a9527bf38 | ||
|
|
e476460ab4 | ||
|
|
b556c51307 | ||
|
|
fac812f1dd | ||
|
|
c66cea41bd | ||
|
|
8a3cdfb14c | ||
|
|
a58de31d4b | ||
|
|
04e6897a0d | ||
|
|
59df08cd93 | ||
|
|
c70898ced7 | ||
|
|
ecc38621df | ||
|
|
56327eba54 | ||
|
|
e776532947 | ||
|
|
35f2d89a23 | ||
|
|
3515ce1034 | ||
|
|
6c1046c060 | ||
|
|
7c76c919bc | ||
|
|
54954f276b | ||
|
|
7fd8e0721f | ||
|
|
d075dd27bc | ||
|
|
ace8be3df4 | ||
|
|
c60df2c26b | ||
|
|
cd030c1de0 | ||
|
|
01893b053d | ||
|
|
79e4aa5aaf | ||
|
|
3f4dbb6978 | ||
|
|
1df5b0c629 | ||
|
|
83dbeddb12 | ||
|
|
a8522320ed | ||
|
|
bcaf3de164 | ||
|
|
cbe9f7474e | ||
|
|
3a4b4e129f | ||
|
|
4535b546f4 | ||
|
|
7d0bbc6952 | ||
|
|
ea844e03f9 | ||
|
|
c7a893b02d | ||
|
|
ca75ecbc93 | ||
|
|
3f9d16ab8c | ||
|
|
fb7ff7b891 | ||
|
|
3cf5b97201 | ||
|
|
2f5a9a50af | ||
|
|
8153d3bda9 | ||
|
|
e796e0f35e | ||
|
|
4537fe0390 | ||
|
|
87944a4934 | ||
|
|
33b9d37250 | ||
|
|
ebeec04f83 | ||
|
|
798327eabb | ||
|
|
702dd21eec | ||
|
|
e5754c70da | ||
|
|
b95119bb0e | ||
|
|
ae8600c882 | ||
|
|
271fb4ce87 | ||
|
|
37cee9cebd | ||
|
|
0b2e9c3f62 | ||
|
|
f2741f9725 | ||
|
|
e4a7ba0bb5 | ||
|
|
6ac8519a9f | ||
|
|
01158c2320 | ||
|
|
bbb78ec749 | ||
|
|
90976cc7e0 | ||
|
|
cfe801f280 | ||
|
|
ca3bc69a61 | ||
|
|
43236049ac | ||
|
|
3c797252d8 | ||
|
|
543238be4b | ||
|
|
1b539ade9d | ||
|
|
c0f82b1ae7 | ||
|
|
5b02393c79 | ||
|
|
c54a3f2053 | ||
|
|
e90ec531e5 | ||
|
|
6ac2274c29 | ||
|
|
ea9f547ada | ||
|
|
28cc7c0700 | ||
|
|
6761028afd | ||
|
|
27f87c3a25 | ||
|
|
223c45130f | ||
|
|
3513c1a431 | ||
|
|
5b4185840f | ||
|
|
390f6e016d | ||
|
|
e10e1e0613 | ||
|
|
ba41e6bc5e | ||
|
|
05562b5c2a | ||
|
|
7d9cce131b | ||
|
|
112b096c56 | ||
|
|
aecb100498 | ||
|
|
d5b4aa5d44 | ||
|
|
cbe5e3a9ff | ||
|
|
ff3fa31c25 | ||
|
|
7baf8eba6e | ||
|
|
53b0223257 | ||
|
|
91cc83c054 | ||
|
|
b1031be748 | ||
|
|
c6b76f5200 | ||
|
|
08269b7ce9 | ||
|
|
c65ef8107d |
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: Check Style
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Check Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- javafx
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HMCL_BASE_VERSION: '3.4'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
|
||||
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_BUILD_NUMBER}" != 3 ]; then exit 1; fi
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- 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.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
|
||||
|
||||
@@ -12,19 +12,17 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: HMCL
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest
|
||||
@@ -10,6 +10,7 @@ hs_err_pid*
|
||||
*.2
|
||||
*.log
|
||||
.mine*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
|
||||
# gradle build
|
||||
|
||||
+97
-8
@@ -6,6 +6,8 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
classpath 'org.glavo:pack200:0.3.0'
|
||||
|
||||
classpath 'com.guardsquare:proguard-gradle:7.1.0' // The ProGuard Gradle plugin.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +40,16 @@ if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET"
|
||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.4"
|
||||
def microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
def versionType = System.getenv("VERSION_TYPE") ?: "nightly"
|
||||
version = versionroot + '.' + buildnumber
|
||||
|
||||
mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
|
||||
dependencies {
|
||||
implementation project(":HMCLCore")
|
||||
implementation project(":JSTUN")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
|
||||
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
@@ -114,13 +118,8 @@ compileJava11Java {
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
classifier = "shadow"
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
|
||||
@@ -129,6 +128,7 @@ shadowJar {
|
||||
'Implementation-Version': project.version,
|
||||
'Microsoft-Auth-Id': microsoftAuthId,
|
||||
'Microsoft-Auth-Secret': microsoftAuthSecret,
|
||||
'Build-Channel': versionType,
|
||||
'Class-Path': 'pack200.jar',
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
@@ -152,6 +152,89 @@ shadowJar {
|
||||
'javafx.base/com.sun.javafx.event'
|
||||
].join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
task proguard(type: proguard.gradle.ProGuardTask) {
|
||||
dependsOn shadowJar
|
||||
injars shadowJar
|
||||
|
||||
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
|
||||
|
||||
dontobfuscate
|
||||
dontoptimize
|
||||
|
||||
keep 'class org.jackhuang.** { *; }'
|
||||
keep 'class com.jfoenix.** { *; }'
|
||||
keep 'class com.google.gson.** { *; }'
|
||||
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
|
||||
|
||||
keep 'class * extends com.google.gson.TypeAdapter'
|
||||
keep 'class * implements com.google.gson.TypeAdapterFactory'
|
||||
keep 'class * implements com.google.gson.JsonSerializer'
|
||||
keep 'class * implements com.google.gson.JsonDeserializer'
|
||||
|
||||
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
|
||||
|
||||
System.out.println(new File(".").absolutePath)
|
||||
|
||||
dontwarn 'com.nqzero.**'
|
||||
dontwarn 'org.slf4j.**'
|
||||
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
|
||||
dontwarn 'com.sun.javafx.**'
|
||||
dontwarn 'com.jfoenix.**'
|
||||
dontwarn 'org.apache.**'
|
||||
|
||||
adaptclassstrings
|
||||
|
||||
// next block taken verbatim from Proguard's documentation examples:
|
||||
|
||||
libraryjars files(configurations.compileClasspath.collect())
|
||||
|
||||
keepattributes 'SourceFile,LineNumberTable'
|
||||
keepattributes '*Annotation*'
|
||||
keepattributes 'Signature'
|
||||
|
||||
var javaHome = System.getProperty('java.home')
|
||||
|
||||
// Automatically handle the Java version of this build.
|
||||
if (System.getProperty('java.version').startsWith('1.')) {
|
||||
// Before Java 9, the runtime classes were packaged in a single jar file.
|
||||
libraryjars "${javaHome}/lib/rt.jar"
|
||||
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
|
||||
} else {
|
||||
// As of Java 9, the runtime classes are packaged in modular jmod files.
|
||||
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
|
||||
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
|
||||
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
|
||||
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task finalJar(type: Jar) {
|
||||
dependsOn proguard
|
||||
|
||||
manifest = shadowJar.manifest
|
||||
|
||||
from { proguard.outJarFiles.collect { zipTree(it)} }
|
||||
|
||||
into('/') {
|
||||
from { shadowJar.outputs.files.collect { zipTree(it) } }
|
||||
include("META-INF/versions/**")
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath) // see repack()
|
||||
@@ -160,6 +243,11 @@ shadowJar {
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn finalJar
|
||||
}
|
||||
|
||||
def createExecutable(String suffix, String header) {
|
||||
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
|
||||
output.bytes = new File(project.projectDir, header).bytes
|
||||
@@ -175,7 +263,8 @@ processResources {
|
||||
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
|
||||
bssFile.parentFile.mkdirs()
|
||||
javaexec {
|
||||
main = "com.sun.javafx.css.parser.Css2Bin"
|
||||
classpath = sourceSets.main.compileClasspath
|
||||
mainClass = "com.sun.javafx.css.parser.Css2Bin"
|
||||
args = [cssFile, bssFile]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class FunctionHelper {
|
||||
|
||||
private FunctionHelper() {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> void always(Consumer<T> consumer, T... ts) {
|
||||
Arrays.asList(ts).forEach(consumer);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <A, B> void alwaysA(BiConsumer<A, B> consumer, A a, B... bs) {
|
||||
Arrays.asList(bs).forEach(b -> consumer.accept(a, b));
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <A, B> void alwaysB(BiConsumer<A, B> consumer, B b, A... as) {
|
||||
Arrays.asList(as).forEach(a -> consumer.accept(a, b));
|
||||
}
|
||||
|
||||
public static <A, B> BiConsumer<B, A> exchange(BiConsumer<A, B> consumer) {
|
||||
return (b, a) -> consumer.accept(a, b);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Consumer<T> link(Consumer<T>... consumers) {
|
||||
return t -> {
|
||||
for (Consumer<T> consumer : consumers)
|
||||
consumer.accept(t);
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T extends Event> EventHandler<T> link(EventHandler<T>... handlers) {
|
||||
return t -> {
|
||||
for (EventHandler<T> handler : handlers)
|
||||
handler.handle(t);
|
||||
};
|
||||
}
|
||||
|
||||
public static <A, B> Consumer<A> link1(Function<A, B> function, Consumer<B> consumer) {
|
||||
return a -> consumer.accept(function.apply(a));
|
||||
}
|
||||
|
||||
public static <A, B, C> BiConsumer<A, C> link2(Function<A, B> function, BiConsumer<B, C> consumer) {
|
||||
return (a, c) -> consumer.accept(function.apply(a), c);
|
||||
}
|
||||
|
||||
public static <A, B> Consumer<B> link2(Supplier<A> supplier, BiConsumer<A, B> consumer) {
|
||||
return b -> consumer.accept(supplier.get(), b);
|
||||
}
|
||||
|
||||
public static <A, B> Supplier<B> link1(Supplier<A> supplier, Function<A, B> function) {
|
||||
return () -> function.apply(supplier.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SkinAnimation {
|
||||
|
||||
protected int weight, left;
|
||||
protected List<SkinTransition> transitions;
|
||||
|
||||
@Deprecated
|
||||
public SkinAnimation() {
|
||||
this.transitions = new LinkedList<>();
|
||||
}
|
||||
|
||||
public SkinAnimation(int weight, SkinTransition... transitions) {
|
||||
this.weight = weight;
|
||||
this.transitions = Arrays.asList(transitions);
|
||||
init();
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
transitions.forEach(t -> {
|
||||
EventHandler<ActionEvent> oldHandler = t.getOnFinished();
|
||||
EventHandler<ActionEvent> newHandler = e -> left--;
|
||||
newHandler = oldHandler == null ? newHandler : FunctionHelper.link(oldHandler, newHandler);
|
||||
t.setOnFinished(newHandler);
|
||||
});
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return left > 0;
|
||||
}
|
||||
|
||||
public void play() {
|
||||
transitions.forEach(SkinTransition::play);
|
||||
left = transitions.size();
|
||||
}
|
||||
|
||||
public void playFromStart() {
|
||||
transitions.forEach(SkinTransition::playFromStart);
|
||||
left = transitions.size();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
transitions.forEach(SkinTransition::stop);
|
||||
left = 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
|
||||
public class SkinAnimationPlayer {
|
||||
|
||||
protected final Random random = new Random();
|
||||
protected LinkedList<SkinAnimation> animations = new LinkedList<>();
|
||||
protected SkinAnimation playing;
|
||||
protected boolean running;
|
||||
protected int weightedSum = 0;
|
||||
protected long lastPlayTime = -1L, interval = 10_000_000_000L;
|
||||
protected AnimationTimer animationTimer = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
if (playing == null || !playing.isPlaying() && now - lastPlayTime > interval) {
|
||||
int nextAni = random.nextInt(weightedSum);
|
||||
SkinAnimation tmp = null;
|
||||
for (SkinAnimation animation : animations) {
|
||||
nextAni -= animation.getWeight();
|
||||
tmp = animation;
|
||||
if (nextAni <= 0)
|
||||
break;
|
||||
}
|
||||
playing = tmp;
|
||||
if (playing == null && animations.size() > 0)
|
||||
playing = animations.getLast();
|
||||
if (playing != null) {
|
||||
playing.playFromStart();
|
||||
lastPlayTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public int getWeightedSum() {
|
||||
return weightedSum;
|
||||
}
|
||||
|
||||
public void setInterval(long interval) {
|
||||
this.interval = interval;
|
||||
if (interval < 1)
|
||||
animationTimer.stop();
|
||||
else
|
||||
start();
|
||||
}
|
||||
|
||||
public long getInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
public long getLastPlayTime() {
|
||||
return lastPlayTime;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
return playing != null;
|
||||
}
|
||||
|
||||
public SkinAnimation getPlaying() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
public void addSkinAnimation(SkinAnimation... animations) {
|
||||
this.animations.addAll(Arrays.asList(animations));
|
||||
this.weightedSum = this.animations.stream().mapToInt(SkinAnimation::getWeight).sum();
|
||||
start();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (!running && weightedSum > 0 && interval > 0) {
|
||||
animationTimer.start();
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (running)
|
||||
animationTimer.stop();
|
||||
if (playing != null)
|
||||
playing.stop();
|
||||
running = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.scene.paint.Material;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.Shape3D;
|
||||
import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Translate;
|
||||
|
||||
public class SkinCanvas extends Group {
|
||||
|
||||
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
|
||||
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/steve.png"));
|
||||
|
||||
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
|
||||
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||
|
||||
public static final SkinCube STEVEN_LARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
|
||||
public static final SkinCube STEVEN_RARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
|
||||
|
||||
protected Image srcSkin, skin;
|
||||
protected boolean isSlim;
|
||||
|
||||
protected double preW, preH;
|
||||
protected boolean msaa;
|
||||
|
||||
protected SubScene subScene;
|
||||
protected Group root = new Group();
|
||||
|
||||
public final SkinMultipleCubes headOuter = new SkinMultipleCubes(8, 8, 8, 32F / 64F, 0F, 1.125, 0.2);
|
||||
public final SkinMultipleCubes bodyOuter = new SkinMultipleCubes(8, 12, 4, 16F / 64F, 32F / 64F, 1, 0.2);
|
||||
public final SkinMultipleCubes larmOuter = new SkinMultipleCubes(4, 12, 4, 48F / 64F, 48F / 64F, 1.0625, 0.2);
|
||||
public final SkinMultipleCubes rarmOuter = new SkinMultipleCubes(4, 12, 4, 40F / 64F, 32F / 64F, 1.0625, 0.2);
|
||||
public final SkinMultipleCubes llegOuter = new SkinMultipleCubes(4, 12, 4, 0F / 64F, 48F / 64F, 1.0625, 0.2);
|
||||
public final SkinMultipleCubes rlegOuter = new SkinMultipleCubes(4, 12, 4, 0F / 64F, 32F / 64F, 1.0625, 0.2);
|
||||
|
||||
public final SkinCube headInside = new SkinCube(8, 8, 8, 32F / 64F, 16F / 64F, 0F, 0F, 0F, false);
|
||||
public final SkinCube bodyInside = new SkinCube(8, 12, 4, 24F / 64F, 16F / 64F, 16F / 64F, 16F / 64F, 0.03F, false);
|
||||
public final SkinCube larmInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
|
||||
public final SkinCube rarmInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
|
||||
public final SkinCube llegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 16F / 64F, 48F / 64F, 0F, false);
|
||||
public final SkinCube rlegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 0F, 16F / 64F, 0F, false);
|
||||
|
||||
public final SkinGroup head = new SkinGroup(
|
||||
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
headOuter, headInside
|
||||
);
|
||||
public final SkinGroup body = new SkinGroup(
|
||||
new Rotate(0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, Rotate.Z_AXIS),
|
||||
bodyOuter, bodyInside
|
||||
);
|
||||
public final SkinGroup larm = new SkinGroup(
|
||||
new Rotate(0, 0, -larmInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, +larmInside.getWidth() / 2, -larmInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
larmOuter, larmInside
|
||||
);
|
||||
public final SkinGroup rarm = new SkinGroup(
|
||||
new Rotate(0, 0, -rarmInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, -rarmInside.getWidth() / 2, -rarmInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
rarmOuter, rarmInside
|
||||
);
|
||||
public final SkinGroup lleg = new SkinGroup(
|
||||
new Rotate(0, 0, -llegInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, 0, -llegInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
llegOuter, llegInside
|
||||
);
|
||||
public final SkinGroup rleg = new SkinGroup(
|
||||
new Rotate(0, 0, -rlegInside.getHeight() / 2, 0, Rotate.X_AXIS),
|
||||
new Rotate(0, Rotate.Y_AXIS),
|
||||
new Rotate(0, 0, -rlegInside.getHeight() / 2, 0, Rotate.Z_AXIS),
|
||||
rlegOuter, rlegInside
|
||||
);
|
||||
|
||||
protected PerspectiveCamera camera = new PerspectiveCamera(true);
|
||||
|
||||
protected Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
|
||||
protected Rotate yRotate = new Rotate(180, Rotate.Y_AXIS);
|
||||
protected Rotate zRotate = new Rotate(0, Rotate.Z_AXIS);
|
||||
protected Translate translate = new Translate(0, 0, -80);
|
||||
protected Scale scale = new Scale(1, 1);
|
||||
|
||||
protected SkinAnimationPlayer animationplayer = new SkinAnimationPlayer();
|
||||
|
||||
public SkinAnimationPlayer getAnimationplayer() {
|
||||
return animationplayer;
|
||||
}
|
||||
|
||||
public Image getSrcSkin() {
|
||||
return srcSkin;
|
||||
}
|
||||
|
||||
public Image getSkin() {
|
||||
return skin;
|
||||
}
|
||||
|
||||
public void updateSkin(Image skin, boolean isSlim) {
|
||||
if (SkinHelper.isNoRequest(skin) && SkinHelper.isSkin(skin)) {
|
||||
this.srcSkin = skin;
|
||||
this.skin = SkinHelper.x32Tox64(skin);
|
||||
int multiple = Math.max((int) (1024 / skin.getWidth()), 1);
|
||||
if (multiple > 1)
|
||||
this.skin = SkinHelper.enlarge(this.skin, multiple);
|
||||
if (this.isSlim != isSlim)
|
||||
updateSkinModel(isSlim);
|
||||
bindMaterial(root);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateSkinModel(boolean isSlim) {
|
||||
this.isSlim = isSlim;
|
||||
FunctionHelper.alwaysB(SkinMultipleCubes::setWidth, isSlim ? 3 : 4, larmOuter, rarmOuter);
|
||||
FunctionHelper.alwaysB(SkinCube::setWidth, isSlim ? 3D : 4D, larmInside, rarmInside);
|
||||
|
||||
FunctionHelper.alwaysB(Node::setTranslateX, -(bodyInside.getWidth() + larmInside.getWidth()) / 2, larm);
|
||||
FunctionHelper.alwaysB(Node::setTranslateX, +(bodyInside.getWidth() + rarmInside.getWidth()) / 2, rarm);
|
||||
if (isSlim) {
|
||||
larmInside.setModel(ALEX_LARM.getModel());
|
||||
rarmInside.setModel(ALEX_RARM.getModel());
|
||||
} else {
|
||||
larmInside.setModel(STEVEN_LARM.getModel());
|
||||
rarmInside.setModel(STEVEN_RARM.getModel());
|
||||
}
|
||||
|
||||
larm.getZRotate().setPivotX(-larmInside.getWidth() / 2);
|
||||
rarm.getZRotate().setPivotX(+rarmInside.getWidth() / 2);
|
||||
}
|
||||
|
||||
public SkinCanvas(double preW, double preH) {
|
||||
this(STEVE, preW, preH, true);
|
||||
}
|
||||
|
||||
public SkinCanvas(Image skin, double preW, double preH, boolean msaa) {
|
||||
this.skin = skin;
|
||||
this.preW = preW;
|
||||
this.preH = preH;
|
||||
this.msaa = msaa;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
protected Material createMaterial() {
|
||||
PhongMaterial material = new PhongMaterial();
|
||||
material.setDiffuseMap(skin);
|
||||
return material;
|
||||
}
|
||||
|
||||
protected void bindMaterial(Group group) {
|
||||
Material material = createMaterial();
|
||||
for (Node node : group.getChildren())
|
||||
if (node instanceof Shape3D)
|
||||
((Shape3D) node).setMaterial(material);
|
||||
else if (node instanceof SkinMultipleCubes)
|
||||
((SkinMultipleCubes) node).updateSkin(skin);
|
||||
else if (node instanceof Group)
|
||||
bindMaterial((Group) node);
|
||||
}
|
||||
|
||||
protected Group createPlayerModel() {
|
||||
head.setTranslateY(-(bodyInside.getHeight() + headInside.getHeight()) / 2);
|
||||
|
||||
larm.setTranslateX(-(bodyInside.getWidth() + larmInside.getWidth()) / 2);
|
||||
rarm.setTranslateX(+(bodyInside.getWidth() + rarmInside.getWidth()) / 2);
|
||||
|
||||
lleg.setTranslateX(-(bodyInside.getWidth() - llegInside.getWidth()) / 2);
|
||||
rleg.setTranslateX(+(bodyInside.getWidth() - rlegInside.getWidth()) / 2);
|
||||
|
||||
lleg.setTranslateY(+(bodyInside.getHeight() + llegInside.getHeight()) / 2);
|
||||
rleg.setTranslateY(+(bodyInside.getHeight() + rlegInside.getHeight()) / 2);
|
||||
|
||||
root.getTransforms().addAll(xRotate);
|
||||
|
||||
root.getChildren().addAll(
|
||||
head,
|
||||
body,
|
||||
larm,
|
||||
rarm,
|
||||
lleg,
|
||||
rleg
|
||||
);
|
||||
updateSkin(skin, false);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected SubScene createSubScene() {
|
||||
Group group = new Group();
|
||||
group.getChildren().add(createPlayerModel());
|
||||
group.getTransforms().add(zRotate);
|
||||
|
||||
camera.getTransforms().addAll(yRotate, translate, scale);
|
||||
|
||||
subScene = new SubScene(group, preW, preH, true,
|
||||
msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED);
|
||||
subScene.setCamera(camera);
|
||||
|
||||
return subScene;
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
getChildren().add(createSubScene());
|
||||
}
|
||||
|
||||
private double lastX, lastY;
|
||||
|
||||
public void enableRotation(double sensitivity) {
|
||||
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
lastX = -1;
|
||||
lastY = -1;
|
||||
});
|
||||
addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||
if (!(lastX == -1 || lastY == -1)) {
|
||||
if (e.isAltDown() || e.isControlDown() || e.isShiftDown()) {
|
||||
if (e.isShiftDown())
|
||||
zRotate.setAngle(zRotate.getAngle() - (e.getSceneY() - lastY) * sensitivity);
|
||||
if (e.isAltDown())
|
||||
yRotate.setAngle(yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity);
|
||||
if (e.isControlDown())
|
||||
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity);
|
||||
} else {
|
||||
double yaw = yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity;
|
||||
yaw %= 360;
|
||||
if (yaw < 0)
|
||||
yaw += 360;
|
||||
|
||||
int flagX = yaw < 90 || yaw > 270 ? 1 : -1;
|
||||
int flagZ = yaw < 180 ? -1 : 1;
|
||||
double kx = Math.abs(90 - yaw % 180) / 90 * flagX, kz = Math.abs(90 - (yaw + 90) % 180) / 90 * flagZ;
|
||||
|
||||
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kx);
|
||||
yRotate.setAngle(yaw);
|
||||
zRotate.setAngle(zRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kz);
|
||||
}
|
||||
}
|
||||
lastX = e.getSceneX();
|
||||
lastY = e.getSceneY();
|
||||
});
|
||||
addEventHandler(ScrollEvent.SCROLL, e -> {
|
||||
double delta = (e.getDeltaY() > 0 ? 1 : e.getDeltaY() == 0 ? 0 : -1) / 10D * sensitivity;
|
||||
scale.setX(Math.min(Math.max(scale.getX() - delta, 0.1), 10));
|
||||
scale.setY(Math.min(Math.max(scale.getY() - delta, 0.1), 10));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.shape.Mesh;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import javafx.scene.shape.TriangleMesh;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
public class SkinCube extends MeshView {
|
||||
|
||||
public static class Model extends TriangleMesh {
|
||||
|
||||
public Model(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, boolean isSlim) {
|
||||
getPoints().addAll(createPoints(width, height, depth));
|
||||
getTexCoords().addAll(createTexCoords(width, height, depth, scaleX, scaleY, startX, startY, isSlim));
|
||||
getFaces().addAll(createFaces());
|
||||
}
|
||||
|
||||
public static float[] createPoints(float width, float height, float depth) {
|
||||
width /= 2F;
|
||||
height /= 2F;
|
||||
depth /= 2F;
|
||||
|
||||
return new float[]{
|
||||
-width, -height, depth, // P0
|
||||
width, -height, depth, // P1
|
||||
-width, height, depth, // P2
|
||||
width, height, depth, // P3
|
||||
-width, -height, -depth, // P4
|
||||
width, -height, -depth, // P5
|
||||
-width, height, -depth, // P6
|
||||
width, height, -depth // P7
|
||||
};
|
||||
}
|
||||
|
||||
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
|
||||
float startX, float startY, boolean isSlim) {
|
||||
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
|
||||
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
|
||||
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
|
||||
return new float[]{
|
||||
top_x, top_y, // T0 ---
|
||||
top_x + half_width, top_y, // T1 |
|
||||
top_x + half_width * 2, top_y, // T2 ---
|
||||
bottom_x, middle_y, // T3 ---
|
||||
bottom_x + half_depth, middle_y, // T4 |
|
||||
bottom_x + half_depth + half_width, middle_y, // T5 |
|
||||
bottom_x + scaleX - arm4, middle_y, // T6 |
|
||||
bottom_x + scaleX, middle_y, // T7 ---
|
||||
bottom_x, bottom_y, // T8 ---
|
||||
bottom_x + half_depth, bottom_y, // T9 |
|
||||
bottom_x + half_depth + half_width, bottom_y, // T10 |
|
||||
bottom_x + scaleX - arm4, bottom_y, // T11 |
|
||||
bottom_x + scaleX, bottom_y // T12 ---
|
||||
};
|
||||
}
|
||||
|
||||
public static int[] createFaces() {
|
||||
int[] faces = new int[]{
|
||||
// TOP
|
||||
5, 0, 4, 1, 0, 5, //P5,T0, P4,T1, P0,T5
|
||||
5, 0, 0, 5, 1, 4, //P5,T0, P0,T5, P1,T4
|
||||
// RIGHT
|
||||
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
|
||||
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
|
||||
// FRONT
|
||||
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
|
||||
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
|
||||
// LEFT
|
||||
5, 3, 1, 4, 3, 9, //P5,T6, P1,T5, P3,T10
|
||||
5, 3, 3, 9, 7, 8, //P5,T6, P3,T10,P7,T11
|
||||
// BACK
|
||||
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
|
||||
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
|
||||
// BOTTOM
|
||||
3, 5, 2, 6, 6, 2, //P3,T2, P2,T1, P6,T5
|
||||
3, 5, 6, 2, 7, 1 //P3,T2, P6,T5, P7,T6
|
||||
};
|
||||
|
||||
int[] copy = faces.clone();
|
||||
ArrayUtils.reverse(copy);
|
||||
for (int i = 0; i < copy.length; i += 2) {
|
||||
int tmp = copy[i];
|
||||
copy[i] = copy[i + 1];
|
||||
copy[i + 1] = tmp;
|
||||
}
|
||||
return ArrayUtils.addAll(faces, copy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private double width, height, depth;
|
||||
private boolean isSlim;
|
||||
private Image skin;
|
||||
private Mesh model;
|
||||
|
||||
public SkinCube(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, float enlarge, boolean isSlim) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.depth = depth;
|
||||
this.isSlim = isSlim;
|
||||
setMesh(model = new Model(width + enlarge, height + enlarge, depth + enlarge, scaleX, scaleY, startX, startY, isSlim));
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setDepth(double depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public double getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public boolean isSlim() {
|
||||
return isSlim;
|
||||
}
|
||||
|
||||
public Mesh getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public void setModel(Mesh model) {
|
||||
this.model = model;
|
||||
setMesh(model);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
public class SkinGroup extends Group {
|
||||
|
||||
protected Rotate xRotate, yRotate, zRotate;
|
||||
|
||||
public SkinGroup(Rotate xRotate, Rotate yRotate, Rotate zRotate, Node... nodes) {
|
||||
this.xRotate = xRotate;
|
||||
this.yRotate = yRotate;
|
||||
this.zRotate = zRotate;
|
||||
Group group = new Group();
|
||||
group.getChildren().addAll(nodes);
|
||||
getChildren().add(addRotate(group, xRotate, yRotate, zRotate));
|
||||
}
|
||||
|
||||
protected Group addRotate(Group group, Rotate... rotates) {
|
||||
for (Rotate rotate : rotates)
|
||||
group = addRotate(group, rotate);
|
||||
return group;
|
||||
}
|
||||
|
||||
protected Group addRotate(Group group, Rotate rotate) {
|
||||
Group newGroup = new Group();
|
||||
group.getTransforms().add(rotate);
|
||||
newGroup.getChildren().add(group);
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
public Rotate getXRotate() {
|
||||
return xRotate;
|
||||
}
|
||||
|
||||
public Rotate getYRotate() {
|
||||
return yRotate;
|
||||
}
|
||||
|
||||
public Rotate getZRotate() {
|
||||
return zRotate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
|
||||
public final class SkinHelper {
|
||||
|
||||
private SkinHelper() {
|
||||
}
|
||||
|
||||
public static class PixelCopyer {
|
||||
|
||||
protected Image srcImage;
|
||||
protected WritableImage newImage;
|
||||
|
||||
public PixelCopyer(Image srcImage, WritableImage newImage) {
|
||||
this.srcImage = srcImage;
|
||||
this.newImage = newImage;
|
||||
}
|
||||
|
||||
public void copy(int srcX, int srcY, int width, int height) {
|
||||
copy(srcX, srcY, srcX, srcY, width, height);
|
||||
}
|
||||
|
||||
public void copy(int srcX, int srcY, int toX, int toY, int width, int height) {
|
||||
copy(srcX, srcY, toX, toY, width, height, false, false);
|
||||
}
|
||||
|
||||
public void copy(int srcX, int srcY, int toX, int toY, int width, int height, boolean reversalX, boolean reversalY) {
|
||||
PixelReader reader = srcImage.getPixelReader();
|
||||
PixelWriter writer = newImage.getPixelWriter();
|
||||
for (int x = 0; x < width; x++)
|
||||
for (int y = 0; y < height; y++)
|
||||
writer.setArgb(toX + x, toY + y,
|
||||
reader.getArgb(srcX + (reversalX ? width - x - 1 : x), srcY + (reversalY ? height - y - 1 : y)));
|
||||
}
|
||||
|
||||
public void copy(float srcX, float srcY, float toX, float toY, float width, float height) {
|
||||
copy(srcX, srcY, toX, toY, width, height, false, false);
|
||||
}
|
||||
|
||||
public void copy(float srcX, float srcY, float toX, float toY, float width, float height, boolean reversalX, boolean reversalY) {
|
||||
PixelReader reader = srcImage.getPixelReader();
|
||||
PixelWriter writer = newImage.getPixelWriter();
|
||||
int srcScaleX = (int) srcImage.getWidth();
|
||||
int srcScaleY = (int) srcImage.getHeight();
|
||||
int newScaleX = (int) newImage.getWidth();
|
||||
int newScaleY = (int) newImage.getHeight();
|
||||
int srcWidth = (int) (width * srcScaleX);
|
||||
int srcHeight = (int) (height * srcScaleY);
|
||||
|
||||
for (int x = 0; x < srcWidth; x++)
|
||||
for (int y = 0; y < srcHeight; y++)
|
||||
writer.setArgb((int) (toX * newScaleX + x), (int) (toY * newScaleY + y),
|
||||
reader.getArgb((int) (srcX * srcScaleX + (reversalX ? srcWidth - x - 1 : x)),
|
||||
(int) (srcY * srcScaleY + (reversalY ? srcHeight - y - 1 : y))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static boolean isNoRequest(Image image) {
|
||||
return image.getRequestedWidth() == 0 && image.getRequestedHeight() == 0;
|
||||
}
|
||||
|
||||
public static boolean isSkin(Image image) {
|
||||
return image.getWidth() % 64 == 0 && image.getWidth() / 64 > 0 &&
|
||||
(image.getHeight() == image.getWidth() / 2 || image.getHeight() == image.getWidth());
|
||||
}
|
||||
|
||||
public static Image x32Tox64(Image srcSkin) {
|
||||
if (srcSkin.getHeight() == 64)
|
||||
return srcSkin;
|
||||
|
||||
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth(), (int) srcSkin.getHeight() * 2);
|
||||
PixelCopyer copyer = new PixelCopyer(srcSkin, newSkin);
|
||||
// HEAD & HAT
|
||||
copyer.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
|
||||
// LEFT-LEG
|
||||
x32Tox64(copyer, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
|
||||
// RIGHT-LEG
|
||||
copyer.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
|
||||
// BODY
|
||||
copyer.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
|
||||
// LEFT-ARM
|
||||
x32Tox64(copyer, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
|
||||
// RIGHT-ARM
|
||||
copyer.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
|
||||
|
||||
return newSkin;
|
||||
}
|
||||
|
||||
static void x32Tox64(PixelCopyer copyer, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
|
||||
// TOP
|
||||
copyer.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
|
||||
// BOTTOM
|
||||
copyer.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
|
||||
// INS
|
||||
copyer.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
|
||||
// OUTS
|
||||
copyer.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
|
||||
// FRONT
|
||||
copyer.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
|
||||
// BACK
|
||||
copyer.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
|
||||
}
|
||||
|
||||
public static Image enlarge(Image srcSkin, int multiple) {
|
||||
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multiple, (int) srcSkin.getHeight() * multiple);
|
||||
PixelReader reader = srcSkin.getPixelReader();
|
||||
PixelWriter writer = newSkin.getPixelWriter();
|
||||
|
||||
for (int x = 0, lenX = (int) srcSkin.getWidth(); x < lenX; x++)
|
||||
for (int y = 0, lenY = (int) srcSkin.getHeight(); y < lenY; y++)
|
||||
for (int mx = 0; mx < multiple; mx++)
|
||||
for (int my = 0; my < multiple; my++) {
|
||||
int argb = reader.getArgb(x, y);
|
||||
writer.setArgb(x * multiple + mx, y * multiple + my, argb);
|
||||
}
|
||||
|
||||
return newSkin;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Material;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.Box;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SkinMultipleCubes extends Group {
|
||||
|
||||
public static class Face extends Group {
|
||||
|
||||
public Face(Image image, int startX, int startY, int width, int height, int interval, boolean reverseX, boolean reverseY,
|
||||
Supplier<Box> supplier, BiConsumer<Box, Point2D> consumer) {
|
||||
PixelReader reader = image.getPixelReader();
|
||||
for (int x = 0; x < width; x++)
|
||||
for (int y = 0; y < height; y++) {
|
||||
int argb;
|
||||
if ((argb = reader.getArgb(startX + (reverseX ? width - x - 1 : x) * interval,
|
||||
startY + (reverseY ? height - y - 1 : y) * interval)) != 0) {
|
||||
Box pixel = supplier.get();
|
||||
consumer.accept(pixel, new Point2D(x, y));
|
||||
pixel.setMaterial(createMaterial(Color.rgb(
|
||||
(argb >> 16) & 0xFF, (argb >> 8) & 0xFF, (argb >> 0) & 0xFF)));
|
||||
getChildren().add(pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Material createMaterial(Color color) {
|
||||
return new PhongMaterial(color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected int width, height, depth;
|
||||
protected float startX, startY;
|
||||
protected double length, thick;
|
||||
|
||||
public SkinMultipleCubes(int width, int height, int depth, float startX, float startY, double length, double thick) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.depth = depth;
|
||||
this.startX = startX;
|
||||
this.startY = startY;
|
||||
this.length = length;
|
||||
this.thick = thick;
|
||||
}
|
||||
|
||||
public void setWidth(int width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setDepth(int depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setStartX(float startX) {
|
||||
this.startX = startX;
|
||||
}
|
||||
|
||||
public float getStartX() {
|
||||
return startX;
|
||||
}
|
||||
|
||||
public void setStartY(float startY) {
|
||||
this.startY = startY;
|
||||
}
|
||||
|
||||
public float getStartY() {
|
||||
return startY;
|
||||
}
|
||||
|
||||
public void setLength(double length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public double getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setThick(double thick) {
|
||||
this.thick = thick;
|
||||
}
|
||||
|
||||
public double getThick() {
|
||||
return thick;
|
||||
}
|
||||
|
||||
public void updateSkin(Image skin) {
|
||||
getChildren().clear();
|
||||
int start_x = (int) (startX * skin.getWidth()), start_y = (int) (startY * skin.getHeight()),
|
||||
interval = (int) Math.max(skin.getWidth() / 64, 1),
|
||||
width_interval = width * interval, height_interval = height * interval, depth_interval = depth * interval;
|
||||
// FRONT
|
||||
getChildren().add(new Face(skin, start_x + depth_interval, start_y + depth_interval, width, height, interval, false, false,
|
||||
() -> new Box(length, length, thick), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ((depth * length + thick) / 2.0);
|
||||
}));
|
||||
// BACK
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval * 2, start_y + depth_interval, width, height, interval, true, false,
|
||||
() -> new Box(length, length, thick), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(-(depth * length + thick) / 2.0);
|
||||
}));
|
||||
// LEFT
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y + depth_interval, depth, height, interval, false, false,
|
||||
() -> new Box(thick, length, length), (b, p) -> {
|
||||
b.setTranslateX((width * length + thick) / 2.0);
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
|
||||
}));
|
||||
// RIGHT
|
||||
getChildren().add(new Face(skin, start_x, start_y + depth_interval, depth, height, interval, true, false,
|
||||
() -> new Box(thick, length, length), (b, p) -> {
|
||||
b.setTranslateX(-(width * length + thick) / 2.0);
|
||||
b.setTranslateY(-((height - 1) / 2.0 - p.getY()) * b.getHeight());
|
||||
b.setTranslateZ(((depth - 1) / 2.0 - p.getX()) * b.getDepth());
|
||||
}));
|
||||
// TOP
|
||||
getChildren().add(new Face(skin, start_x + depth_interval, start_y, width, depth, interval, false, false,
|
||||
() -> new Box(length, thick, length), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY(-(height * length + thick) / 2.0);
|
||||
b.setTranslateZ(-((depth - 1) / 2.0 - p.getY()) * b.getDepth());
|
||||
}));
|
||||
// BOTTOM
|
||||
getChildren().add(new Face(skin, start_x + width_interval + depth_interval, start_y, width, depth, interval, false, false,
|
||||
() -> new Box(length, thick, length), (b, p) -> {
|
||||
b.setTranslateX(((width - 1) / 2.0 - p.getX()) * b.getWidth());
|
||||
b.setTranslateY((height * length + thick) / 2.0);
|
||||
b.setTranslateZ(-((depth - 1) / 2.0 - p.getY()) * b.getDepth());
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SkinTransition extends Transition {
|
||||
|
||||
protected Function<Double, Double> expression;
|
||||
protected List<WritableValue<Number>> observables;
|
||||
protected boolean fix;
|
||||
protected int count;
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public SkinTransition(Duration duration, Function<Double, Double> expression, WritableValue<Number>... observables) {
|
||||
setCycleDuration(duration);
|
||||
this.expression = expression;
|
||||
this.observables = Arrays.asList(observables);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
if (frac == 0 || frac == 1)
|
||||
count++;
|
||||
double val = expression.apply(frac);
|
||||
observables.forEach(w -> w.setValue(val));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void play() {
|
||||
count = 0;
|
||||
super.play();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package moe.mickey.minecraft.skin.fx.animation;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import moe.mickey.minecraft.skin.fx.FunctionHelper;
|
||||
import moe.mickey.minecraft.skin.fx.SkinAnimation;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.SkinTransition;
|
||||
|
||||
public final class SkinAniRunning extends SkinAnimation {
|
||||
|
||||
private SkinTransition larmTransition, rarmTransition;
|
||||
|
||||
public SkinAniRunning(int weight, int time, double angle, SkinCanvas canvas) {
|
||||
larmTransition = new SkinTransition(Duration.millis(time),
|
||||
v -> v * (larmTransition.getCount() % 4 < 2 ? 1 : -1) * angle,
|
||||
canvas.larm.getXRotate().angleProperty(), canvas.rleg.getXRotate().angleProperty());
|
||||
|
||||
rarmTransition = new SkinTransition(Duration.millis(time),
|
||||
v -> v * (rarmTransition.getCount() % 4 < 2 ? 1 : -1) * -angle,
|
||||
canvas.rarm.getXRotate().angleProperty(), canvas.lleg.getXRotate().angleProperty());
|
||||
|
||||
FunctionHelper.alwaysB(SkinTransition::setAutoReverse, true, larmTransition, rarmTransition);
|
||||
FunctionHelper.alwaysB(SkinTransition::setCycleCount, 16, larmTransition, rarmTransition);
|
||||
FunctionHelper.always(transitions::add, larmTransition, rarmTransition);
|
||||
this.weight = weight;
|
||||
init();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package moe.mickey.minecraft.skin.fx.animation;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import moe.mickey.minecraft.skin.fx.FunctionHelper;
|
||||
import moe.mickey.minecraft.skin.fx.SkinAnimation;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.SkinTransition;
|
||||
|
||||
public final class SkinAniWavingArms extends SkinAnimation {
|
||||
|
||||
public SkinAniWavingArms(int weight, int time, double angle, SkinCanvas canvas) {
|
||||
SkinTransition larmTransition = new SkinTransition(Duration.millis(time), v -> v * angle,
|
||||
canvas.larm.getZRotate().angleProperty());
|
||||
|
||||
SkinTransition rarmTransition = new SkinTransition(Duration.millis(time), v -> v * -angle,
|
||||
canvas.rarm.getZRotate().angleProperty());
|
||||
|
||||
FunctionHelper.alwaysB(SkinTransition::setAutoReverse, true, larmTransition, rarmTransition);
|
||||
FunctionHelper.alwaysB(SkinTransition::setCycleCount, 2, larmTransition, rarmTransition);
|
||||
FunctionHelper.always(transitions::add, larmTransition, rarmTransition);
|
||||
this.weight = weight;
|
||||
init();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
@@ -31,6 +32,7 @@ import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
@@ -61,6 +63,8 @@ public final class Launcher extends Application {
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load config", e);
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
@@ -106,7 +110,9 @@ public final class Launcher extends Application {
|
||||
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
|
||||
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
|
||||
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
|
||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
@@ -116,7 +122,7 @@ public final class Launcher extends Application {
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
|
||||
launch(args);
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ package org.jackhuang.hmcl;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -33,8 +35,8 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
@@ -59,10 +61,7 @@ public final class Main {
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(() -> {
|
||||
fixLetsEncrypt();
|
||||
checkDSTRootCAX3();
|
||||
}, "CA Certificate Check", true);
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
@@ -99,31 +98,12 @@ public final class Main {
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.log(Level.SEVERE, "unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDSTRootCAX3() {
|
||||
TrustManagerFactory tmf;
|
||||
try {
|
||||
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
} catch (NoSuchAlgorithmException | KeyStoreException e) {
|
||||
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
|
||||
// don't know what to do here
|
||||
return;
|
||||
}
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
|
||||
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
@@ -153,8 +133,9 @@ public final class Main {
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -41,6 +42,9 @@ public final class Metadata {
|
||||
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
|
||||
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Build-Channel")).orElse("nightly");
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
|
||||
@@ -49,8 +53,24 @@ public final class Metadata {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
// to fulfill XDG standard.
|
||||
String xdgCache = System.getenv("XDG_CACHE_HOME");
|
||||
if (StringUtils.isNotBlank(xdgCache)) {
|
||||
return Paths.get(xdgCache, "hmcl");
|
||||
}
|
||||
return Paths.get(home, ".cache", "hmcl");
|
||||
}
|
||||
return OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
|
||||
public static boolean isStable() {
|
||||
return "stable".equals(BUILD_CHANNEL);
|
||||
}
|
||||
|
||||
public static boolean isDev() {
|
||||
return "dev".equals(BUILD_CHANNEL);
|
||||
}
|
||||
|
||||
public static boolean isNightly() {
|
||||
return !isStable() && !isDev();
|
||||
}
|
||||
}
|
||||
@@ -32,21 +32,24 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.ProxyManager;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
@@ -89,6 +92,13 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Version> getDisplayVersions() {
|
||||
return getVersions().stream()
|
||||
.filter(v -> !v.isHidden())
|
||||
.sorted(Comparator.comparing((Version v) -> v.getReleaseTime() == null ? new Date(0L) : v.getReleaseTime())
|
||||
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshVersionsImpl() {
|
||||
localVersionSettings.clear();
|
||||
@@ -105,7 +115,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
if (!file.exists() && !versions.isEmpty())
|
||||
FileUtils.writeText(file, PROFILE);
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
|
||||
// https://github.com/huanghongxun/HMCL/issues/938
|
||||
@@ -128,15 +138,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
File srcDir = getVersionRoot(srcId);
|
||||
File dstDir = getVersionRoot(dstId);
|
||||
Path srcDir = getVersionRoot(srcId).toPath();
|
||||
Path dstDir = getVersionRoot(dstId).toPath();
|
||||
|
||||
if (dstDir.exists()) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
|
||||
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir, dstDir);
|
||||
|
||||
Path fromJson = dstDir.resolve(srcId + ".json");
|
||||
Path fromJar = dstDir.resolve(srcId + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId + ".json");
|
||||
Path toJar = dstDir.resolve(dstId + ".jar");
|
||||
|
||||
if (Files.exists(fromJar)) {
|
||||
Files.move(fromJar, toJar);
|
||||
}
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
|
||||
Files.move(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
@@ -258,7 +279,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
|
||||
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -284,14 +305,14 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
|
||||
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setVersionName(version)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
@@ -307,7 +328,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
.setServerIp(vs.getServerIp())
|
||||
.setWrapper(vs.getWrapper())
|
||||
.setPrecalledCommand(vs.getPreLaunchCommand())
|
||||
.setPreLaunchCommand(vs.getPreLaunchCommand())
|
||||
.setPostExitCommand(vs.getPostExitCommand())
|
||||
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
|
||||
@@ -35,8 +35,6 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final File zipFile;
|
||||
@@ -107,13 +105,5 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStages() {
|
||||
return Stream.concat(
|
||||
dependents.stream().flatMap(task -> task.getStages().stream()),
|
||||
Stream.of("hmcl.modpack")
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "HMCL";
|
||||
}
|
||||
@@ -18,14 +18,17 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.FileSystem;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -37,23 +40,31 @@ public final class HMCLModpackManager {
|
||||
/**
|
||||
* Read the manifest in a HMCL modpack.
|
||||
*
|
||||
* @param file a HMCL modpack file.
|
||||
* @param fs a HMCL modpack file.
|
||||
* @param encoding encoding of modpack zip file.
|
||||
* @return the manifest of HMCL modpack.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
*/
|
||||
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class).setEncoding(encoding);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
|
||||
public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = FileUtils.readText(fs.getPath("modpack.json"));
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = FileUtils.readText(fs.getPath("minecraft/pack.json"));
|
||||
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||
if (game.getJar() == null)
|
||||
if (StringUtils.isBlank(manifest.getVersion()))
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack");
|
||||
else
|
||||
return manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
return manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,23 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
|
||||
import org.jackhuang.hmcl.launch.PermissionException;
|
||||
import org.jackhuang.hmcl.launch.ProcessCreationException;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.launch.*;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
@@ -42,45 +38,34 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.LogWindow;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Queue;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -105,28 +90,25 @@ public final class LauncherHelper {
|
||||
this.launchingStepsPane.setTitle(i18n("version.launch"));
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
public void setTestMode() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
showLogs = true;
|
||||
}
|
||||
|
||||
public void setKeep() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
Logging.LOG.info("Launching game version: " + selectedVersion);
|
||||
|
||||
GameRepository repository = profile.getRepository();
|
||||
Version version = repository.getResolvedVersion(selectedVersion);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
checkGameState(profile, setting, version, () -> {
|
||||
Controllers.dialog(launchingStepsPane);
|
||||
Schedulers.defaultScheduler().execute(this::launch0);
|
||||
});
|
||||
} catch (InterruptedException | RejectedExecutionException ignore) {
|
||||
}
|
||||
});
|
||||
Controllers.dialog(launchingStepsPane);
|
||||
launch0();
|
||||
}
|
||||
|
||||
public void makeLaunchScript(File scriptFile) {
|
||||
@@ -143,7 +125,13 @@ public final class LauncherHelper {
|
||||
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
|
||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||
|
||||
TaskExecutor executor = dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck)
|
||||
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
|
||||
|
||||
TaskExecutor executor = checkGameState(profile, setting, version)
|
||||
.thenComposeAsync(javaVersion -> {
|
||||
javaVersionRef.set(Objects.requireNonNull(javaVersion));
|
||||
return dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck);
|
||||
})
|
||||
.thenComposeAsync(Task.allOf(
|
||||
Task.composeAsync(() -> {
|
||||
if (setting.isNotCheckGame())
|
||||
@@ -151,18 +139,22 @@ public final class LauncherHelper {
|
||||
else
|
||||
return dependencyManager.checkGameCompletionAsync(version, integrityCheck);
|
||||
}), Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
if (setting.isNotCheckGame()) {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
|
||||
else
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}))).withStage("launch.state.dependencies")
|
||||
.thenComposeAsync(() -> {
|
||||
@@ -180,14 +172,15 @@ public final class LauncherHelper {
|
||||
}
|
||||
}).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir());
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
version,
|
||||
authInfo,
|
||||
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
|
||||
launchOptions,
|
||||
launcherVisibility == LauncherVisibility.CLOSE
|
||||
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
||||
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
|
||||
: new HMCLProcessListener(repository, version, authInfo, launchOptions, launchingLatch, gameVersion.isPresent())
|
||||
);
|
||||
}).thenComposeAsync(launcher -> { // launcher is prev task's result
|
||||
if (scriptFile == null) {
|
||||
@@ -218,6 +211,7 @@ public final class LauncherHelper {
|
||||
launchingLatch.await();
|
||||
}).withStage("launch.state.waiting_launching"))
|
||||
.withStagesHint(Lang.immutableListOf(
|
||||
"launch.state.java",
|
||||
"launch.state.dependencies",
|
||||
"launch.state.logging_in",
|
||||
"launch.state.waiting_launching"))
|
||||
@@ -234,7 +228,7 @@ public final class LauncherHelper {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
if (!success) {
|
||||
Exception ex = executor.getException();
|
||||
if (ex != null) {
|
||||
if (!(ex instanceof CancellationException)) {
|
||||
String message;
|
||||
if (ex instanceof CurseCompletionException) {
|
||||
if (ex.getCause() instanceof FileNotFoundException)
|
||||
@@ -288,6 +282,25 @@ public final class LauncherHelper {
|
||||
message = i18n("download.code.404", url);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||
(result, resolve, reject) -> {
|
||||
if (CommandBuilder.setExecutionPolicy()) {
|
||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||
resolve.run();
|
||||
} else {
|
||||
LOG.warning("Failed to set ExecutionPolicy");
|
||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
}
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (ex instanceof AccessDeniedException) {
|
||||
message = i18n("exception.access_denied", ((AccessDeniedException) ex).getFile());
|
||||
} else {
|
||||
message = StringUtils.getStackTrace(ex);
|
||||
}
|
||||
@@ -305,248 +318,278 @@ public final class LauncherHelper {
|
||||
executor.start();
|
||||
}
|
||||
|
||||
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
|
||||
if (setting.isNotCheckJVM()) {
|
||||
onAccept.run();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean flag = false;
|
||||
boolean java8required = false;
|
||||
boolean newJavaRequired = false;
|
||||
|
||||
// Without onAccept called, the launching operation will be terminated.
|
||||
|
||||
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
|
||||
VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown"));
|
||||
JavaVersion java = setting.getJavaVersion();
|
||||
if (java == null) {
|
||||
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept);
|
||||
setting.setJava(null);
|
||||
setting.setDefaultJavaPath(null);
|
||||
java = JavaVersion.fromCurrentEnvironment();
|
||||
flag = true;
|
||||
|
||||
if (setting.isNotCheckJVM()) {
|
||||
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
|
||||
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
|
||||
.withStage("launch.state.java");
|
||||
}
|
||||
|
||||
if (!flag && version.getJavaVersion() != null) {
|
||||
if (java.getParsedVersion() < version.getJavaVersion().getMajorVersion()) {
|
||||
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (acceptableJava.isPresent()) {
|
||||
setting.setJavaVersion(acceptableJava.get());
|
||||
return Task.composeAsync(() -> {
|
||||
return setting.getJavaVersion(gameVersion, version);
|
||||
}).thenComposeAsync(Schedulers.javafx(), javaVersion -> {
|
||||
// Reset invalid java version
|
||||
if (javaVersion == null) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
|
||||
|
||||
if (setting.isJavaAutoSelected()) {
|
||||
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
|
||||
GameJavaVersion targetJavaVersion;
|
||||
|
||||
if (range.getMandatory().contains(VersionNumber.asVersion("16"))) {
|
||||
targetJavaVersion = GameJavaVersion.JAVA_16;
|
||||
} else if (range.getMandatory().contains(VersionNumber.asVersion("1.8.0_51"))) {
|
||||
targetJavaVersion = GameJavaVersion.JAVA_8;
|
||||
} else {
|
||||
targetJavaVersion = null;
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null) {
|
||||
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
|
||||
.thenAcceptAsync(downloadedJavaVersion -> {
|
||||
future.complete(downloadedJavaVersion);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MessageDialogPane dialog = new MessageDialogPane(
|
||||
i18n("launch.advice.require_newer_java_version",
|
||||
gameVersion.toString(),
|
||||
version.getJavaVersion().getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION);
|
||||
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
|
||||
JFXButton linkButton = new JFXButton(i18n("download.external_link"));
|
||||
linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/"));
|
||||
linkButton.getStyleClass().add("dialog-accept");
|
||||
dialog.addButton(linkButton);
|
||||
setting.setJava(null);
|
||||
setting.setDefaultJavaPath(null);
|
||||
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
|
||||
}
|
||||
|
||||
JFXButton yesButton = new JFXButton(i18n("button.ok"));
|
||||
yesButton.setOnAction(event -> {
|
||||
downloadJava(version.getJavaVersion(), profile)
|
||||
.thenAcceptAsync(x -> {
|
||||
try {
|
||||
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
newAcceptableJava.ifPresent(setting::setJavaVersion);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.log(Level.SEVERE, "Cannot list javas", e);
|
||||
return Task.fromCompletableFuture(future);
|
||||
} else {
|
||||
return Task.completed(javaVersion);
|
||||
}
|
||||
}).thenComposeAsync(javaVersion -> {
|
||||
return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version)));
|
||||
}).thenComposeAsync(Schedulers.javafx(), javaVersions -> {
|
||||
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
|
||||
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
|
||||
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
|
||||
|
||||
JavaVersionConstraint violatedMandatoryConstraint = null;
|
||||
JavaVersionConstraint violatedSuggestedConstraint = null;
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion)) {
|
||||
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
|
||||
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
|
||||
violatedMandatoryConstraint = constraint;
|
||||
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
|
||||
violatedSuggestedConstraint = constraint;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
boolean suggested = false;
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
Runnable continueAction = () -> future.complete(javaVersion);
|
||||
Runnable breakAction = () -> {
|
||||
future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
|
||||
};
|
||||
|
||||
if (violatedMandatoryConstraint != null) {
|
||||
if (suggestedJavaVersion != null) {
|
||||
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
|
||||
setting.setJavaAutoSelected();
|
||||
future.complete(suggestedJavaVersion);
|
||||
}, breakAction);
|
||||
return Task.fromCompletableFuture(future);
|
||||
} else {
|
||||
switch (violatedMandatoryConstraint) {
|
||||
case GAME_JSON:
|
||||
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
|
||||
.thenAcceptAsync(downloadedJavaVersion -> {
|
||||
setting.setJavaVersion(downloadedJavaVersion);
|
||||
future.complete(downloadedJavaVersion);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
breakAction.run();
|
||||
return null;
|
||||
});
|
||||
return Task.fromCompletableFuture(future);
|
||||
case VANILLA_JAVA_16:
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
|
||||
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
|
||||
}, breakAction);
|
||||
return null;
|
||||
case VANILLA_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
case VANILLA_LINUX_JAVA_8:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
|
||||
// Windows on ARM introduced translation support for x86-64 after 10.0.21277.
|
||||
|| (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
}
|
||||
}, Platform::runLater).thenAccept(x -> onAccept.run());
|
||||
});
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
dialog.addButton(yesButton);
|
||||
|
||||
JFXButton noButton = new JFXButton(i18n("button.cancel"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
dialog.addButton(noButton);
|
||||
dialog.setCancelButton(noButton);
|
||||
|
||||
Controllers.dialog(dialog);
|
||||
flag = true;
|
||||
}
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case LAUNCH_WRAPPER:
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game later than 1.17 requires Java 16.
|
||||
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_16 && gameVersion.compareTo(VersionNumber.asVersion("1.17")) >= 0) {
|
||||
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_16)
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (acceptableJava.isPresent()) {
|
||||
setting.setJavaVersion(acceptableJava.get());
|
||||
} else {
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
|
||||
FXUtils.openLink("https://adoptopenjdk.net/");
|
||||
}, null);
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64
|
||||
&& javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
|
||||
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.warning"), MessageType.ERROR, continueAction);
|
||||
suggested = true;
|
||||
}
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// Game later than 1.7.2 accepts Java 8.
|
||||
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
|
||||
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (java8.isPresent()) {
|
||||
newJavaRequired = true;
|
||||
setting.setJavaVersion(java8.get());
|
||||
} else {
|
||||
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
|
||||
// Minecraft 1.13 and later versions only support Java 8 or later.
|
||||
// Terminate launching operation.
|
||||
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, null);
|
||||
} else {
|
||||
// Most mods require Java 8 or later version.
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept);
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (javaVersion.getBits() == Bits.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
// Actual memory limit depends on operating system and memory.
|
||||
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), continueAction, breakAction);
|
||||
suggested = true;
|
||||
}
|
||||
|
||||
if (!suggested && violatedSuggestedConstraint != null) {
|
||||
suggested = true;
|
||||
switch (violatedSuggestedConstraint) {
|
||||
case MODDED_JAVA_7:
|
||||
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
return null;
|
||||
case MODDED_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
break;
|
||||
case VANILLA_JAVA_8_51:
|
||||
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
break;
|
||||
}
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
|
||||
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|
||||
&& version.getLibraries().stream()
|
||||
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
|
||||
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
|
||||
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
|
||||
if (java8.isPresent()) {
|
||||
java8required = true;
|
||||
setting.setJavaVersion(java8.get());
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageType.INFORMATION, onAccept);
|
||||
flag = true;
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, null);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
|
||||
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51");
|
||||
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
|
||||
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (java8.isPresent()) {
|
||||
newJavaRequired = true;
|
||||
setting.setJavaVersion(java8.get());
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, onAccept);
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
|
||||
final JavaVersion java32 = java;
|
||||
|
||||
// First find if same java version but whose platform is 64-bit installed.
|
||||
Optional<JavaVersion> java64 = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.getPlatform())
|
||||
.filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
|
||||
if (!java64.isPresent()) {
|
||||
final boolean java8requiredFinal = java8required, newJavaRequiredFinal = newJavaRequired;
|
||||
|
||||
// Then find if other java version which satisfies requirements installed.
|
||||
java64 = JavaVersion.getJavas().stream()
|
||||
.filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.getPlatform())
|
||||
.filter(javaVersion -> {
|
||||
if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8;
|
||||
if (newJavaRequiredFinal) return javaVersion.getParsedVersion() >= JavaVersion.JAVA_8;
|
||||
return true;
|
||||
})
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
}
|
||||
|
||||
if (java64.isPresent()) {
|
||||
setting.setJavaVersion(java64.get());
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageType.ERROR, onAccept);
|
||||
flag = true;
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (!suggested && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, null);
|
||||
suggested = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
// Actual memory limit depends on operating system and memory.
|
||||
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
// Forge 2760~2773 will crash game with LiteLoader.
|
||||
if (!flag) {
|
||||
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
|
||||
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
// Forge 2760~2773 will crash game with LiteLoader.
|
||||
if (!suggested) {
|
||||
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
|
||||
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, null);
|
||||
suggested = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
||||
if (!flag) {
|
||||
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), onAccept, null);
|
||||
flag = true;
|
||||
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
|
||||
if (!suggested) {
|
||||
boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, null);
|
||||
suggested = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!flag)
|
||||
onAccept.run();
|
||||
if (!suggested) {
|
||||
future.complete(javaVersion);
|
||||
}
|
||||
|
||||
return Task.fromCompletableFuture(future);
|
||||
}).withStage("launch.state.java");
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
|
||||
link.setOnAction(e -> {
|
||||
FXUtils.openLink("https://docs.microsoft.com/zh-cn/java/openjdk/download");
|
||||
future.completeExceptionally(new CancellationException());
|
||||
});
|
||||
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("launch.advice.require_newer_java_version",
|
||||
gameVersion,
|
||||
javaVersion.getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION)
|
||||
.addAction(link)
|
||||
.yesOrNo(() -> {
|
||||
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
|
||||
.thenAcceptAsync(downloadedJava -> {
|
||||
future.complete(downloadedJava);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolvedException = resolveException(throwable);
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
future.completeExceptionally(new CancellationException());
|
||||
return null;
|
||||
});
|
||||
}, () -> {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}).build());
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly start java downloading.
|
||||
*
|
||||
* @param javaVersion target Java version
|
||||
* @param downloadProvider download provider
|
||||
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
|
||||
*/
|
||||
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
|
||||
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
|
||||
profile.getDependency().getDownloadProvider()).executor(false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
super.onStop(success, executor);
|
||||
Platform.runLater(() -> {
|
||||
if (!success) {
|
||||
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion, downloadProvider)
|
||||
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
|
||||
if (exception != null) {
|
||||
future.completeExceptionally(exception);
|
||||
} else {
|
||||
future.complete(null);
|
||||
future.complete(downloadedJava);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.executor(false);
|
||||
|
||||
javaDownloadingPane.setExecutor(executor, true);
|
||||
Controllers.dialog(javaDownloadingPane);
|
||||
executor.start();
|
||||
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -582,8 +625,9 @@ public final class LauncherHelper {
|
||||
class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final String version;
|
||||
private final Version version;
|
||||
private final Map<String, String> forbiddenTokens;
|
||||
private final LaunchOptions launchOptions;
|
||||
private ManagedProcess process;
|
||||
private boolean lwjgl;
|
||||
private LogWindow logWindow;
|
||||
@@ -592,9 +636,10 @@ public final class LauncherHelper {
|
||||
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch launchingLatch;
|
||||
|
||||
public HMCLProcessListener(HMCLGameRepository repository, String version, AuthInfo authInfo, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.launchOptions = launchOptions;
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
|
||||
@@ -618,6 +663,11 @@ public final class LauncherHelper {
|
||||
|
||||
LOG.info("Launched process: " + command);
|
||||
|
||||
String classpath = process.getClasspath();
|
||||
if (classpath != null) {
|
||||
LOG.info("Process ClassPath: " + classpath);
|
||||
}
|
||||
|
||||
if (showLogs)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
@@ -703,43 +753,8 @@ public final class LauncherHelper {
|
||||
if (!lwjgl) finishLaunch();
|
||||
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
repository.markVersionLaunchedAbnormally(version);
|
||||
Platform.runLater(() -> {
|
||||
if (logWindow == null) {
|
||||
logWindow = new LogWindow();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
switch (exitType) {
|
||||
case JVM_ERROR:
|
||||
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
|
||||
break;
|
||||
case APPLICATION_ERROR:
|
||||
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
|
||||
break;
|
||||
}
|
||||
|
||||
logWindow.showGameCrashReport(logs -> {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
LogExporter.exportLogs(logFile, repository, version, logs, new CommandBuilder().addAll(process.getCommands()).toString())
|
||||
.thenRunAsync(() -> {
|
||||
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx())
|
||||
.exceptionally(e -> {
|
||||
LOG.log(Level.WARNING, "Failed to export game crash info", e);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
repository.markVersionLaunchedAbnormally(version.getId());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
|
||||
}
|
||||
|
||||
checkExit();
|
||||
@@ -748,6 +763,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
while (!PROCESSES.isEmpty())
|
||||
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.game;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ManuallyCreatedModpackException extends Exception {
|
||||
private final Path path;
|
||||
|
||||
public ManuallyCreatedModpackException(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.game;
|
||||
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
|
||||
private final Profile profile;
|
||||
private final Path zipFile;
|
||||
private final Charset charset;
|
||||
private final String name;
|
||||
|
||||
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
|
||||
this.profile = profile;
|
||||
this.zipFile = zipFile;
|
||||
this.charset = charset;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path subdirectory;
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
|
||||
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
|
||||
}
|
||||
|
||||
Path dest = Paths.get("externalgames").resolve(name);
|
||||
|
||||
setResult(dest);
|
||||
|
||||
new Unzipper(zipFile, dest)
|
||||
.setSubDirectory(subdirectory.toString())
|
||||
.setTerminateIfSubDirectoryNotExists()
|
||||
.setEncoding(charset)
|
||||
.unzip();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -38,54 +39,97 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
|
||||
public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception ignored) {
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
}
|
||||
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
|
||||
throw new ManuallyCreatedModpackException(file);
|
||||
} catch (IOException e) {
|
||||
// ignore it
|
||||
}
|
||||
|
||||
throw new UnsupportedModpackException(file.toString());
|
||||
}
|
||||
|
||||
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
|
||||
Path root = fs.getPath("/");
|
||||
if (isMinecraftDirectory(root)) return root;
|
||||
try (Stream<Path> firstLayer = Files.list(root)) {
|
||||
for (Path dir : toIterable(firstLayer)) {
|
||||
if (isMinecraftDirectory(dir)) return dir;
|
||||
|
||||
try (Stream<Path> secondLayer = Files.list(dir)) {
|
||||
for (Path subdir : toIterable(secondLayer)) {
|
||||
if (isMinecraftDirectory(subdir)) return subdir;
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
throw new UnsupportedModpackException(modpackName);
|
||||
}
|
||||
|
||||
private static boolean isMinecraftDirectory(Path path) {
|
||||
return Files.isDirectory(path.resolve("versions")) &&
|
||||
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
|
||||
}
|
||||
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
|
||||
if (!file.exists())
|
||||
throw new FileNotFoundException(file.getPath());
|
||||
@@ -109,7 +153,7 @@ public final class ModpackHelper {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
|
||||
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -129,10 +173,29 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
public static boolean isExternalGameNameConflicts(String name) {
|
||||
return Files.exists(Paths.get("externalgames").resolve(name));
|
||||
}
|
||||
|
||||
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
|
||||
if (isExternalGameNameConflicts(name)) {
|
||||
throw new IllegalArgumentException("name existing");
|
||||
}
|
||||
|
||||
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
|
||||
.thenAcceptAsync(Schedulers.javafx(), location -> {
|
||||
Profile newProfile = new Profile(name, location.toFile());
|
||||
newProfile.setUseRelativePath(true);
|
||||
Profiles.getProfiles().add(newProfile);
|
||||
Profiles.setSelectedProfile(newProfile);
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
@@ -151,36 +214,30 @@ public final class ModpackHelper {
|
||||
}
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof CurseManifest)
|
||||
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof HMCLModpackManifest)
|
||||
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else if (modpack.getManifest() instanceof ServerModpackManifest)
|
||||
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure);
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
|
||||
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
|
||||
.whenComplete(Schedulers.javafx(), success, failure);
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
switch (configuration.getType()) {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name));
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
|
||||
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
|
||||
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
|
||||
|
||||
switch (configuration.getType()) {
|
||||
|
||||
+90
-11
@@ -19,15 +19,19 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -37,13 +41,15 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MicrosoftAuthenticationServer extends NanoHTTPD implements MicrosoftService.OAuthSession {
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
public static String lastlyOpenedURL;
|
||||
|
||||
private MicrosoftAuthenticationServer(int port) {
|
||||
private String idToken;
|
||||
|
||||
private OAuthServer(int port) {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
@@ -59,22 +65,47 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
return future.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
if (session.getMethod() != Method.GET || !"/auth-response".equals(session.getUri())) {
|
||||
if (!"/auth-response".equals(session.getUri())) {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
|
||||
}
|
||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(session.getQueryParameterString()));
|
||||
|
||||
if (session.getMethod() == Method.POST) {
|
||||
Map<String, String> files = new HashMap<>();
|
||||
try {
|
||||
session.parseBody(files);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to read post data", e);
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
} catch (ResponseException re) {
|
||||
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
|
||||
}
|
||||
} else if (session.getMethod() == Method.GET) {
|
||||
// do nothing
|
||||
} else {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
|
||||
}
|
||||
String parameters = session.getQueryParameterString();
|
||||
|
||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
||||
if (query.containsKey("code")) {
|
||||
idToken = query.get("id_token");
|
||||
future.complete(query.get("code"));
|
||||
} else {
|
||||
Logging.LOG.warning("Error: " + parameters);
|
||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||
}
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(MicrosoftAuthenticationServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
@@ -90,15 +121,21 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||
}
|
||||
|
||||
public static class Factory implements MicrosoftService.OAuthCallback {
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public MicrosoftService.OAuthSession startServer() throws IOException {
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
if (StringUtils.isBlank(getClientId())) {
|
||||
throw new MicrosoftAuthenticationNotSupportedException();
|
||||
}
|
||||
|
||||
IOException exception = null;
|
||||
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
|
||||
try {
|
||||
MicrosoftAuthenticationServer server = new MicrosoftAuthenticationServer(port);
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
OAuthServer server = new OAuthServer(port);
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
|
||||
return server;
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
@@ -107,10 +144,17 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantDeviceCode(String userCode, String verificationURI) {
|
||||
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,4 +170,39 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class GrantDeviceCodeEvent extends Event {
|
||||
private final String userCode;
|
||||
private final String verificationUri;
|
||||
|
||||
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
|
||||
super(source);
|
||||
this.userCode = userCode;
|
||||
this.verificationUri = verificationUri;
|
||||
}
|
||||
|
||||
public String getUserCode() {
|
||||
return userCode;
|
||||
}
|
||||
|
||||
public String getVerificationUri() {
|
||||
return verificationUri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OpenBrowserEvent extends Event {
|
||||
private final String url;
|
||||
|
||||
public OpenBrowserEvent(Object source, String url) {
|
||||
super(source);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,7 @@ import javafx.beans.property.ReadOnlyListWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterDeletedException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.*;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
@@ -40,7 +34,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
@@ -81,10 +75,12 @@ public final class Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(new MicrosoftAuthenticationServer.Factory()));
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
@@ -340,14 +336,21 @@ public final class Accounts {
|
||||
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
|
||||
String remoteMessage = remoteException.getRemoteMessage();
|
||||
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
|
||||
if (remoteMessage.contains("Invalid credentials"))
|
||||
if (remoteMessage.contains("Invalid credentials")) {
|
||||
return i18n("account.failed.invalid_credentials");
|
||||
else if (remoteMessage.contains("Invalid token"))
|
||||
} else if (remoteMessage.contains("Invalid token")) {
|
||||
return i18n("account.failed.invalid_token");
|
||||
else if (remoteMessage.contains("Invalid username or password"))
|
||||
} else if (remoteMessage.contains("Invalid username or password")) {
|
||||
return i18n("account.failed.invalid_password");
|
||||
else
|
||||
} else {
|
||||
return remoteMessage;
|
||||
}
|
||||
} else if ("ResourceException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
|
||||
if (remoteMessage.contains("The requested resource is no longer available")) {
|
||||
return i18n("account.failed.migration");
|
||||
} else {
|
||||
return remoteMessage;
|
||||
}
|
||||
}
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
@@ -371,6 +374,10 @@ public final class Accounts {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family_probably");
|
||||
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
return i18n("account.methods.microsoft.snapshot");
|
||||
} else if (exception instanceof OAuthAccount.WrongAccountException) {
|
||||
return i18n("account.failed.wrong_account");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -34,7 +34,6 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
@@ -83,6 +82,9 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("bgpath")
|
||||
private StringProperty backgroundImage = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("bgurl")
|
||||
private StringProperty backgroundImageUrl = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("commonDirType")
|
||||
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
|
||||
@@ -149,15 +151,18 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("fontSize")
|
||||
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
|
||||
|
||||
@SerializedName("launcherFontFamily")
|
||||
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("logLines")
|
||||
private IntegerProperty logLines = new SimpleIntegerProperty(100);
|
||||
|
||||
@SerializedName("titleTransparent")
|
||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
|
||||
|
||||
@SerializedName("updateChannel")
|
||||
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
|
||||
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
@@ -239,6 +244,18 @@ public final class Config implements Cloneable, Observable {
|
||||
return backgroundImage;
|
||||
}
|
||||
|
||||
public String getBackgroundImageUrl() {
|
||||
return backgroundImageUrl.get();
|
||||
}
|
||||
|
||||
public StringProperty backgroundImageUrlProperty() {
|
||||
return backgroundImageUrl;
|
||||
}
|
||||
|
||||
public void setBackgroundImageUrl(String backgroundImageUrl) {
|
||||
this.backgroundImageUrl.set(backgroundImageUrl);
|
||||
}
|
||||
|
||||
public EnumCommonDirectory getCommonDirType() {
|
||||
return commonDirType.get();
|
||||
}
|
||||
@@ -487,6 +504,18 @@ public final class Config implements Cloneable, Observable {
|
||||
return fontSize;
|
||||
}
|
||||
|
||||
public String getLauncherFontFamily() {
|
||||
return launcherFontFamily.get();
|
||||
}
|
||||
|
||||
public StringProperty launcherFontFamilyProperty() {
|
||||
return launcherFontFamily;
|
||||
}
|
||||
|
||||
public void setLauncherFontFamily(String launcherFontFamily) {
|
||||
this.launcherFontFamily.set(launcherFontFamily);
|
||||
}
|
||||
|
||||
public int getLogLines() {
|
||||
return logLines.get();
|
||||
}
|
||||
@@ -503,18 +532,6 @@ public final class Config implements Cloneable, Observable {
|
||||
return authlibInjectorServers;
|
||||
}
|
||||
|
||||
public UpdateChannel getUpdateChannel() {
|
||||
return updateChannel.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateChannel> updateChannelProperty() {
|
||||
return updateChannel;
|
||||
}
|
||||
|
||||
public void setUpdateChannel(UpdateChannel updateChannel) {
|
||||
this.updateChannel.set(updateChannel);
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
return configVersion.get();
|
||||
}
|
||||
@@ -550,4 +567,16 @@ public final class Config implements Cloneable, Observable {
|
||||
public StringProperty preferredLoginTypeProperty() {
|
||||
return preferredLoginType;
|
||||
}
|
||||
|
||||
public boolean isTitleTransparent() {
|
||||
return titleTransparent.get();
|
||||
}
|
||||
|
||||
public BooleanProperty titleTransparentProperty() {
|
||||
return titleTransparent;
|
||||
}
|
||||
|
||||
public void setTitleTransparent(boolean titleTransparent) {
|
||||
this.titleTransparent.set(titleTransparent);
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,14 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.*;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -36,13 +35,16 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public final class ConfigHolder {
|
||||
|
||||
private ConfigHolder() {}
|
||||
private ConfigHolder() {
|
||||
}
|
||||
|
||||
public static final String CONFIG_FILENAME = "hmcl.json";
|
||||
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
|
||||
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("config.json");
|
||||
|
||||
private static Path configLocation;
|
||||
private static Config configInstance;
|
||||
private static GlobalConfig globalConfigInstance;
|
||||
private static boolean newlyCreated;
|
||||
|
||||
public static Config config() {
|
||||
@@ -52,6 +54,13 @@ public final class ConfigHolder {
|
||||
return configInstance;
|
||||
}
|
||||
|
||||
public static GlobalConfig globalConfig() {
|
||||
if (globalConfigInstance == null) {
|
||||
throw new IllegalStateException("Configuration hasn't been loaded");
|
||||
}
|
||||
return globalConfigInstance;
|
||||
}
|
||||
|
||||
public static boolean isNewlyCreated() {
|
||||
return newlyCreated;
|
||||
}
|
||||
@@ -68,6 +77,9 @@ public final class ConfigHolder {
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> markConfigDirty());
|
||||
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
|
||||
|
||||
Settings.init();
|
||||
|
||||
if (newlyCreated) {
|
||||
@@ -84,9 +96,16 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +114,7 @@ public final class ConfigHolder {
|
||||
try {
|
||||
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toURI()).toAbsolutePath();
|
||||
if (Files.isRegularFile(jarPath)) {
|
||||
if (Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
jarPath = jarPath.getParent();
|
||||
exePath = jarPath;
|
||||
|
||||
@@ -145,7 +164,7 @@ public final class ConfigHolder {
|
||||
return new Config();
|
||||
}
|
||||
|
||||
private static InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
private static final InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
try {
|
||||
writeToConfig(content);
|
||||
} catch (IOException e) {
|
||||
@@ -167,4 +186,48 @@ public final class ConfigHolder {
|
||||
private static void saveConfigSync() throws IOException {
|
||||
writeToConfig(configInstance.toJson());
|
||||
}
|
||||
|
||||
// Global Config
|
||||
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
GlobalConfig deserialized = GlobalConfig.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Malformed config.", e);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Creating an empty global config");
|
||||
return new GlobalConfig();
|
||||
}
|
||||
|
||||
private static final InvocationDispatcher<String> globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
try {
|
||||
writeToGlobalConfig(content);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to save config", e);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToGlobalConfig(String content) throws IOException {
|
||||
LOG.info("Saving global config");
|
||||
synchronized (GLOBAL_CONFIG_PATH) {
|
||||
Files.write(GLOBAL_CONFIG_PATH, content.getBytes(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
static void markGlobalConfigDirty() {
|
||||
globalConfigWriter.accept(globalConfigInstance.toJson());
|
||||
}
|
||||
|
||||
private static void saveGlobalConfigSync() throws IOException {
|
||||
writeToConfig(globalConfigInstance.toJson());
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,14 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -135,7 +138,7 @@ public final class DownloadProviders {
|
||||
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Exception exception) {
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
@@ -147,9 +150,19 @@ public final class DownloadProviders {
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else if (exception instanceof CancellationException) {
|
||||
return i18n("message.cancelled");
|
||||
}
|
||||
return StringUtils.getStackTrace(exception);
|
||||
}
|
||||
|
||||
@@ -19,5 +19,8 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
public enum EnumBackgroundImage {
|
||||
DEFAULT,
|
||||
CUSTOM
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.hildan.fxgson.creators.ObservableListCreator;
|
||||
import org.hildan.fxgson.creators.ObservableMapCreator;
|
||||
import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.Proxy;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||
public class GlobalConfig implements Cloneable, Observable {
|
||||
|
||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
||||
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
|
||||
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
@Nullable
|
||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
GlobalConfig instance = new GlobalConfig();
|
||||
PropertyUtils.copyProperties(loaded, instance);
|
||||
instance.unknownFields.putAll(loaded.unknownFields);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private StringProperty multiplayerToken = new SimpleStringProperty();
|
||||
|
||||
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
|
||||
|
||||
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public GlobalConfig() {
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return CONFIG_GSON.toJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalConfig clone() {
|
||||
return fromJson(this.toJson());
|
||||
}
|
||||
|
||||
public int getAgreementVersion() {
|
||||
return agreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty agreementVersionProperty() {
|
||||
return agreementVersion;
|
||||
}
|
||||
|
||||
public void setAgreementVersion(int agreementVersion) {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public boolean isMultiplayerRelay() {
|
||||
return multiplayerRelay.get();
|
||||
}
|
||||
|
||||
public BooleanProperty multiplayerRelayProperty() {
|
||||
return multiplayerRelay;
|
||||
}
|
||||
|
||||
public void setMultiplayerRelay(boolean multiplayerRelay) {
|
||||
this.multiplayerRelay.set(multiplayerRelay);
|
||||
}
|
||||
|
||||
public int getMultiplayerAgreementVersion() {
|
||||
return multiplayerAgreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty multiplayerAgreementVersionProperty() {
|
||||
return multiplayerAgreementVersion;
|
||||
}
|
||||
|
||||
public void setMultiplayerAgreementVersion(int multiplayerAgreementVersion) {
|
||||
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
|
||||
}
|
||||
|
||||
public String getMultiplayerToken() {
|
||||
return multiplayerToken.get();
|
||||
}
|
||||
|
||||
public StringProperty multiplayerTokenProperty() {
|
||||
return multiplayerToken;
|
||||
}
|
||||
|
||||
public void setMultiplayerToken(String multiplayerToken) {
|
||||
this.multiplayerToken.set(multiplayerToken);
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"multiplayerToken",
|
||||
"multiplayerAgreementVersion"
|
||||
));
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) {
|
||||
return JsonNull.INSTANCE;
|
||||
}
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
|
||||
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
|
||||
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
|
||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (!(json instanceof JsonObject)) return null;
|
||||
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
|
||||
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
|
||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
||||
if (!knownFields.contains(entry.getKey())) {
|
||||
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class HMCLAccounts {
|
||||
|
||||
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
||||
|
||||
private HMCLAccounts() {
|
||||
}
|
||||
|
||||
public static HMCLAccount getAccount() {
|
||||
return account.get();
|
||||
}
|
||||
|
||||
public static ObjectProperty<HMCLAccount> accountProperty() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public static void setAccount(HMCLAccount account) {
|
||||
HMCLAccounts.account.set(account);
|
||||
}
|
||||
|
||||
public static Task<Void> login() {
|
||||
String nonce = UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
String scope = "openid offline_access";
|
||||
|
||||
return Task.supplyAsync(() -> {
|
||||
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
|
||||
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
mapOf(
|
||||
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
|
||||
pair("response_type", "id_token code"),
|
||||
pair("response_mode", "form_post"),
|
||||
pair("scope", scope),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("nonce", nonce)
|
||||
)));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
|
||||
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
|
||||
.getString();
|
||||
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
OAuth.AuthorizationResponse.class);
|
||||
|
||||
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
|
||||
.header("Token-Type", response.tokenType)
|
||||
.header("Access-Token", response.accessToken)
|
||||
.header("Authorization-Provider", "microsoft")
|
||||
.authorization("Bearer", session.getIdToken())
|
||||
.getJson(HMCLAccountProfile.class);
|
||||
|
||||
return new HMCLAccount("microsoft", profile.nickname, profile.email, profile.role, session.getIdToken(), response.tokenType, response.accessToken, response.refreshToken);
|
||||
}).thenAcceptAsync(Schedulers.javafx(), account -> {
|
||||
setAccount(account);
|
||||
});
|
||||
}
|
||||
|
||||
public static class HMCLAccount implements HttpRequest.Authorization {
|
||||
private final String provider;
|
||||
private final String nickname;
|
||||
private final String email;
|
||||
private final String role;
|
||||
private final String idToken;
|
||||
private final String tokenType;
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
public HMCLAccount(String provider, String nickname, String email, String role, String idToken, String tokenType, String accessToken, String refreshToken) {
|
||||
this.provider = provider;
|
||||
this.nickname = nickname;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
this.idToken = idToken;
|
||||
this.tokenType = tokenType;
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public String getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class HMCLAccountProfile {
|
||||
@SerializedName("ID")
|
||||
String id;
|
||||
@SerializedName("Provider")
|
||||
String provider;
|
||||
@SerializedName("Email")
|
||||
String email;
|
||||
@SerializedName("NickName")
|
||||
String nickname;
|
||||
@SerializedName("Role")
|
||||
String role;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
public SambaException() {
|
||||
}
|
||||
|
||||
public SambaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SambaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public SambaException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -55,7 +56,7 @@ public class Theme {
|
||||
|
||||
Theme(String name, String color) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.color = Objects.requireNonNull(color);
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
@@ -79,9 +80,18 @@ public class Theme {
|
||||
return isLight() ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
public String[] getStylesheets() {
|
||||
public String[] getStylesheets(String overrideFontFamily) {
|
||||
Color textFill = getForegroundColor();
|
||||
|
||||
String fontFamily;
|
||||
if (System.getProperty("hmcl.font.override") != null) {
|
||||
fontFamily = System.getProperty("hmcl.font.override");
|
||||
} else if (overrideFontFamily != null) {
|
||||
fontFamily = overrideFontFamily;
|
||||
} else {
|
||||
fontFamily = null;
|
||||
}
|
||||
|
||||
String css;
|
||||
try {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
@@ -93,7 +103,7 @@ public class Theme {
|
||||
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
|
||||
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
|
||||
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
|
||||
.replace("%font%", Optional.ofNullable(System.getProperty("hmcl.font.override")).map(fontFamily -> "-fx-font-family: " + fontFamily + ";").orElse("")));
|
||||
.replace("%font%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse("")));
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
|
||||
@@ -21,14 +21,16 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.NativesDirectoryType;
|
||||
import org.jackhuang.hmcl.game.ProcessPriority;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
@@ -36,8 +38,11 @@ import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -105,6 +110,15 @@ public final class VersionSetting implements Cloneable {
|
||||
setDefaultJavaPath(null);
|
||||
}
|
||||
|
||||
public boolean isJavaAutoSelected() {
|
||||
return "Auto".equals(getJava());
|
||||
}
|
||||
|
||||
public void setJavaAutoSelected() {
|
||||
setJava("Auto");
|
||||
setDefaultJavaPath(null);
|
||||
}
|
||||
|
||||
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
|
||||
|
||||
/**
|
||||
@@ -115,6 +129,10 @@ public final class VersionSetting implements Cloneable {
|
||||
return defaultJavaPathProperty.get();
|
||||
}
|
||||
|
||||
public StringProperty defaultJavaPathPropertyProperty() {
|
||||
return defaultJavaPathProperty;
|
||||
}
|
||||
|
||||
public void setDefaultJavaPath(String defaultJavaPath) {
|
||||
defaultJavaPathProperty.set(defaultJavaPath);
|
||||
}
|
||||
@@ -269,6 +287,24 @@ public final class VersionSetting implements Cloneable {
|
||||
preLaunchCommandProperty.set(preLaunchCommand);
|
||||
}
|
||||
|
||||
private final StringProperty postExitCommand = new SimpleStringProperty(this, "postExitCommand", "");
|
||||
|
||||
public StringProperty postExitCommandProperty() {
|
||||
return postExitCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command that will be executed after game exits.
|
||||
* Operating system relevant.
|
||||
*/
|
||||
public String getPostExitCommand() {
|
||||
return postExitCommand.get();
|
||||
}
|
||||
|
||||
public void setPostExitCommand(String postExitCommand) {
|
||||
this.postExitCommand.set(postExitCommand);
|
||||
}
|
||||
|
||||
// options
|
||||
|
||||
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
|
||||
@@ -553,38 +589,50 @@ public final class VersionSetting implements Cloneable {
|
||||
launcherVisibilityProperty.set(launcherVisibility);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion() throws InterruptedException {
|
||||
return getJavaVersion(true);
|
||||
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version) {
|
||||
return getJavaVersion(gameVersion, version, true);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
|
||||
// TODO: lazy initialization may result in UI suspension.
|
||||
if (StringUtils.isBlank(getJava()))
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
|
||||
else if (isUsesCustomJavaDir()) {
|
||||
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
if (StringUtils.isBlank(getJava())) {
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||
}
|
||||
}).thenSupplyAsync(() -> {
|
||||
try {
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform());
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
if ("Default".equals(getJava())) {
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else if (isJavaAutoSelected()) {
|
||||
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
|
||||
} else if (isUsesCustomJavaDir()) {
|
||||
try {
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
}
|
||||
} else if (StringUtils.isNotBlank(getJava())) {
|
||||
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
|
||||
.filter(java -> java.getVersion().equals(getJava()))
|
||||
.collect(Collectors.toList());
|
||||
if (matchedJava.isEmpty()) {
|
||||
runInFX(() -> {
|
||||
setJava("Default");
|
||||
});
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else {
|
||||
return matchedJava.stream()
|
||||
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
|
||||
.findFirst()
|
||||
.orElse(matchedJava.get(0));
|
||||
}
|
||||
} else throw new Error();
|
||||
} catch (InterruptedException e) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
} else if (StringUtils.isNotBlank(getJava())) {
|
||||
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
|
||||
.filter(java -> java.getVersion().equals(getJava()))
|
||||
.collect(Collectors.toList());
|
||||
if (matchedJava.isEmpty()) {
|
||||
setJava("Default");
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else {
|
||||
return matchedJava.stream()
|
||||
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
|
||||
.findFirst()
|
||||
.orElse(matchedJava.get(0));
|
||||
}
|
||||
} else throw new Error();
|
||||
});
|
||||
}
|
||||
|
||||
public void setJavaVersion(JavaVersion java) {
|
||||
@@ -602,6 +650,7 @@ public final class VersionSetting implements Cloneable {
|
||||
minMemoryProperty.addListener(listener);
|
||||
autoMemory.addListener(listener);
|
||||
preLaunchCommandProperty.addListener(listener);
|
||||
postExitCommand.addListener(listener);
|
||||
javaArgsProperty.addListener(listener);
|
||||
minecraftArgsProperty.addListener(listener);
|
||||
noJVMArgsProperty.addListener(listener);
|
||||
@@ -636,6 +685,7 @@ public final class VersionSetting implements Cloneable {
|
||||
versionSetting.setMinMemory(getMinMemory());
|
||||
versionSetting.setAutoMemory(isAutoMemory());
|
||||
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
|
||||
versionSetting.setPostExitCommand(getPostExitCommand());
|
||||
versionSetting.setJavaArgs(getJavaArgs());
|
||||
versionSetting.setMinecraftArgs(getMinecraftArgs());
|
||||
versionSetting.setNoJVMArgs(isNoJVMArgs());
|
||||
@@ -673,6 +723,7 @@ public final class VersionSetting implements Cloneable {
|
||||
obj.addProperty("height", src.getHeight());
|
||||
obj.addProperty("javaDir", src.getJavaDir());
|
||||
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
|
||||
obj.addProperty("postExitCommand", src.getPostExitCommand());
|
||||
obj.addProperty("serverIp", src.getServerIp());
|
||||
obj.addProperty("java", src.getJava());
|
||||
obj.addProperty("wrapper", src.getWrapper());
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
@@ -29,19 +32,14 @@ import javafx.stage.StageStyle;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseModManager;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
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.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
|
||||
import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
@@ -49,9 +47,7 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
@@ -64,6 +60,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -86,13 +83,6 @@ public final class Controllers {
|
||||
});
|
||||
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
|
||||
private static DecoratorController decorator;
|
||||
private static Lazy<DownloadListPage> modDownloadListPage = new Lazy<>(() -> {
|
||||
return new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
|
||||
{
|
||||
state.set(State.fromTitle(i18n("modpack.download")));
|
||||
}
|
||||
};
|
||||
});
|
||||
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
@@ -130,11 +120,6 @@ public final class Controllers {
|
||||
return rootPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static DownloadListPage getModpackDownloadListPage() {
|
||||
return modDownloadListPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static MultiplayerPage getMultiplayerPage() {
|
||||
return multiplayerPage.get();
|
||||
@@ -195,12 +180,33 @@ public final class Controllers {
|
||||
stage.setMinWidth(800 + 2 + 16); // bg width + border width*2 + shadow width*2
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
|
||||
scene.getStylesheets().setAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
|
||||
stage.getIcons().add(newImage("/assets/img/icon.png"));
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
stage.initStyle(StageStyle.TRANSPARENT);
|
||||
stage.setScene(scene);
|
||||
|
||||
if (globalConfig().getAgreementVersion() < 1) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
agreementPane.setBody(new Label(i18n("launcher.agreement.hint")));
|
||||
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
|
||||
agreementLink.setOnAction(e -> FXUtils.openLink(Metadata.EULA_URL));
|
||||
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
yesButton.setOnAction(e -> {
|
||||
globalConfig().setAgreementVersion(1);
|
||||
agreementPane.fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
noButton.setOnAction(e -> {
|
||||
System.exit(1);
|
||||
});
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
@@ -213,7 +219,7 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title) {
|
||||
dialog(text, title, MessageType.INFORMATION);
|
||||
dialog(text, title, MessageType.INFO);
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title, MessageType type) {
|
||||
@@ -221,7 +227,7 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void dialog(String text, String title, MessageType type, Runnable ok) {
|
||||
dialog(MessageDialogPane.ok(text, title, type, ok));
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).ok(ok).build());
|
||||
}
|
||||
|
||||
public static void confirm(String text, String title, Runnable yes, Runnable no) {
|
||||
@@ -229,15 +235,15 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void confirm(String text, String title, MessageType type, Runnable yes, Runnable no) {
|
||||
dialog(MessageDialogPane.yesOrNo(text, title, type, yes, no));
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).yesOrNo(yes, no).build());
|
||||
}
|
||||
|
||||
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton) {
|
||||
dialog(MessageDialogPane.actionOrCancel(text, title, type, actionButton, null));
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, null).build());
|
||||
}
|
||||
|
||||
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
|
||||
dialog(MessageDialogPane.actionOrCancel(text, title, type, actionButton, cancel));
|
||||
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
|
||||
@@ -268,14 +274,35 @@ public final class Controllers {
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, Consumer<Region> onCancel) {
|
||||
TaskExecutor executor = task.executor();
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
|
||||
pane.setTitle(title);
|
||||
pane.setExecutor(executor);
|
||||
dialog(pane);
|
||||
executor.start();
|
||||
return pane;
|
||||
}
|
||||
|
||||
public static void navigate(Node node) {
|
||||
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
|
||||
decorator.navigate(node);
|
||||
}
|
||||
|
||||
public static void showToast(String content) {
|
||||
decorator.showToast(content);
|
||||
}
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
if (href.startsWith("hmcl://")) {
|
||||
if ("hmcl://settings/feedback".equals(href)) {
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
}
|
||||
} else {
|
||||
FXUtils.openLink(href);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isStopped() {
|
||||
return decorator == null;
|
||||
}
|
||||
@@ -285,7 +312,6 @@ public final class Controllers {
|
||||
versionPage = null;
|
||||
gameListPage = null;
|
||||
settingsPage = null;
|
||||
modDownloadListPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
|
||||
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -35,11 +33,23 @@ public final class DialogController {
|
||||
}
|
||||
|
||||
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
if (account instanceof ClassicAccount) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
AccountLoginPane pane = new AccountLoginPane(account, it -> {
|
||||
ClassicAccountLoginDialog pane = new ClassicAccountLoginDialog((ClassicAccount) account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
Controllers.dialog(pane);
|
||||
});
|
||||
latch.await();
|
||||
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
|
||||
} else if (account instanceof OAuthAccount) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
|
||||
runInFX(() -> {
|
||||
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
|
||||
res.set(it);
|
||||
latch.countDown();
|
||||
}, latch::countDown);
|
||||
|
||||
@@ -39,13 +39,18 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -53,9 +58,17 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
@@ -63,6 +76,8 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -103,9 +118,9 @@ public final class FXUtils {
|
||||
value.addListener((a, b, c) -> consumer.accept(c));
|
||||
}
|
||||
|
||||
public static <T> WeakChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
WeakChangeListener<T> listener = new WeakChangeListener<>((a, b, c) -> consumer.accept(c));
|
||||
value.addListener(listener);
|
||||
public static <T> ChangeListener<T> onWeakChange(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
ChangeListener<T> listener = (a, b, c) -> consumer.accept(c);
|
||||
value.addListener(new WeakChangeListener<>(listener));
|
||||
return listener;
|
||||
}
|
||||
|
||||
@@ -114,7 +129,7 @@ public final class FXUtils {
|
||||
onChange(value, consumer);
|
||||
}
|
||||
|
||||
public static <T> WeakChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
public static <T> ChangeListener<T> onWeakChangeAndOperate(ObservableValue<T> value, Consumer<T> consumer) {
|
||||
consumer.accept(value.getValue());
|
||||
return onWeakChange(value, consumer);
|
||||
}
|
||||
@@ -319,7 +334,7 @@ public final class FXUtils {
|
||||
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 (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
if (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
@@ -621,6 +636,19 @@ public final class FXUtils {
|
||||
};
|
||||
}
|
||||
|
||||
public static ColumnConstraints getColumnFillingWidth() {
|
||||
ColumnConstraints constraint = new ColumnConstraints();
|
||||
constraint.setFillWidth(true);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
public static ColumnConstraints getColumnHgrowing() {
|
||||
ColumnConstraints constraint = new ColumnConstraints();
|
||||
constraint.setFillWidth(true);
|
||||
constraint.setHgrow(Priority.ALWAYS);
|
||||
return constraint;
|
||||
}
|
||||
|
||||
public static final Interpolator SINE = new Interpolator() {
|
||||
@Override
|
||||
protected double curve(double t) {
|
||||
@@ -672,4 +700,50 @@ public final class FXUtils {
|
||||
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(new InputSource(new StringReader("<body>" + segment + "</body>")));
|
||||
Element r = doc.getDocumentElement();
|
||||
|
||||
NodeList children = r.getChildNodes();
|
||||
List<javafx.scene.Node> texts = new ArrayList<>();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
org.w3c.dom.Node node = children.item(i);
|
||||
|
||||
if (node instanceof Element) {
|
||||
Element element = (Element) node;
|
||||
if ("a".equals(element.getTagName())) {
|
||||
String href = element.getAttribute("href");
|
||||
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
|
||||
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
|
||||
texts.add(hyperlink);
|
||||
} else if ("b".equals(element.getTagName())) {
|
||||
Text text = new Text(element.getTextContent());
|
||||
text.getStyleClass().add("bold");
|
||||
texts.add(text);
|
||||
} else if ("br".equals(element.getTagName())) {
|
||||
texts.add(new Text("\n"));
|
||||
} else {
|
||||
throw new IllegalArgumentException("unsupported tag " + element.getTagName());
|
||||
}
|
||||
} else {
|
||||
texts.add(new Text(node.getTextContent()));
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse xml", e);
|
||||
return Collections.singletonList(new Text(segment));
|
||||
}
|
||||
}
|
||||
|
||||
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
|
||||
TextFlow tf = new TextFlow();
|
||||
tf.getChildren().setAll(parseSegment(segment, hyperlinkAction));
|
||||
return tf;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final Version version;
|
||||
private final String memory;
|
||||
private final String java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
|
||||
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
|
||||
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty();
|
||||
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
|
||||
|
||||
private final ManagedProcess managedProcess;
|
||||
private final DefaultGameRepository repository;
|
||||
private final ProcessListener.ExitType exitType;
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final List<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.launchOptions = launchOptions;
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(version);
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
|
||||
|
||||
this.view = new View();
|
||||
|
||||
setScene(new Scene(view, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("game.crash.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
analyzeCrashReport();
|
||||
}
|
||||
|
||||
private void analyzeCrashReport() {
|
||||
loading.set(true);
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
|
||||
Set<String> keywords = Collections.emptySet();
|
||||
String crashReport = null;
|
||||
try {
|
||||
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read crash report", e);
|
||||
}
|
||||
if (crashReport == null) {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
}
|
||||
return pair(
|
||||
CrashReportAnalyzer.anaylze(rawLog),
|
||||
keywords);
|
||||
}).whenCompleteAsync((pair, exception) -> {
|
||||
loading.set(false);
|
||||
|
||||
if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
|
||||
reason.set(i18n("game.crash.reason.unknown"));
|
||||
} else {
|
||||
List<CrashReportAnalyzer.Result> results = pair.getKey();
|
||||
Set<String> keywords = pair.getValue();
|
||||
StringBuilder reasonText = new StringBuilder();
|
||||
for (CrashReportAnalyzer.Result result : results) {
|
||||
switch (result.getRule()) {
|
||||
case TOO_OLD_JAVA:
|
||||
reasonText.append(i18n("game.crash.reason.too_old_java",
|
||||
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))))
|
||||
.append("\n");
|
||||
break;
|
||||
case MOD_RESOLUTION_CONFLICT:
|
||||
case MOD_RESOLUTION_MISSING:
|
||||
case MOD_RESOLUTION_COLLECTION:
|
||||
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("sourcemod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod"))));
|
||||
break;
|
||||
case MOD_RESOLUTION_MISSING_MINECRAFT:
|
||||
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version")));
|
||||
break;
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
reasonText.append(i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
.toArray()))
|
||||
.append("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (results.isEmpty()) {
|
||||
if (!keywords.isEmpty()) {
|
||||
reason.set(i18n("game.crash.reason.stacktrace", String.join(", ", keywords)));
|
||||
} else {
|
||||
reason.set(i18n("game.crash.reason.unknown"));
|
||||
}
|
||||
|
||||
feedbackLabel.setVisible(true);
|
||||
} else {
|
||||
reason.set(reasonText.toString());
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException);
|
||||
}
|
||||
|
||||
private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?<modid>.*?) @ (?<version>.*?)}");
|
||||
|
||||
private String translateFabricModId(String modName) {
|
||||
switch (modName) {
|
||||
case "fabricloader":
|
||||
return "Fabric";
|
||||
case "fabric":
|
||||
return "Fabric API";
|
||||
case "minecraft":
|
||||
return "Minecraft";
|
||||
default:
|
||||
return modName;
|
||||
}
|
||||
}
|
||||
|
||||
private String parseFabricModId(String modName) {
|
||||
Matcher matcher = FABRIC_MOD_ID.matcher(modName);
|
||||
if (matcher.find()) {
|
||||
String modid = matcher.group("modid");
|
||||
String version = matcher.group("version");
|
||||
if ("[*]".equals(version)) {
|
||||
return i18n("game.crash.reason.mod_resolution_mod_version.any", translateFabricModId(modid));
|
||||
} else {
|
||||
return i18n("game.crash.reason.mod_resolution_mod_version", translateFabricModId(modid), version);
|
||||
}
|
||||
}
|
||||
return translateFabricModId(modName);
|
||||
}
|
||||
|
||||
private void showLogWindow() {
|
||||
LogWindow logWindow = new LogWindow();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
|
||||
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
|
||||
logWindow.showNormal();
|
||||
}
|
||||
|
||||
private void exportGameCrashInfo() {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.thenRunAsync(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx())
|
||||
.exceptionally(e -> {
|
||||
LOG.log(Level.WARNING, "Failed to export game crash info", e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
|
||||
View() {
|
||||
setStyle("-fx-background-color: white");
|
||||
|
||||
HBox titlePane = new HBox();
|
||||
{
|
||||
Label title = new Label();
|
||||
HBox.setHgrow(title, Priority.ALWAYS);
|
||||
|
||||
switch (exitType) {
|
||||
case JVM_ERROR:
|
||||
title.setText(i18n("launch.failed.cannot_create_jvm"));
|
||||
break;
|
||||
case APPLICATION_ERROR:
|
||||
title.setText(i18n("launch.failed.exited_abnormally"));
|
||||
break;
|
||||
}
|
||||
|
||||
titlePane.setAlignment(Pos.CENTER);
|
||||
titlePane.getStyleClass().addAll("jfx-tool-bar-second", "depth-1", "padding-8");
|
||||
titlePane.getChildren().setAll(title);
|
||||
}
|
||||
|
||||
HBox infoPane = new HBox(8);
|
||||
{
|
||||
infoPane.setPadding(new Insets(8));
|
||||
infoPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
TwoLineListItem version = new TwoLineListItem();
|
||||
version.getStyleClass().setAll("two-line-item-second-large");
|
||||
version.setTitle(i18n("archive.game_version"));
|
||||
version.setSubtitle(GameCrashWindow.this.version.getId());
|
||||
|
||||
TwoLineListItem memory = new TwoLineListItem();
|
||||
memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
memory.setTitle(i18n("settings.memory"));
|
||||
memory.setSubtitle(GameCrashWindow.this.memory);
|
||||
|
||||
TwoLineListItem java = new TwoLineListItem();
|
||||
java.getStyleClass().setAll("two-line-item-second-large");
|
||||
java.setTitle("Java");
|
||||
java.setSubtitle(GameCrashWindow.this.java);
|
||||
|
||||
TwoLineListItem os = new TwoLineListItem();
|
||||
os.getStyleClass().setAll("two-line-item-second-large");
|
||||
os.setTitle(i18n("system.operating_system"));
|
||||
os.subtitleProperty().bind(GameCrashWindow.this.os);
|
||||
|
||||
TwoLineListItem arch = new TwoLineListItem();
|
||||
arch.getStyleClass().setAll("two-line-item-second-large");
|
||||
arch.setTitle(i18n("system.architecture"));
|
||||
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
|
||||
|
||||
infoPane.getChildren().setAll(version, memory, java, os, arch);
|
||||
}
|
||||
|
||||
HBox moddedPane = new HBox(8);
|
||||
{
|
||||
moddedPane.setPadding(new Insets(8));
|
||||
moddedPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
analyzer.getVersion(type).ifPresent(ver -> {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.getStyleClass().setAll("two-line-item-second-large");
|
||||
item.setTitle(i18n("install.installer." + type.getPatchId()));
|
||||
item.setSubtitle(ver);
|
||||
moddedPane.getChildren().add(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
VBox gameDirPane = new VBox(8);
|
||||
{
|
||||
TwoLineListItem gameDir = new TwoLineListItem();
|
||||
gameDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
gameDir.setTitle(i18n("game.directory"));
|
||||
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
|
||||
|
||||
TwoLineListItem javaDir = new TwoLineListItem();
|
||||
javaDir.getStyleClass().setAll("two-line-item-second-large");
|
||||
javaDir.setTitle(i18n("settings.game.java_directory"));
|
||||
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
|
||||
|
||||
TwoLineListItem reason = new TwoLineListItem();
|
||||
reason.getStyleClass().setAll("two-line-item-second-large", "wrap-text");
|
||||
reason.setTitle(i18n("game.crash.reason"));
|
||||
reason.subtitleProperty().bind(GameCrashWindow.this.reason);
|
||||
|
||||
gameDirPane.setPadding(new Insets(8));
|
||||
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, reason);
|
||||
}
|
||||
|
||||
HBox toolBar = new HBox();
|
||||
{
|
||||
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
exportGameCrashInfoButton.getStyleClass().add("jfx-button-raised");
|
||||
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
|
||||
|
||||
JFXButton logButton = new JFXButton(i18n("logwindow.title"));
|
||||
logButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
logButton.getStyleClass().add("jfx-button-raised");
|
||||
logButton.setOnMouseClicked(e -> showLogWindow());
|
||||
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, feedbackLabel);
|
||||
}
|
||||
|
||||
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
@@ -37,6 +37,7 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
@@ -153,24 +154,29 @@ public class InstallerItem extends Control {
|
||||
super(control);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
getChildren().setAll(hbox);
|
||||
JFXDepthManager.setDepth(hbox, 1);
|
||||
|
||||
hbox.getStyleClass().add("card");
|
||||
hbox.getStyleClass().add("md-list-cell");
|
||||
hbox.setPadding(new Insets(8));
|
||||
RipplerContainer container = new RipplerContainer(hbox);
|
||||
getChildren().setAll(container);
|
||||
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
if (control.imageUrl != null) {
|
||||
hbox.getChildren().add(FXUtils.limitingSize(new ImageView(new Image(control.imageUrl, 32, 32, true, true)), 32, 32));
|
||||
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
|
||||
Node node = FXUtils.limitingSize(view, 32, 32);
|
||||
node.setMouseTransparent(true);
|
||||
hbox.getChildren().add(node);
|
||||
}
|
||||
|
||||
Label nameLabel = new Label();
|
||||
nameLabel.setMouseTransparent(true);
|
||||
hbox.getChildren().add(nameLabel);
|
||||
nameLabel.setPrefWidth(80);
|
||||
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
|
||||
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
|
||||
|
||||
Label label = new Label();
|
||||
label.setMouseTransparent(true);
|
||||
hbox.getChildren().add(label);
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(label, Priority.ALWAYS);
|
||||
@@ -213,11 +219,11 @@ public class InstallerItem extends Control {
|
||||
|
||||
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
|
||||
if (clickable) {
|
||||
hbox.onMouseClickedProperty().bind(control.action);
|
||||
container.onMouseClickedProperty().bind(control.action);
|
||||
hbox.setCursor(Cursor.HAND);
|
||||
} else {
|
||||
hbox.onMouseClickedProperty().unbind();
|
||||
hbox.onMouseClickedProperty().set(null);
|
||||
container.onMouseClickedProperty().unbind();
|
||||
container.onMouseClickedProperty().set(null);
|
||||
hbox.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,17 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.Control;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.construct.SpinnerPane.FAILED_ACTION;
|
||||
|
||||
public class ListPageBase<T> extends Control {
|
||||
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
|
||||
private final StringProperty failedReason = new SimpleStringProperty(this, "failed");
|
||||
|
||||
public ObservableList<T> getItems() {
|
||||
return items.get();
|
||||
@@ -52,4 +54,35 @@ public class ListPageBase<T> extends Control {
|
||||
public BooleanProperty loadingProperty() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
public String getFailedReason() {
|
||||
return failedReason.get();
|
||||
}
|
||||
|
||||
public StringProperty failedReasonProperty() {
|
||||
return failedReason;
|
||||
}
|
||||
|
||||
public void setFailedReason(String failedReason) {
|
||||
this.failedReason.set(failedReason);
|
||||
}
|
||||
|
||||
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
|
||||
return onFailedAction;
|
||||
}
|
||||
|
||||
public final void setOnFailedAction(EventHandler<Event> value) {
|
||||
onFailedActionProperty().set(value);
|
||||
}
|
||||
|
||||
public final EventHandler<Event> getOnFailedAction() {
|
||||
return onFailedActionProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(FAILED_ACTION, get());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -83,7 +83,7 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
};
|
||||
private final LogWindowImpl impl = new LogWindowImpl();
|
||||
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||
|
||||
private Consumer<String> exportGameCrashInfoCallback;
|
||||
|
||||
@@ -91,7 +91,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
public LogWindow() {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("logwindow.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
|
||||
@@ -73,6 +73,12 @@ public final class SVG {
|
||||
height);
|
||||
}
|
||||
|
||||
public static Node cancel(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M12 2C17.5 2 22 6.5 22 12S17.5 22 12 22 2 17.5 2 12 6.5 2 12 2M12 4C10.1 4 8.4 4.6 7.1 5.7L18.3 16.9C19.3 15.5 20 13.8 20 12C20 7.6 16.4 4 12 4M16.9 18.3L5.7 7.1C4.6 8.4 4 10.1 4 12C4 16.4 7.6 20 12 20C13.9 20 15.6 19.4 16.9 18.3Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",
|
||||
@@ -216,6 +222,10 @@ public final class SVG {
|
||||
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node alertOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16", fill, width, height);
|
||||
}
|
||||
|
||||
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
|
||||
}
|
||||
@@ -486,4 +496,46 @@ public final class SVG {
|
||||
"M7,10L12,15L17,10H7Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node restore(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node bug(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node discord(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node lan(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node thumbUpOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node thumbDownOutline(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
|
||||
public static Node selectAll(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath(
|
||||
"M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z",
|
||||
fill, width, height);
|
||||
}
|
||||
}
|
||||
@@ -19,17 +19,18 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
import java.util.List;
|
||||
@@ -40,43 +41,39 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
|
||||
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
|
||||
spinnerPane.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
ComponentList root = new ComponentList();
|
||||
root.getStyleClass().add("no-padding");
|
||||
StackPane.setMargin(root, new Insets(10));
|
||||
|
||||
List<Node> toolbarButtons = initializeToolbar(skinnable);
|
||||
if (!toolbarButtons.isEmpty()) {
|
||||
HBox toolbar = new HBox();
|
||||
toolbar.getStyleClass().add("jfx-tool-bar-second");
|
||||
JFXDepthManager.setDepth(toolbar, 1);
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setPickOnBounds(false);
|
||||
toolbar.getChildren().setAll(toolbarButtons);
|
||||
root.setTop(toolbar);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
ComponentList.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
|
||||
VBox content = new VBox();
|
||||
content.setSpacing(10);
|
||||
content.setPadding(new Insets(10));
|
||||
|
||||
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
|
||||
root.setCenter(scrollPane);
|
||||
root.getContent().add(scrollPane);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.loadingProperty(), loading -> {
|
||||
if (loading) {
|
||||
spinnerPane.showSpinner();
|
||||
} else {
|
||||
spinnerPane.hideSpinner();
|
||||
}
|
||||
});
|
||||
spinnerPane.setContent(root);
|
||||
|
||||
getChildren().setAll(spinnerPane);
|
||||
|
||||
@@ -24,28 +24,28 @@ import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class UpgradeDialog extends JFXDialogLayout {
|
||||
public UpgradeDialog(Runnable updateRunnable) {
|
||||
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
|
||||
{
|
||||
setHeading(new Label(i18n("update.changelog")));
|
||||
}
|
||||
|
||||
{
|
||||
String url = CHANGELOG_URL + config().getUpdateChannel().channelName + ".html";
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
try {
|
||||
WebEngine engine = webView.getEngine();
|
||||
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
|
||||
engine.load(CHANGELOG_URL + remoteVersion.getChannel().channelName);
|
||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
||||
String viewURL = engine.getLoadWorker().getMessage().trim();
|
||||
if (!viewURL.startsWith(CHANGELOG_URL)) {
|
||||
|
||||
@@ -42,7 +42,7 @@ public class WebStage extends Stage {
|
||||
|
||||
public WebStage(int width, int height) {
|
||||
setScene(new Scene(pane, width, height));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
webView.setContextMenuEnabled(false);
|
||||
|
||||
@@ -20,7 +20,10 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
@@ -46,6 +49,7 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -54,8 +58,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -135,6 +137,8 @@ public class AccountListItem extends RadioButton {
|
||||
} else {
|
||||
return createBooleanBinding(() -> true);
|
||||
}
|
||||
} else if (account instanceof OfflineAccount) {
|
||||
return createBooleanBinding(() -> true);
|
||||
} else {
|
||||
return createBooleanBinding(() -> false);
|
||||
}
|
||||
@@ -145,6 +149,10 @@ public class AccountListItem extends RadioButton {
|
||||
*/
|
||||
@Nullable
|
||||
public Task<?> uploadSkin() {
|
||||
if (account instanceof OfflineAccount) {
|
||||
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
|
||||
return null;
|
||||
}
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
@@ -37,29 +36,30 @@ import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
public class AccountListPage extends ListPageBase<AccountListItem> implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage"), -1));
|
||||
public class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private final ObservableList<AccountListItem> items;
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
|
||||
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
|
||||
private final ListProperty<AuthlibInjectorServer> authServers = new SimpleListProperty<>(this, "authServers", FXCollections.observableArrayList());
|
||||
private final ObjectProperty<Account> selectedAccount;
|
||||
|
||||
public AccountListPage() {
|
||||
setItems(MappedObservableList.create(accounts, AccountListItem::new));
|
||||
selectedAccount = createSelectedItemPropertyFor(getItems(), Account.class);
|
||||
items = MappedObservableList.create(accounts, AccountListItem::new);
|
||||
selectedAccount = createSelectedItemPropertyFor(items, Account.class);
|
||||
}
|
||||
|
||||
public ObjectProperty<Account> selectedAccountProperty() {
|
||||
@@ -84,100 +84,87 @@ public class AccountListPage extends ListPageBase<AccountListItem> implements De
|
||||
return new AccountListPageSkin(this);
|
||||
}
|
||||
|
||||
private static class AccountListPageSkin extends SkinBase<AccountListPage> {
|
||||
private static class AccountListPageSkin extends DecoratorAnimatedPageSkin<AccountListPage> {
|
||||
|
||||
private final ObservableList<AdvancedListItem> authServerItems;
|
||||
|
||||
public AccountListPageSkin(AccountListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
{
|
||||
BorderPane left = new BorderPane();
|
||||
FXUtils.setLimitWidth(left, 200);
|
||||
|
||||
VBox boxMethods = new VBox();
|
||||
{
|
||||
VBox boxItemList = new VBox();
|
||||
boxItemList.getStyleClass().add("advanced-list-box-content");
|
||||
boxMethods.getStyleClass().add("advanced-list-box-content");
|
||||
boxMethods.getChildren().add(new ClassTitle(i18n("account.create")));
|
||||
FXUtils.setLimitWidth(boxMethods, 200);
|
||||
|
||||
boxItemList.getChildren().add(new ClassTitle(i18n("account.create")));
|
||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG::account));
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
boxMethods.getChildren().add(offlineItem);
|
||||
|
||||
{
|
||||
VBox boxMethods = new VBox();
|
||||
FXUtils.setLimitWidth(boxMethods, 200);
|
||||
AdvancedListItem mojangItem = new AdvancedListItem();
|
||||
mojangItem.getStyleClass().add("navigation-drawer-item");
|
||||
mojangItem.setActionButtonVisible(false);
|
||||
mojangItem.setTitle(i18n("account.methods.yggdrasil"));
|
||||
mojangItem.setLeftGraphic(wrap(SVG::mojang));
|
||||
mojangItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
|
||||
boxMethods.getChildren().add(mojangItem);
|
||||
|
||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG.account(Theme.blackFillBinding(), 24, 24)));
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
boxMethods.getChildren().add(offlineItem);
|
||||
AdvancedListItem microsoftItem = new AdvancedListItem();
|
||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||
microsoftItem.setActionButtonVisible(false);
|
||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG::microsoft));
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
boxMethods.getChildren().add(microsoftItem);
|
||||
|
||||
AdvancedListItem mojangItem = new AdvancedListItem();
|
||||
mojangItem.getStyleClass().add("navigation-drawer-item");
|
||||
mojangItem.setActionButtonVisible(false);
|
||||
mojangItem.setTitle(i18n("account.methods.yggdrasil"));
|
||||
mojangItem.setLeftGraphic(wrap(SVG.mojang(Theme.blackFillBinding(), 24, 24)));
|
||||
mojangItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MOJANG)));
|
||||
boxMethods.getChildren().add(mojangItem);
|
||||
VBox boxAuthServers = new VBox();
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setLeftGraphic(wrap(SVG::server));
|
||||
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
|
||||
|
||||
AdvancedListItem microsoftItem = new AdvancedListItem();
|
||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||
microsoftItem.setActionButtonVisible(false);
|
||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG.microsoft(Theme.blackFillBinding(), 24, 24)));
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
boxMethods.getChildren().add(microsoftItem);
|
||||
|
||||
VBox boxAuthServers = new VBox();
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
item.getStyleClass().add("navigation-drawer-item");
|
||||
item.setLeftGraphic(wrap(SVG.server(Theme.blackFillBinding(), 24, 24)));
|
||||
item.setOnAction(e -> Controllers.dialog(new CreateAccountPane(server)));
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnAction(e -> {
|
||||
skinnable.authServersProperty().remove(server);
|
||||
e.consume();
|
||||
});
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
|
||||
item.setRightGraphic(btnRemove);
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
|
||||
FXUtils.installFastTooltip(item, tooltip);
|
||||
|
||||
return item;
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnAction(e -> {
|
||||
skinnable.authServersProperty().remove(server);
|
||||
e.consume();
|
||||
});
|
||||
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
|
||||
boxMethods.getChildren().add(boxAuthServers);
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
btnRemove.setGraphic(SVG.close(Theme.blackFillBinding(), 14, 14));
|
||||
item.setRightGraphic(btnRemove);
|
||||
|
||||
boxItemList.getChildren().add(new ScrollPane(boxMethods));
|
||||
}
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
|
||||
FXUtils.installFastTooltip(item, tooltip);
|
||||
|
||||
left.setCenter(boxItemList);
|
||||
return item;
|
||||
});
|
||||
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
|
||||
boxMethods.getChildren().add(boxAuthServers);
|
||||
}
|
||||
|
||||
AdvancedListItem addAuthServerItem = new AdvancedListItem();
|
||||
{
|
||||
AdvancedListItem addAuthServerItem = new AdvancedListItem();
|
||||
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
|
||||
addAuthServerItem.setTitle(i18n("account.injector.add"));
|
||||
addAuthServerItem.setSubtitle(i18n("account.methods.authlib_injector"));
|
||||
addAuthServerItem.setActionButtonVisible(false);
|
||||
addAuthServerItem.setLeftGraphic(wrap(SVG.plusCircleOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
addAuthServerItem.setLeftGraphic(wrap(SVG::plusCircleOutline));
|
||||
addAuthServerItem.setOnAction(e -> Controllers.dialog(new AddAuthlibInjectorServerPane()));
|
||||
BorderPane.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
|
||||
left.setBottom(addAuthServerItem);
|
||||
VBox.setMargin(addAuthServerItem, new Insets(0, 0, 12, 0));
|
||||
}
|
||||
|
||||
root.setLeft(left);
|
||||
ScrollPane scrollPane = new ScrollPane(boxMethods);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
setLeft(scrollPane, addAuthServerItem);
|
||||
}
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
@@ -189,15 +176,13 @@ public class AccountListPage extends ListPageBase<AccountListItem> implements De
|
||||
list.setSpacing(10);
|
||||
list.getStyleClass().add("card-list");
|
||||
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
Bindings.bindContent(list.getChildren(), skinnable.items);
|
||||
|
||||
scrollPane.setContent(list);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
|
||||
root.setCenter(scrollPane);
|
||||
setCenter(scrollPane);
|
||||
}
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +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.account;
|
||||
|
||||
import com.jfoenix.controls.JFXPasswordField;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class AccountLoginPane extends StackPane {
|
||||
private final Account oldAccount;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
|
||||
@FXML private Label lblUsername;
|
||||
@FXML private JFXPasswordField txtPassword;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private JFXProgressBar progressBar;
|
||||
|
||||
public AccountLoginPane(Account oldAccount, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.oldAccount = oldAccount;
|
||||
this.success = success;
|
||||
this.failed = failed;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/account-login.fxml");
|
||||
|
||||
lblUsername.setText(oldAccount.getUsername());
|
||||
txtPassword.setOnAction(e -> onAccept());
|
||||
|
||||
onEscPressed(this, this::onCancel);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAccept() {
|
||||
String password = txtPassword.getText();
|
||||
progressBar.setVisible(true);
|
||||
lblCreationWarning.setText("");
|
||||
Task.supplyAsync(() -> oldAccount.logInWithPassword(password))
|
||||
.whenComplete(Schedulers.javafx(), authInfo -> {
|
||||
success.accept(authInfo);
|
||||
fireEvent(new DialogCloseEvent());
|
||||
progressBar.setVisible(false);
|
||||
}, e -> {
|
||||
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
|
||||
if (e instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblCreationWarning.setText(Accounts.localizeErrorMessage(e));
|
||||
}
|
||||
progressBar.setVisible(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onCancel() {
|
||||
failed.run();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXPasswordField;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.ClassicAccount;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ClassicAccountLoginDialog extends StackPane {
|
||||
private final ClassicAccount oldAccount;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
|
||||
private final JFXPasswordField txtPassword;
|
||||
private final Label lblCreationWarning = new Label();
|
||||
private final JFXProgressBar progressBar;
|
||||
|
||||
public ClassicAccountLoginDialog(ClassicAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.oldAccount = oldAccount;
|
||||
this.success = success;
|
||||
this.failed = failed;
|
||||
|
||||
progressBar = new JFXProgressBar();
|
||||
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
|
||||
progressBar.setVisible(false);
|
||||
|
||||
JFXDialogLayout dialogLayout = new JFXDialogLayout();
|
||||
|
||||
{
|
||||
dialogLayout.setHeading(new Label(i18n("login.enter_password")));
|
||||
}
|
||||
|
||||
{
|
||||
VBox body = new VBox(15);
|
||||
body.setPadding(new Insets(15, 0, 0, 0));
|
||||
|
||||
Label usernameLabel = new Label(oldAccount.getUsername());
|
||||
|
||||
txtPassword = new JFXPasswordField();
|
||||
txtPassword.setOnAction(e -> onAccept());
|
||||
txtPassword.getValidators().add(new RequiredValidator());
|
||||
txtPassword.setLabelFloat(true);
|
||||
txtPassword.setPromptText(i18n("account.password"));
|
||||
|
||||
body.getChildren().setAll(usernameLabel, txtPassword);
|
||||
|
||||
if (oldAccount instanceof YggdrasilAccount && !(oldAccount instanceof AuthlibInjectorAccount)) {
|
||||
HBox linkPane = new HBox(8);
|
||||
body.getChildren().add(linkPane);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
|
||||
|
||||
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
|
||||
}
|
||||
|
||||
dialogLayout.setBody(body);
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.setOnAction(e -> onAccept());
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> onCancel());
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
|
||||
dialogLayout.setActions(lblCreationWarning, acceptButton, cancelButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(dialogLayout);
|
||||
|
||||
onEscPressed(this, this::onCancel);
|
||||
}
|
||||
|
||||
private void onAccept() {
|
||||
String password = txtPassword.getText();
|
||||
progressBar.setVisible(true);
|
||||
lblCreationWarning.setText("");
|
||||
Task.supplyAsync(() -> oldAccount.logInWithPassword(password))
|
||||
.whenComplete(Schedulers.javafx(), authInfo -> {
|
||||
success.accept(authInfo);
|
||||
fireEvent(new DialogCloseEvent());
|
||||
progressBar.setVisible(false);
|
||||
}, e -> {
|
||||
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
|
||||
if (e instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblCreationWarning.setText(Accounts.localizeErrorMessage(e));
|
||||
}
|
||||
progressBar.setVisible(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
failed.run();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -43,7 +45,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -53,6 +55,7 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
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.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
@@ -62,6 +65,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
@@ -87,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private final Pane detailsContainer;
|
||||
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
@@ -216,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
@@ -260,23 +267,33 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
VBox vbox = new VBox(8);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
|
||||
runInFX(() -> deviceCode.set(value));
|
||||
}));
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
|
||||
box.getChildren().setAll(profileLink, birthLink);
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
vbox.getChildren().setAll(hintPane, box);
|
||||
@@ -415,17 +432,32 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
|
||||
if (factory instanceof YggdrasilAccountFactory) {
|
||||
HBox box = new HBox();
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/en-us/articles/360050865492-JAVA-Account-Migration-FAQ"));
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
box.getChildren().setAll(migrationLink);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
|
||||
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
|
||||
box.getChildren().setAll(migrationLink, migrationHowLink, purchaseLink);
|
||||
add(box, 0, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
if (factory instanceof OfflineAccountFactory) {
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
HBox linkPane = new HBox(purchaseLink);
|
||||
GridPane.setColumnSpan(linkPane, 2);
|
||||
add(linkPane, 0, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
|
||||
HBox box = new HBox();
|
||||
MenuUpDownButton advancedButton = new MenuUpDownButton();
|
||||
box.getChildren().setAll(advancedButton);
|
||||
@@ -504,7 +536,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
if (factory instanceof AuthlibInjectorAccountFactory) {
|
||||
return getAuthServer();
|
||||
} else if (factory instanceof OfflineAccountFactory) {
|
||||
return txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
|
||||
UUID uuid = txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
|
||||
return new OfflineAccountFactory.AdditionalData(uuid, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.OAuthAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class OAuthAccountLoginDialog extends DialogPane {
|
||||
private final OAuthAccount account;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.account = account;
|
||||
this.success = success;
|
||||
this.failed = failed;
|
||||
|
||||
setTitle(i18n("account.login.refresh"));
|
||||
|
||||
VBox vbox = new VBox(8);
|
||||
Label usernameLabel = new Label(account.getUsername());
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
|
||||
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
|
||||
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
vbox.getChildren().setAll(usernameLabel, hintPane, box);
|
||||
setBody(vbox);
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
|
||||
}
|
||||
|
||||
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
|
||||
FXUtils.runInFX(() -> {
|
||||
deviceCode.set(event);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
Task.supplyAsync(account::logInWhenCredentialsExpired)
|
||||
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
|
||||
if (exception == null) {
|
||||
success.accept(authInfo);
|
||||
onSuccess();
|
||||
} else {
|
||||
LOG.log(Level.INFO, "Failed to login when credentials expired: " + account, exception);
|
||||
onFailure(Accounts.localizeErrorMessage(exception));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancel() {
|
||||
failed.run();
|
||||
super.onCancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class OfflineAccountSkinPane extends StackPane {
|
||||
private final OfflineAccount account;
|
||||
|
||||
private final MultiFileItem<Skin.Type> skinItem = new MultiFileItem<>();
|
||||
private final JFXTextField cslApiField = new JFXTextField();
|
||||
private final JFXComboBox<TextureModel> modelCombobox = new JFXComboBox<>();
|
||||
private final FileSelector skinSelector = new FileSelector();
|
||||
private final FileSelector capeSelector = new FileSelector();
|
||||
|
||||
private final InvalidationListener skinBinding;
|
||||
|
||||
public OfflineAccountSkinPane(OfflineAccount account) {
|
||||
this.account = account;
|
||||
|
||||
getStyleClass().add("skin-pane");
|
||||
|
||||
JFXDialogLayout layout = new JFXDialogLayout();
|
||||
getChildren().setAll(layout);
|
||||
layout.setHeading(new Label(i18n("account.skin")));
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 300, 300, true);
|
||||
StackPane canvasPane = new StackPane(canvas);
|
||||
canvasPane.setPrefWidth(300);
|
||||
canvasPane.setPrefHeight(300);
|
||||
pane.setCenter(canvas);
|
||||
canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
|
||||
canvas.enableRotation(.5);
|
||||
|
||||
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
|
||||
if (e.getDragboard().hasFiles()) {
|
||||
File file = e.getDragboard().getFiles().get(0);
|
||||
if (file.getAbsolutePath().endsWith(".png"))
|
||||
e.acceptTransferModes(TransferMode.COPY);
|
||||
}
|
||||
});
|
||||
canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> {
|
||||
if (e.isAccepted()) {
|
||||
File skin = e.getDragboard().getFiles().get(0);
|
||||
Platform.runLater(() -> {
|
||||
skinSelector.setValue(skin.getAbsolutePath());
|
||||
skinItem.setSelectedData(Skin.Type.LOCAL_FILE);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
StackPane skinOptionPane = new StackPane();
|
||||
skinOptionPane.setMaxWidth(300);
|
||||
VBox optionPane = new VBox(skinItem, skinOptionPane);
|
||||
pane.setRight(optionPane);
|
||||
|
||||
layout.setBody(pane);
|
||||
|
||||
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
|
||||
cslApiField.setValidators(new URLValidator());
|
||||
|
||||
skinItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
|
||||
new MultiFileItem.Option<>("Steve", Skin.Type.STEVE),
|
||||
new MultiFileItem.Option<>("Alex", Skin.Type.ALEX),
|
||||
new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE),
|
||||
new MultiFileItem.Option<>("LittleSkin", Skin.Type.LITTLE_SKIN),
|
||||
new MultiFileItem.Option<>(i18n("account.skin.type.csl_api"), Skin.Type.CUSTOM_SKIN_LOADER_API)
|
||||
));
|
||||
|
||||
modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName)));
|
||||
modelCombobox.getItems().setAll(TextureModel.STEVE, TextureModel.ALEX);
|
||||
|
||||
if (account.getSkin() == null) {
|
||||
skinItem.setSelectedData(Skin.Type.DEFAULT);
|
||||
modelCombobox.setValue(TextureModel.STEVE);
|
||||
} else {
|
||||
skinItem.setSelectedData(account.getSkin().getType());
|
||||
cslApiField.setText(account.getSkin().getCslApi());
|
||||
modelCombobox.setValue(account.getSkin().getTextureModel());
|
||||
skinSelector.setValue(account.getSkin().getLocalSkinPath());
|
||||
capeSelector.setValue(account.getSkin().getLocalCapePath());
|
||||
}
|
||||
|
||||
skinBinding = FXUtils.observeWeak(() -> {
|
||||
getSkin().load(account.getUsername())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to load skin", exception);
|
||||
Controllers.showToast(i18n("message.failed"));
|
||||
} else {
|
||||
if (result == null || result.getSkin() == null) {
|
||||
canvas.updateSkin(getDefaultTexture(), isDefaultSlim());
|
||||
return;
|
||||
}
|
||||
canvas.updateSkin(new Image(result.getSkin().getInputStream()), result.getModel() == TextureModel.ALEX);
|
||||
}
|
||||
}).start();
|
||||
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(0, 0, 0, 10));
|
||||
gridPane.setHgap(16);
|
||||
gridPane.setVgap(8);
|
||||
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
|
||||
switch (selectedData) {
|
||||
case DEFAULT:
|
||||
case STEVE:
|
||||
case ALEX:
|
||||
break;
|
||||
case LITTLE_SKIN:
|
||||
HintPane hint = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hint.setText(i18n("account.skin.type.little_skin.hint"));
|
||||
gridPane.addRow(0, hint);
|
||||
break;
|
||||
case LOCAL_FILE:
|
||||
gridPane.addRow(0, new Label(i18n("account.skin.model")), modelCombobox);
|
||||
gridPane.addRow(1, new Label(i18n("account.skin")), skinSelector);
|
||||
gridPane.addRow(2, new Label(i18n("account.cape")), capeSelector);
|
||||
break;
|
||||
case CUSTOM_SKIN_LOADER_API:
|
||||
gridPane.addRow(0, new Label(i18n("account.skin.type.csl_api.location")), cslApiField);
|
||||
break;
|
||||
}
|
||||
|
||||
skinOptionPane.getChildren().setAll(gridPane);
|
||||
});
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
acceptButton.setOnAction(e -> {
|
||||
account.setSkin(getSkin());
|
||||
fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
|
||||
JFXHyperlink littleSkinLink = new JFXHyperlink(i18n("account.skin.type.little_skin"));
|
||||
littleSkinLink.setOnAction(e -> FXUtils.openLink("https://mcskin.littleservice.cn/"));
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
layout.setActions(littleSkinLink, acceptButton, cancelButton);
|
||||
}
|
||||
|
||||
private Skin getSkin() {
|
||||
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue());
|
||||
}
|
||||
|
||||
private boolean isDefaultSlim() {
|
||||
return TextureModel.detectUUID(account.getUUID()) == TextureModel.ALEX;
|
||||
}
|
||||
|
||||
private Image getDefaultTexture() {
|
||||
if (isDefaultSlim()) {
|
||||
return SkinCanvas.ALEX;
|
||||
} else {
|
||||
return SkinCanvas.STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
|
||||
updateContent(newView);
|
||||
|
||||
if (previousNode == EMPTY_PANE) {
|
||||
setMouseTransparent(false);
|
||||
getChildren().setAll(newView);
|
||||
return;
|
||||
}
|
||||
|
||||
transition.init(this);
|
||||
|
||||
// runLater or "init" will not work
|
||||
@@ -95,5 +101,8 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
getChildren().setAll(previousNode, currentNode);
|
||||
}
|
||||
|
||||
private final StackPane EMPTY_PANE = new StackPane();
|
||||
private final EmptyPane EMPTY_PANE = new EmptyPane();
|
||||
|
||||
public static class EmptyPane extends StackPane {
|
||||
}
|
||||
}
|
||||
@@ -19,37 +19,26 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class AdvancedListBox extends ScrollPane {
|
||||
private final VBox container = new VBox();
|
||||
public class AdvancedListBox extends VBox {
|
||||
|
||||
{
|
||||
setContent(container);
|
||||
|
||||
FXUtils.smoothScrolling(this);
|
||||
|
||||
setFitToHeight(true);
|
||||
setFitToWidth(true);
|
||||
setHbarPolicy(ScrollBarPolicy.NEVER);
|
||||
|
||||
container.getStyleClass().add("advanced-list-box-content");
|
||||
getStyleClass().add("advanced-list-box-content");
|
||||
}
|
||||
|
||||
public AdvancedListBox add(Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(child);
|
||||
getChildren().add(child);
|
||||
else {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(pane);
|
||||
getChildren().add(pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -64,27 +53,27 @@ public class AdvancedListBox extends ScrollPane {
|
||||
|
||||
public AdvancedListBox add(int index, Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(index, child);
|
||||
getChildren().add(index, child);
|
||||
else {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(index, pane);
|
||||
getChildren().add(index, pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvancedListBox remove(Node child) {
|
||||
container.getChildren().remove(indexOf(child));
|
||||
getChildren().remove(indexOf(child));
|
||||
return this;
|
||||
}
|
||||
|
||||
public int indexOf(Node child) {
|
||||
if (child instanceof Pane) {
|
||||
return container.getChildren().indexOf(child);
|
||||
return getChildren().indexOf(child);
|
||||
} else {
|
||||
for (int i = 0; i < container.getChildren().size(); ++i) {
|
||||
Node node = container.getChildren().get(i);
|
||||
for (int i = 0; i < getChildren().size(); ++i) {
|
||||
Node node = getChildren().get(i);
|
||||
if (node instanceof StackPane) {
|
||||
ObservableList<Node> list = ((StackPane) node).getChildren();
|
||||
if (list.size() == 1 && list.get(0) == child)
|
||||
@@ -99,11 +88,7 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return add(new ClassTitle(category));
|
||||
}
|
||||
|
||||
public void setSpacing(double spacing) {
|
||||
container.setSpacing(spacing);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
container.getChildren().clear();
|
||||
getChildren().clear();
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
|
||||
RipplerContainer container = new RipplerContainer(root);
|
||||
|
||||
HBox left = new HBox();
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
left.setMouseTransparent(true);
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class AnnouncementCard extends VBox {
|
||||
|
||||
public AnnouncementCard(String title, String content) {
|
||||
TextFlow tf = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
|
||||
|
||||
Label label = new Label(title);
|
||||
label.getStyleClass().add("title");
|
||||
getChildren().setAll(label, tf);
|
||||
setSpacing(14);
|
||||
getStyleClass().addAll("card", "announcement");
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ public class ComponentList extends Control {
|
||||
public void onExpand() {
|
||||
if (!expanded && lazyInitializer != null) {
|
||||
lazyInitializer.accept(this);
|
||||
setNeedsLayout(true);
|
||||
}
|
||||
|
||||
expanded = true;
|
||||
@@ -136,6 +137,9 @@ public class ComponentList extends Control {
|
||||
if (node.getProperties().containsKey("ComponentList.vgrow")) {
|
||||
VBox.setVgrow(cell, (Priority) node.getProperties().get("ComponentList.vgrow"));
|
||||
}
|
||||
if (node.getProperties().containsKey("ComponentList.noPadding")) {
|
||||
cell.getStyleClass().add("no-padding");
|
||||
}
|
||||
return cell;
|
||||
});
|
||||
|
||||
|
||||
@@ -22,16 +22,14 @@ import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -78,7 +76,9 @@ class ComponentListCell extends StackPane {
|
||||
content.getStyleClass().remove("options-list");
|
||||
content.getStyleClass().add("options-sublist");
|
||||
|
||||
BorderPane groupNode = new BorderPane();
|
||||
getStyleClass().add("no-padding");
|
||||
|
||||
VBox groupNode = new VBox();
|
||||
|
||||
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 20, 20);
|
||||
JFXButton expandButton = new JFXButton();
|
||||
@@ -86,6 +86,7 @@ class ComponentListCell extends StackPane {
|
||||
expandButton.getStyleClass().add("options-list-item-expand-button");
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
labelVBox.setMouseTransparent(true);
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
boolean overrideHeaderLeft = false;
|
||||
@@ -110,59 +111,72 @@ class ComponentListCell extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
groupNode.setLeft(labelVBox);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setSpacing(16);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
HBox header = new HBox();
|
||||
header.setSpacing(16);
|
||||
header.getChildren().add(labelVBox);
|
||||
header.setPadding(new Insets(10, 16, 10, 16));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(labelVBox, Priority.ALWAYS);
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
||||
if (rightNode != null)
|
||||
right.getChildren().add(rightNode);
|
||||
header.getChildren().add(rightNode);
|
||||
}
|
||||
right.getChildren().add(expandButton);
|
||||
groupNode.setRight(right);
|
||||
header.getChildren().add(expandButton);
|
||||
|
||||
RipplerContainer headerRippler = new RipplerContainer(header);
|
||||
groupNode.getChildren().add(headerRippler);
|
||||
|
||||
VBox container = new VBox();
|
||||
container.setPadding(new Insets(8, 0, 0, 0));
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
container.getChildren().setAll(content);
|
||||
groupNode.setBottom(container);
|
||||
groupNode.getChildren().add(container);
|
||||
|
||||
expandButton.setOnMouseClicked(e -> {
|
||||
Runnable onExpand = () -> {
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
setExpanded(!isExpanded());
|
||||
|
||||
double newAnimatedHeight = content.prefHeight(-1) * (isExpanded() ? 1 : -1);
|
||||
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
|
||||
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
|
||||
|
||||
if (isExpanded()) {
|
||||
updateClip(newHeight);
|
||||
list.onExpand();
|
||||
list.layout();
|
||||
}
|
||||
|
||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
||||
));
|
||||
Platform.runLater(() -> {
|
||||
double newAnimatedHeight = (content.prefHeight(-1) + 8 + 10) * (isExpanded() ? 1 : -1);
|
||||
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
|
||||
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
|
||||
|
||||
if (!isExpanded()) {
|
||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||
}
|
||||
if (isExpanded()) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
|
||||
expandAnimation.play();
|
||||
});
|
||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
||||
));
|
||||
|
||||
if (!isExpanded()) {
|
||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||
}
|
||||
|
||||
expandAnimation.play();
|
||||
});
|
||||
};
|
||||
|
||||
headerRippler.setOnMouseClicked(e -> onExpand.run());
|
||||
expandButton.setOnMouseClicked(e -> onExpand.run());
|
||||
|
||||
expandedProperty().addListener((a, b, newValue) ->
|
||||
expandIcon.setRotate(newValue ? 180 : 0));
|
||||
|
||||
getChildren().setAll(groupNode);
|
||||
} else {
|
||||
getStyleClass().remove("no-padding");
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,7 @@ public interface DialogAware {
|
||||
default void onDialogShown() {
|
||||
}
|
||||
|
||||
default void onDialogClosed() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class DialogPane extends JFXDialogLayout {
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final BooleanProperty valid = new SimpleBooleanProperty();
|
||||
protected final SpinnerPane acceptPane = new SpinnerPane();
|
||||
protected final JFXButton cancelButton = new JFXButton();
|
||||
protected final Label warningLabel = new Label();
|
||||
private final JFXProgressBar progressBar = new JFXProgressBar();
|
||||
|
||||
public DialogPane() {
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textProperty().bind(title);
|
||||
setHeading(titleLabel);
|
||||
getChildren().add(progressBar);
|
||||
|
||||
progressBar.setVisible(false);
|
||||
StackPane.setMargin(progressBar, new Insets(-24.0D, -24.0D, -16.0D, -24.0D));
|
||||
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
|
||||
progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.setOnAction(e -> onAccept());
|
||||
acceptButton.disableProperty().bind(valid.not());
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
acceptPane.getStyleClass().add("small-spinner-pane");
|
||||
acceptPane.setContent(acceptButton);
|
||||
|
||||
cancelButton.setText(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> onCancel());
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
setActions(warningLabel, acceptPane, cancelButton);
|
||||
}
|
||||
|
||||
protected JFXProgressBar getProgressBar() {
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid.get();
|
||||
}
|
||||
|
||||
public BooleanProperty validProperty() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public void setValid(boolean valid) {
|
||||
this.valid.set(valid);
|
||||
}
|
||||
|
||||
protected void onCancel() {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
protected void onAccept() {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
protected void setLoading() {
|
||||
acceptPane.showSpinner();
|
||||
warningLabel.setText("");
|
||||
}
|
||||
|
||||
protected void onSuccess() {
|
||||
acceptPane.hideSpinner();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
protected void onFailure(String msg) {
|
||||
acceptPane.hideSpinner();
|
||||
warningLabel.setText(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class FileSelector extends HBox {
|
||||
private StringProperty value = new SimpleStringProperty();
|
||||
private String chooserTitle = i18n("selector.choose_file");
|
||||
private boolean directory = false;
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
|
||||
public String getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
public StringProperty valueProperty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value.set(value);
|
||||
}
|
||||
|
||||
public String getChooserTitle() {
|
||||
return chooserTitle;
|
||||
}
|
||||
|
||||
public FileSelector setChooserTitle(String chooserTitle) {
|
||||
this.chooserTitle = chooserTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public FileSelector setDirectory(boolean directory) {
|
||||
this.directory = directory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
return extensionFilters;
|
||||
}
|
||||
|
||||
public FileSelector() {
|
||||
JFXTextField customField = new JFXTextField();
|
||||
customField.textProperty().bindBidirectional(valueProperty());
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
|
||||
selectButton.setOnMouseClicked(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
File dir = chooser.showDialog(Controllers.getStage());
|
||||
if (dir != null)
|
||||
customField.setText(dir.getAbsolutePath());
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.setTitle(chooserTitle);
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
customField.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(3);
|
||||
getChildren().addAll(customField, selectButton);
|
||||
}
|
||||
}
|
||||
@@ -21,38 +21,57 @@ import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class HintPane extends VBox {
|
||||
private final Text label = new Text();
|
||||
private final StringProperty text = new SimpleStringProperty(this, "text");
|
||||
private final TextFlow flow = new TextFlow();
|
||||
|
||||
public HintPane() {
|
||||
this(MessageDialogPane.MessageType.INFO);
|
||||
}
|
||||
|
||||
public HintPane(MessageDialogPane.MessageType type) {
|
||||
setFillWidth(true);
|
||||
getStyleClass().add("hint");
|
||||
getStyleClass().addAll("hint", type.name().toLowerCase());
|
||||
HBox hbox = new HBox();
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
hbox.getChildren().setAll(
|
||||
SVG.informationOutline(Theme.blackFillBinding(), 16, 16),
|
||||
new Label(i18n("message.info"))
|
||||
);
|
||||
TextFlow flow = new TextFlow();
|
||||
|
||||
switch (type) {
|
||||
case INFO:
|
||||
hbox.getChildren().add(SVG.informationOutline(Theme.blackFillBinding(), 16, 16));
|
||||
break;
|
||||
case ERROR:
|
||||
hbox.getChildren().add(SVG.closeCircleOutline(Theme.blackFillBinding(), 16, 16));
|
||||
break;
|
||||
case SUCCESS:
|
||||
hbox.getChildren().add(SVG.checkCircleOutline(Theme.blackFillBinding(), 16, 16));
|
||||
break;
|
||||
case WARNING:
|
||||
hbox.getChildren().add(SVG.alertOutline(Theme.blackFillBinding(), 16, 16));
|
||||
break;
|
||||
case QUESTION:
|
||||
hbox.getChildren().add(SVG.helpCircleOutline(Theme.blackFillBinding(), 16, 16));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized message box message type " + type);
|
||||
}
|
||||
|
||||
|
||||
hbox.getChildren().add(new Text(type.getDisplayName()));
|
||||
flow.getChildren().setAll(label);
|
||||
getChildren().setAll(hbox, flow);
|
||||
label.textProperty().bind(text);
|
||||
VBox.setMargin(flow, new Insets(2, 2, 0, 2));
|
||||
|
||||
label.fillProperty().bind(BindingMapping.of(disabledProperty()).map(disabled -> disabled ? new Color(0, 0, 0, 0.5) : Color.BLACK));
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
@@ -66,4 +85,12 @@ public class HintPane extends VBox {
|
||||
public void setText(String text) {
|
||||
this.text.set(text);
|
||||
}
|
||||
|
||||
public void setSegment(String segment) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
|
||||
}
|
||||
|
||||
public void setChildren(Node... children) {
|
||||
flow.getChildren().setAll(children);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class JFXCheckBoxTreeTableCell<S, T> extends TreeTableCell<S, T> {
|
||||
|
||||
private final CheckBox checkBox;
|
||||
private boolean showLabel;
|
||||
private ObservableValue<Boolean> booleanProperty;
|
||||
|
||||
public JFXCheckBoxTreeTableCell() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public JFXCheckBoxTreeTableCell(
|
||||
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty) {
|
||||
this(getSelectedProperty, null);
|
||||
}
|
||||
|
||||
public JFXCheckBoxTreeTableCell(
|
||||
final Callback<Integer, ObservableValue<Boolean>> getSelectedProperty,
|
||||
final StringConverter<T> converter) {
|
||||
this.getStyleClass().add("check-box-tree-table-cell");
|
||||
this.checkBox = new JFXCheckBox();
|
||||
setGraphic(null);
|
||||
setSelectedStateCallback(getSelectedProperty);
|
||||
setConverter(converter);
|
||||
}
|
||||
|
||||
private ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<StringConverter<T>>(this, "converter") {
|
||||
protected void invalidated() {
|
||||
updateShowLabel();
|
||||
}
|
||||
};
|
||||
|
||||
public final ObjectProperty<StringConverter<T>> converterProperty() {
|
||||
return converter;
|
||||
}
|
||||
|
||||
public final void setConverter(StringConverter<T> value) {
|
||||
converterProperty().set(value);
|
||||
}
|
||||
|
||||
public final StringConverter<T> getConverter() {
|
||||
return converterProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<Callback<Integer, ObservableValue<Boolean>>>
|
||||
selectedStateCallback =
|
||||
new SimpleObjectProperty<Callback<Integer, ObservableValue<Boolean>>>(
|
||||
this, "selectedStateCallback");
|
||||
|
||||
public final ObjectProperty<Callback<Integer, ObservableValue<Boolean>>> selectedStateCallbackProperty() {
|
||||
return selectedStateCallback;
|
||||
}
|
||||
|
||||
public final void setSelectedStateCallback(Callback<Integer, ObservableValue<Boolean>> value) {
|
||||
selectedStateCallbackProperty().set(value);
|
||||
}
|
||||
|
||||
public final Callback<Integer, ObservableValue<Boolean>> getSelectedStateCallback() {
|
||||
return selectedStateCallbackProperty().get();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
StringConverter<T> c = getConverter();
|
||||
|
||||
if (showLabel) {
|
||||
setText(c.toString(item));
|
||||
}
|
||||
setGraphic(checkBox);
|
||||
|
||||
if (booleanProperty instanceof BooleanProperty) {
|
||||
checkBox.selectedProperty().unbindBidirectional((BooleanProperty) booleanProperty);
|
||||
}
|
||||
ObservableValue<?> obsValue = getSelectedProperty();
|
||||
if (obsValue instanceof BooleanProperty) {
|
||||
booleanProperty = (ObservableValue<Boolean>) obsValue;
|
||||
checkBox.selectedProperty().bindBidirectional((BooleanProperty) booleanProperty);
|
||||
}
|
||||
|
||||
checkBox.disableProperty().bind(Bindings.not(
|
||||
getTreeTableView().editableProperty().and(
|
||||
getTableColumn().editableProperty()).and(
|
||||
editableProperty())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShowLabel() {
|
||||
this.showLabel = converter != null;
|
||||
this.checkBox.setAlignment(showLabel ? Pos.CENTER_LEFT : Pos.CENTER);
|
||||
}
|
||||
|
||||
private ObservableValue<?> getSelectedProperty() {
|
||||
return getSelectedStateCallback() != null ?
|
||||
getSelectedStateCallback().call(getIndex()) :
|
||||
getTableColumn().getCellObservableValue(getIndex());
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -28,20 +28,30 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MessageDialogPane extends StackPane {
|
||||
|
||||
public enum MessageType {
|
||||
ERROR,
|
||||
INFORMATION,
|
||||
INFO,
|
||||
WARNING,
|
||||
QUESTION,
|
||||
FINE,
|
||||
SUCCESS;
|
||||
|
||||
public String getDisplayName() {
|
||||
return i18n("message." + name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -64,13 +74,13 @@ public final class MessageDialogPane extends StackPane {
|
||||
this.title.setText(title);
|
||||
|
||||
switch (type) {
|
||||
case INFORMATION:
|
||||
case INFO:
|
||||
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
|
||||
break;
|
||||
case ERROR:
|
||||
graphic.setGraphic(SVG.closeCircle(Theme.blackFillBinding(), 40, 40));
|
||||
break;
|
||||
case FINE:
|
||||
case SUCCESS:
|
||||
graphic.setGraphic(SVG.checkCircle(Theme.blackFillBinding(), 40, 40));
|
||||
break;
|
||||
case WARNING:
|
||||
@@ -90,7 +100,7 @@ public final class MessageDialogPane extends StackPane {
|
||||
});
|
||||
}
|
||||
|
||||
public void addButton(ButtonBase btn) {
|
||||
public void addButton(Node btn) {
|
||||
btn.addEventHandler(ActionEvent.ACTION, e -> fireEvent(new DialogCloseEvent()));
|
||||
actions.getChildren().add(btn);
|
||||
}
|
||||
@@ -99,53 +109,97 @@ public final class MessageDialogPane extends StackPane {
|
||||
cancelButton = btn;
|
||||
}
|
||||
|
||||
public static MessageDialogPane ok(String text, String title, MessageType type, Runnable ok) {
|
||||
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
|
||||
|
||||
JFXButton btnOk = new JFXButton(i18n("button.ok"));
|
||||
btnOk.getStyleClass().add("dialog-accept");
|
||||
if (ok != null) {
|
||||
btnOk.setOnAction(e -> ok.run());
|
||||
}
|
||||
dialog.addButton(btnOk);
|
||||
dialog.setCancelButton(btnOk);
|
||||
|
||||
return dialog;
|
||||
public ButtonBase getCancelButton() {
|
||||
return cancelButton;
|
||||
}
|
||||
|
||||
public static MessageDialogPane yesOrNo(String text, String title, MessageType type, Runnable yes, Runnable no) {
|
||||
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
|
||||
public static class Builder {
|
||||
private final MessageDialogPane dialog;
|
||||
|
||||
JFXButton btnYes = new JFXButton(i18n("button.yes"));
|
||||
btnYes.getStyleClass().add("dialog-accept");
|
||||
if (yes != null) {
|
||||
btnYes.setOnAction(e -> yes.run());
|
||||
public Builder(String text, String title, MessageType type) {
|
||||
this.dialog = new MessageDialogPane(text, title, type);
|
||||
}
|
||||
dialog.addButton(btnYes);
|
||||
|
||||
JFXButton btnNo = new JFXButton(i18n("button.no"));
|
||||
btnNo.getStyleClass().add("dialog-cancel");
|
||||
if (no != null) {
|
||||
btnNo.setOnAction(e -> no.run());
|
||||
public Builder addAction(Node actionNode) {
|
||||
dialog.addButton(actionNode);
|
||||
return this;
|
||||
}
|
||||
dialog.addButton(btnNo);
|
||||
dialog.setCancelButton(btnNo);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static MessageDialogPane actionOrCancel(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
|
||||
MessageDialogPane dialog = new MessageDialogPane(text, title, type);
|
||||
dialog.addButton(actionButton);
|
||||
|
||||
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
if (cancel != null) {
|
||||
btnCancel.setOnAction(e -> cancel.run());
|
||||
public Builder ok(Runnable ok) {
|
||||
JFXButton btnOk = new JFXButton(i18n("button.ok"));
|
||||
btnOk.getStyleClass().add("dialog-accept");
|
||||
if (ok != null) {
|
||||
btnOk.setOnAction(e -> ok.run());
|
||||
}
|
||||
dialog.addButton(btnOk);
|
||||
dialog.setCancelButton(btnOk);
|
||||
return this;
|
||||
}
|
||||
dialog.addButton(btnCancel);
|
||||
dialog.setCancelButton(btnCancel);
|
||||
|
||||
return dialog;
|
||||
public Builder yesOrNo(Runnable yes, Runnable no) {
|
||||
JFXButton btnYes = new JFXButton(i18n("button.yes"));
|
||||
btnYes.getStyleClass().add("dialog-accept");
|
||||
if (yes != null) {
|
||||
btnYes.setOnAction(e -> yes.run());
|
||||
}
|
||||
dialog.addButton(btnYes);
|
||||
|
||||
JFXButton btnNo = new JFXButton(i18n("button.no"));
|
||||
btnNo.getStyleClass().add("dialog-cancel");
|
||||
if (no != null) {
|
||||
btnNo.setOnAction(e -> no.run());
|
||||
}
|
||||
dialog.addButton(btnNo);
|
||||
dialog.setCancelButton(btnNo);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder actionOrCancel(ButtonBase actionButton, Runnable cancel) {
|
||||
dialog.addButton(actionButton);
|
||||
|
||||
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
btnCancel.getStyleClass().add("dialog-cancel");
|
||||
if (cancel != null) {
|
||||
btnCancel.setOnAction(e -> cancel.run());
|
||||
}
|
||||
dialog.addButton(btnCancel);
|
||||
dialog.setCancelButton(btnCancel);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder cancelOnTimeout(long timeoutMs) {
|
||||
if (dialog.getCancelButton() == null) {
|
||||
throw new IllegalStateException("Call ok/yesOrNo/actionOrCancel before calling cancelOnTimeout");
|
||||
}
|
||||
|
||||
ButtonBase cancelButton = dialog.getCancelButton();
|
||||
String originalText = cancelButton.getText();
|
||||
|
||||
Timer timer = Lang.getTimer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
long timeout = timeoutMs;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (timeout <= 0) {
|
||||
cancel();
|
||||
runInFX(() -> {
|
||||
cancelButton.fire();
|
||||
});
|
||||
return;
|
||||
}
|
||||
timeout -= 1000;
|
||||
long currentTimeout = timeout;
|
||||
runInFX(() -> cancelButton.setText(originalText + " (" + (currentTimeout / 1000) + ")"));
|
||||
}
|
||||
}, 1000, 1000);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageDialogPane build() {
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.NamedArg;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -30,84 +28,31 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiFileItem<T> extends ComponentSublist {
|
||||
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
|
||||
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
|
||||
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
|
||||
public class MultiFileItem<T> extends VBox {
|
||||
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
|
||||
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
|
||||
private final ToggleGroup group = new ToggleGroup();
|
||||
private final JFXTextField txtCustom = new JFXTextField();
|
||||
private final JFXButton btnSelect = new JFXButton();
|
||||
private final JFXRadioButton radioCustom = new JFXRadioButton();
|
||||
private final BorderPane custom = new BorderPane();
|
||||
private final VBox pane = new VBox();
|
||||
private final boolean hasCustom;
|
||||
|
||||
private Consumer<Toggle> toggleSelectedListener;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public MultiFileItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
|
||||
this.hasCustom = hasCustom;
|
||||
|
||||
BorderPane.setAlignment(txtCustom, Pos.CENTER_RIGHT);
|
||||
|
||||
btnSelect.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
|
||||
btnSelect.setOnMouseClicked(e -> {
|
||||
if (isDirectory()) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.titleProperty().bind(chooserTitle);
|
||||
File dir = chooser.showDialog(Controllers.getStage());
|
||||
if (dir != null)
|
||||
txtCustom.setText(dir.getAbsolutePath());
|
||||
} else {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().addAll(getExtensionFilters());
|
||||
chooser.titleProperty().bind(chooserTitle);
|
||||
File file = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
txtCustom.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
radioCustom.textProperty().bind(customTitleProperty());
|
||||
radioCustom.setToggleGroup(group);
|
||||
txtCustom.disableProperty().bind(radioCustom.selectedProperty().not());
|
||||
btnSelect.disableProperty().bind(radioCustom.selectedProperty().not());
|
||||
|
||||
custom.setLeft(radioCustom);
|
||||
custom.setStyle("-fx-padding: 3;");
|
||||
HBox right = new HBox();
|
||||
right.setSpacing(3);
|
||||
right.getChildren().addAll(txtCustom, btnSelect);
|
||||
custom.setRight(right);
|
||||
FXUtils.setLimitHeight(custom, 20);
|
||||
|
||||
pane.setStyle("-fx-padding: 0 0 10 0;");
|
||||
pane.setSpacing(8);
|
||||
|
||||
if (hasCustom)
|
||||
pane.getChildren().add(custom);
|
||||
getContent().add(pane);
|
||||
public MultiFileItem() {
|
||||
setPadding(new Insets(0, 0, 10, 0));
|
||||
setSpacing(8);
|
||||
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
if (toggleSelectedListener != null)
|
||||
@@ -117,7 +62,7 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
});
|
||||
selectedData.addListener((a, b, newValue) -> {
|
||||
Optional<Toggle> selecting = group.getToggles().stream()
|
||||
.filter(it -> it.getUserData() == newValue)
|
||||
.filter(it -> Objects.equals(it.getUserData(), newValue))
|
||||
.findFirst();
|
||||
if (!selecting.isPresent()) {
|
||||
selecting = group.getToggles().stream()
|
||||
@@ -129,117 +74,20 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
});
|
||||
}
|
||||
|
||||
public Node createChildren(String title) {
|
||||
return createChildren(title, null);
|
||||
}
|
||||
|
||||
public Node createChildren(String title, T userData) {
|
||||
return createChildren(title, "", userData);
|
||||
}
|
||||
|
||||
public Node createChildren(String title, String subtitle, T userData) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(3));
|
||||
FXUtils.setLimitHeight(pane, 20);
|
||||
|
||||
JFXRadioButton left = new JFXRadioButton(title);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(userData);
|
||||
pane.setLeft(left);
|
||||
|
||||
Label right = new Label(subtitle);
|
||||
right.setWrapText(true);
|
||||
right.getStyleClass().add("subtitle-label");
|
||||
right.setStyle("-fx-font-size: 10;");
|
||||
pane.setRight(right);
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
public void loadChildren(Collection<Node> list) {
|
||||
pane.getChildren().setAll(list);
|
||||
|
||||
if (hasCustom)
|
||||
pane.getChildren().add(custom);
|
||||
}
|
||||
|
||||
public void loadChildren(Collection<Node> list, T customUserData) {
|
||||
loadChildren(list);
|
||||
setCustomUserData(customUserData);
|
||||
public void loadChildren(Collection<Option<T>> options) {
|
||||
getChildren().setAll(options.stream()
|
||||
.map(option -> option.createItem(group))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public ToggleGroup getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public String getCustomTitle() {
|
||||
return customTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty customTitleProperty() {
|
||||
return customTitle;
|
||||
}
|
||||
|
||||
public void setCustomTitle(String customTitle) {
|
||||
this.customTitle.set(customTitle);
|
||||
}
|
||||
|
||||
public String getChooserTitle() {
|
||||
return chooserTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty chooserTitleProperty() {
|
||||
return chooserTitle;
|
||||
}
|
||||
|
||||
public void setChooserTitle(String chooserTitle) {
|
||||
this.chooserTitle.set(chooserTitle);
|
||||
}
|
||||
|
||||
public void setCustomUserData(T userData) {
|
||||
radioCustom.setUserData(userData);
|
||||
}
|
||||
|
||||
public boolean isCustomToggle(Toggle toggle) {
|
||||
return radioCustom == toggle;
|
||||
}
|
||||
|
||||
public void setToggleSelectedListener(Consumer<Toggle> consumer) {
|
||||
toggleSelectedListener = consumer;
|
||||
}
|
||||
|
||||
public StringProperty customTextProperty() {
|
||||
return txtCustom.textProperty();
|
||||
}
|
||||
|
||||
public String getCustomText() {
|
||||
return txtCustom.getText();
|
||||
}
|
||||
|
||||
public void setCustomText(String customText) {
|
||||
txtCustom.setText(customText);
|
||||
}
|
||||
|
||||
public JFXTextField getTxtCustom() {
|
||||
return txtCustom;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return directory.get();
|
||||
}
|
||||
|
||||
public BooleanProperty directoryProperty() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public void setDirectory(boolean directory) {
|
||||
this.directory.set(directory);
|
||||
}
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
return extensionFilters;
|
||||
}
|
||||
|
||||
public T getSelectedData() {
|
||||
return selectedData.get();
|
||||
}
|
||||
@@ -263,4 +111,187 @@ public class MultiFileItem<T> extends ComponentSublist {
|
||||
public void setFallbackData(T fallbackData) {
|
||||
this.fallbackData.set(fallbackData);
|
||||
}
|
||||
|
||||
public static class Option<T> {
|
||||
protected final String title;
|
||||
protected String subtitle;
|
||||
protected final T data;
|
||||
protected final BooleanProperty selected = new SimpleBooleanProperty();
|
||||
protected final JFXRadioButton left = new JFXRadioButton();
|
||||
|
||||
public Option(String title, T data) {
|
||||
this.title = title;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public Option<T> setSubtitle(String subtitle) {
|
||||
this.subtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return left.isSelected();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return left.selectedProperty();
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
left.setSelected(selected);
|
||||
}
|
||||
|
||||
protected Node createItem(ToggleGroup group) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(3));
|
||||
FXUtils.setLimitHeight(pane, 30);
|
||||
|
||||
left.setText(title);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(data);
|
||||
pane.setLeft(left);
|
||||
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
Optional<String> shortSubtitle = StringUtils.truncate(subtitle);
|
||||
Label right;
|
||||
if (shortSubtitle.isPresent()) {
|
||||
right = new Label(shortSubtitle.get());
|
||||
right.setTooltip(new Tooltip(subtitle));
|
||||
} else {
|
||||
right = new Label(subtitle);
|
||||
}
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.setWrapText(true);
|
||||
right.getStyleClass().add("subtitle-label");
|
||||
right.setStyle("-fx-font-size: 10;");
|
||||
pane.setRight(right);
|
||||
}
|
||||
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringOption<T> extends Option<T> {
|
||||
private JFXTextField customField = new JFXTextField();
|
||||
|
||||
public StringOption(String title, T data) {
|
||||
super(title, data);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return customField.getText();
|
||||
}
|
||||
|
||||
public StringProperty valueProperty() {
|
||||
return customField.textProperty();
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
customField.setText(value);
|
||||
}
|
||||
|
||||
public StringOption<T> bindBidirectional(Property<String> property) {
|
||||
customField.textProperty().bindBidirectional(property);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringOption<T> setValidators(ValidatorBase... validators) {
|
||||
customField.setValidators(validators);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createItem(ToggleGroup group) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(3));
|
||||
FXUtils.setLimitHeight(pane, 30);
|
||||
|
||||
left.setText(title);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(data);
|
||||
pane.setLeft(left);
|
||||
|
||||
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
|
||||
customField.disableProperty().bind(left.selectedProperty().not());
|
||||
|
||||
if (!customField.getValidators().isEmpty()) {
|
||||
FXUtils.setValidateWhileTextChanged(customField, true);
|
||||
}
|
||||
|
||||
pane.setRight(customField);
|
||||
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileOption<T> extends Option<T> {
|
||||
private FileSelector selector = new FileSelector();
|
||||
|
||||
public FileOption(String title, T data) {
|
||||
super(title, data);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return selector.getValue();
|
||||
}
|
||||
|
||||
public StringProperty valueProperty() {
|
||||
return selector.valueProperty();
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
selector.setValue(value);
|
||||
}
|
||||
|
||||
public FileOption<T> setDirectory(boolean directory) {
|
||||
selector.setDirectory(directory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FileOption<T> bindBidirectional(Property<String> property) {
|
||||
selector.valueProperty().bindBidirectional(property);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FileOption<T> setChooserTitle(String chooserTitle) {
|
||||
selector.setChooserTitle(chooserTitle);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
|
||||
return selector.getExtensionFilters();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createItem(ToggleGroup group) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(3));
|
||||
FXUtils.setLimitHeight(pane, 30);
|
||||
|
||||
left.setText(title);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(data);
|
||||
pane.setLeft(left);
|
||||
|
||||
selector.disableProperty().bind(left.selectedProperty().not());
|
||||
BorderPane.setAlignment(selector, Pos.CENTER_RIGHT);
|
||||
pane.setRight(selector);
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ public class Navigator extends TransitionPane {
|
||||
getChildren().setAll(init);
|
||||
|
||||
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
|
||||
if (init instanceof PageAware) ((PageAware) init).onPageShown();
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
@@ -78,6 +79,7 @@ public class Navigator extends TransitionPane {
|
||||
|
||||
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
|
||||
node.fireEvent(navigated);
|
||||
if (node instanceof PageAware) ((PageAware) node).onPageShown();
|
||||
|
||||
EventHandler<PageCloseEvent> handler = event -> close(node);
|
||||
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
|
||||
@@ -108,7 +110,11 @@ public class Navigator extends TransitionPane {
|
||||
|
||||
Logging.LOG.info("Closed page " + from);
|
||||
|
||||
stack.pop();
|
||||
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();
|
||||
|
||||
backable.set(canGoBack());
|
||||
Node node = stack.peek();
|
||||
|
||||
@@ -202,6 +208,7 @@ public class Navigator extends TransitionPane {
|
||||
};
|
||||
|
||||
public static 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");
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXToggleButton;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class OptionToggleButton extends StackPane {
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty();
|
||||
|
||||
public OptionToggleButton() {
|
||||
getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 8, 8, 16));
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
getChildren().setAll(container);
|
||||
|
||||
VBox left = new VBox();
|
||||
left.setMouseTransparent(true);
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textProperty().bind(title);
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.setMouseTransparent(true);
|
||||
subtitleLabel.textProperty().bind(subtitle);
|
||||
pane.setLeft(left);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||
pane.setRight(toggleButton);
|
||||
toggleButton.selectedProperty().bindBidirectional(selected);
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
container.setOnMouseClicked(e -> {
|
||||
toggleButton.setSelected(!toggleButton.isSelected());
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
left.getChildren().setAll(titleLabel, subtitleLabel);
|
||||
} else {
|
||||
left.getChildren().setAll(titleLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
public interface PageAware {
|
||||
default void onPageShown() {
|
||||
}
|
||||
|
||||
default void onPageHidden() {
|
||||
}
|
||||
}
|
||||
@@ -17,108 +17,111 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class PromptDialogPane extends StackPane {
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
|
||||
public class PromptDialogPane extends DialogPane {
|
||||
private final CompletableFuture<List<Builder.Question<?>>> future = new CompletableFuture<>();
|
||||
|
||||
@FXML
|
||||
private JFXButton acceptButton;
|
||||
@FXML
|
||||
private JFXButton cancelButton;
|
||||
@FXML
|
||||
private VBox vbox;
|
||||
@FXML
|
||||
private Label title;
|
||||
@FXML
|
||||
private Label lblCreationWarning;
|
||||
@FXML
|
||||
private SpinnerPane acceptPane;
|
||||
private final Builder builder;
|
||||
|
||||
public PromptDialogPane(Builder builder) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
|
||||
this.title.setText(builder.title);
|
||||
this.builder = builder;
|
||||
setTitle(builder.title);
|
||||
|
||||
GridPane body = new GridPane();
|
||||
body.setVgap(8);
|
||||
body.setHgap(16);
|
||||
body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
setBody(body);
|
||||
List<BooleanBinding> bindings = new ArrayList<>();
|
||||
int rowIndex = 0;
|
||||
for (Builder.Question<?> question : builder.questions) {
|
||||
if (question instanceof Builder.StringQuestion) {
|
||||
Builder.StringQuestion stringQuestion = (Builder.StringQuestion) question;
|
||||
JFXTextField textField = new JFXTextField();
|
||||
textField.textProperty().addListener((a, b, newValue) -> ((Builder.StringQuestion) question).value = textField.getText());
|
||||
textField.setText(((Builder.StringQuestion) question).value);
|
||||
textField.textProperty().addListener((a, b, newValue) -> stringQuestion.value = textField.getText());
|
||||
textField.setText(stringQuestion.value);
|
||||
textField.setValidators(((Builder.StringQuestion) question).validators.toArray(new ValidatorBase[0]));
|
||||
if (stringQuestion.promptText != null) {
|
||||
textField.setPromptText(stringQuestion.promptText);
|
||||
}
|
||||
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
|
||||
|
||||
if (StringUtils.isNotBlank(question.question)) {
|
||||
vbox.getChildren().add(new Label(question.question));
|
||||
if (StringUtils.isNotBlank(question.question.get())) {
|
||||
body.addRow(rowIndex++, new Label(question.question.get()), textField);
|
||||
} else {
|
||||
GridPane.setColumnSpan(textField, 2);
|
||||
body.addRow(rowIndex++, textField);
|
||||
}
|
||||
VBox.setMargin(textField, new Insets(0, 0, 20, 0));
|
||||
vbox.getChildren().add(textField);
|
||||
GridPane.setMargin(textField, new Insets(0, 0, 20, 0));
|
||||
} else if (question instanceof Builder.BooleanQuestion) {
|
||||
HBox hBox = new HBox();
|
||||
GridPane.setColumnSpan(hBox, 2);
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
hBox.getChildren().setAll(checkBox);
|
||||
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
|
||||
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
|
||||
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
|
||||
checkBox.setText(question.question);
|
||||
vbox.getChildren().add(hBox);
|
||||
checkBox.setText(question.question.get());
|
||||
body.addRow(rowIndex++, hBox);
|
||||
} else if (question instanceof Builder.CandidatesQuestion) {
|
||||
HBox hBox = new HBox();
|
||||
JFXComboBox<String> comboBox = new JFXComboBox<>();
|
||||
hBox.getChildren().setAll(comboBox);
|
||||
comboBox.getItems().setAll(((Builder.CandidatesQuestion) question).candidates);
|
||||
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
|
||||
((Builder.CandidatesQuestion) question).value = newValue.intValue());
|
||||
comboBox.getSelectionModel().select(0);
|
||||
if (StringUtils.isNotBlank(question.question)) {
|
||||
vbox.getChildren().add(new Label(question.question));
|
||||
if (StringUtils.isNotBlank(question.question.get())) {
|
||||
body.addRow(rowIndex++, new Label(question.question.get()), comboBox);
|
||||
} else {
|
||||
GridPane.setColumnSpan(comboBox, 2);
|
||||
body.addRow(rowIndex++, comboBox);
|
||||
}
|
||||
vbox.getChildren().add(hBox);
|
||||
} else if (question instanceof Builder.HintQuestion) {
|
||||
HintPane pane = new HintPane();
|
||||
GridPane.setColumnSpan(pane, 2);
|
||||
pane.textProperty().bind(question.question);
|
||||
body.addRow(rowIndex++, pane);
|
||||
}
|
||||
}
|
||||
|
||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x),
|
||||
validProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> bindings.stream().allMatch(BooleanBinding::get),
|
||||
bindings.toArray(new BooleanBinding[0])
|
||||
));
|
||||
}
|
||||
|
||||
acceptButton.setOnMouseClicked(e -> {
|
||||
acceptPane.showSpinner();
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
|
||||
builder.callback.call(builder.questions, () -> {
|
||||
acceptPane.hideSpinner();
|
||||
future.complete(builder.questions);
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}, msg -> {
|
||||
acceptPane.hideSpinner();
|
||||
lblCreationWarning.setText(msg);
|
||||
});
|
||||
builder.callback.call(builder.questions, () -> {
|
||||
future.complete(builder.questions);
|
||||
runInFX(this::onSuccess);
|
||||
}, msg -> {
|
||||
runInFX(() -> onFailure(msg));
|
||||
});
|
||||
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {
|
||||
@@ -141,26 +144,50 @@ public class PromptDialogPane extends StackPane {
|
||||
}
|
||||
|
||||
public static class Question<T> {
|
||||
public final String question;
|
||||
public final StringProperty question = new SimpleStringProperty();
|
||||
protected T value;
|
||||
|
||||
public Question(String question) {
|
||||
this.question = question;
|
||||
this.question.set(question);
|
||||
}
|
||||
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getQuestion() {
|
||||
return question.get();
|
||||
}
|
||||
|
||||
public StringProperty questionProperty() {
|
||||
return question;
|
||||
}
|
||||
|
||||
public void setQuestion(String question) {
|
||||
this.question.set(question);
|
||||
}
|
||||
}
|
||||
|
||||
public static class HintQuestion extends Question<Void> {
|
||||
public HintQuestion(String hint) {
|
||||
super(hint);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringQuestion extends Question<String> {
|
||||
protected final List<ValidatorBase> validators;
|
||||
protected String promptText;
|
||||
|
||||
public StringQuestion(String question, String defaultValue, ValidatorBase... validators) {
|
||||
super(question);
|
||||
this.value = defaultValue;
|
||||
this.validators = Arrays.asList(validators);
|
||||
}
|
||||
|
||||
public StringQuestion setPromptText(String promptText) {
|
||||
this.promptText = promptText;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CandidatesQuestion extends Question<Integer> {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
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;
|
||||
import javafx.beans.NamedArg;
|
||||
@@ -26,12 +28,7 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleablePropertyFactory;
|
||||
import javafx.css.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Background;
|
||||
@@ -41,6 +38,7 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.List;
|
||||
@@ -104,6 +102,40 @@ public class RipplerContainer extends StackPane {
|
||||
rectangle.widthProperty().bind(widthProperty());
|
||||
rectangle.heightProperty().bind(heightProperty());
|
||||
}));
|
||||
|
||||
setOnMouseEntered(e -> {
|
||||
new Transition() {
|
||||
{
|
||||
setCycleCount(1);
|
||||
setCycleDuration(Duration.millis(200));
|
||||
setInterpolator(Interpolator.EASE_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(frac);
|
||||
}
|
||||
}.play();
|
||||
});
|
||||
|
||||
setOnMouseExited(e -> {
|
||||
new Transition() {
|
||||
{
|
||||
setCycleCount(1);
|
||||
setCycleDuration(Duration.millis(200));
|
||||
setInterpolator(Interpolator.EASE_OUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
interpolateBackground(1 - frac);
|
||||
}
|
||||
}.play();
|
||||
});
|
||||
}
|
||||
|
||||
private void interpolateBackground(double frac) {
|
||||
setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, frac * 0.04), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
|
||||
protected void updateChildren() {
|
||||
|
||||
@@ -21,6 +21,9 @@ import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.event.EventType;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -29,6 +32,7 @@ import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class SpinnerPane extends Control {
|
||||
@@ -85,6 +89,24 @@ public class SpinnerPane extends Control {
|
||||
this.failedReason.set(failedReason);
|
||||
}
|
||||
|
||||
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
|
||||
return onFailedAction;
|
||||
}
|
||||
|
||||
public final void setOnFailedAction(EventHandler<Event> value) {
|
||||
onFailedActionProperty().set(value);
|
||||
}
|
||||
|
||||
public final EventHandler<Event> getOnFailedAction() {
|
||||
return onFailedActionProperty().get();
|
||||
}
|
||||
|
||||
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
setEventHandler(FAILED_ACTION, get());
|
||||
}
|
||||
};
|
||||
@Override
|
||||
protected Skin createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
@@ -105,7 +127,15 @@ public class SpinnerPane extends Control {
|
||||
|
||||
topPane.getChildren().setAll(spinner);
|
||||
topPane.getStyleClass().add("notice-pane");
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
failedPane.getChildren().setAll(failedReasonLabel);
|
||||
failedPane.onMouseClickedProperty().bind(
|
||||
BindingMapping.of(control.onFailedAction)
|
||||
.map(actionHandler -> (e -> {
|
||||
if (actionHandler != null) {
|
||||
actionHandler.handle(new Event(FAILED_ACTION));
|
||||
}
|
||||
})));
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().content, newValue -> {
|
||||
if (newValue == null) {
|
||||
@@ -146,4 +176,6 @@ public class SpinnerPane extends Control {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
public static final EventType<Event> FAILED_ACTION = new EventType<>(Event.ANY, "FAILED_ACTION");
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
public class TabHeader extends Control implements TabControl {
|
||||
public class TabHeader extends Control implements TabControl, PageAware {
|
||||
|
||||
public TabHeader(Tab<?>... tabs) {
|
||||
getStyleClass().setAll("tab-header");
|
||||
@@ -70,6 +70,42 @@ public class TabHeader extends Control implements TabControl {
|
||||
this.selectionModel.set(selectionModel);
|
||||
}
|
||||
|
||||
public void select(Tab<?> tab) {
|
||||
Tab<?> oldTab = getSelectionModel().getSelectedItem();
|
||||
if (oldTab != null) {
|
||||
if (oldTab.getNode() instanceof PageAware) {
|
||||
((PageAware) oldTab.getNode()).onPageHidden();
|
||||
}
|
||||
}
|
||||
|
||||
tab.initializeIfNeeded();
|
||||
if (tab.getNode() instanceof PageAware) {
|
||||
((PageAware) tab.getNode()).onPageShown();
|
||||
}
|
||||
|
||||
getSelectionModel().select(tab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
Tab<?> tab = getSelectionModel().getSelectedItem();
|
||||
if (tab != null) {
|
||||
if (tab.getNode() instanceof PageAware) {
|
||||
((PageAware) tab.getNode()).onPageShown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
Tab<?> tab = getSelectionModel().getSelectedItem();
|
||||
if (tab != null) {
|
||||
if (tab.getNode() instanceof PageAware) {
|
||||
((PageAware) tab.getNode()).onPageHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The position to place the tabs.
|
||||
*/
|
||||
|
||||
@@ -61,6 +61,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -96,7 +97,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
@Override
|
||||
public void onReady(Task<?> task) {
|
||||
if (task instanceof Task.StageTask) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
|
||||
});
|
||||
@@ -143,14 +144,14 @@ public final class TaskListPane extends StackPane {
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
|
||||
listBox.add(listBox.indexOf(stageNode) + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task<?> task) {
|
||||
if (task instanceof Task.StageTask) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
|
||||
});
|
||||
@@ -167,7 +168,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
@Override
|
||||
public void onFailed(Task<?> task, Throwable throwable) {
|
||||
if (task instanceof Task.StageTask) {
|
||||
if (task.getStage() != null) {
|
||||
Platform.runLater(() -> {
|
||||
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
|
||||
});
|
||||
@@ -181,14 +182,29 @@ public final class TaskListPane extends StackPane {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPropertiesUpdate(Map<String, Map<String, Object>> stageProperties) {
|
||||
stageProperties.forEach((stage, properties) -> {
|
||||
int count = tryCast(properties.get("count"), Integer.class).orElse(0),
|
||||
total = tryCast(properties.get("total"), Integer.class).orElse(0);
|
||||
if (total > 0)
|
||||
Platform.runLater(() ->
|
||||
stageNodes.stream().filter(x -> x.stage.equals(stage)).findAny().ifPresent(stageNode -> stageNode.updateCounter(count, total)));
|
||||
});
|
||||
public void onPropertiesUpdate(Task<?> task) {
|
||||
if (task instanceof Task.CountTask) {
|
||||
runInFX(() -> {
|
||||
stageNodes.stream()
|
||||
.filter(x -> x.stage.equals(((Task.CountTask) task).getCountStage()))
|
||||
.findAny()
|
||||
.ifPresent(StageNode::count);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.getStage() != null) {
|
||||
int total = tryCast(task.getProperties().get("total"), Integer.class).orElse(0);
|
||||
runInFX(() -> {
|
||||
stageNodes.stream()
|
||||
.filter(x -> x.stage.equals(task.getStage()))
|
||||
.findAny()
|
||||
.ifPresent(stageNode -> {
|
||||
stageNode.setTotal(total);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -197,6 +213,8 @@ public final class TaskListPane extends StackPane {
|
||||
private final String stage;
|
||||
private final Label title = new Label();
|
||||
private final String message;
|
||||
private int count = 0;
|
||||
private int total = 0;
|
||||
private boolean started = false;
|
||||
|
||||
public StageNode(String stage) {
|
||||
@@ -242,6 +260,15 @@ public final class TaskListPane extends StackPane {
|
||||
setLeft(FXUtils.limitingSize(SVG.check(Theme.blackFillBinding(), 14, 14), 14, 14));
|
||||
}
|
||||
|
||||
public void count() {
|
||||
updateCounter(++count, total);
|
||||
}
|
||||
|
||||
public void setTotal(int total) {
|
||||
this.total = total;
|
||||
updateCounter(count, total);
|
||||
}
|
||||
|
||||
public void updateCounter(int count, int total) {
|
||||
if (total > 0)
|
||||
title.setText(String.format("%s - %d/%d", message, count, total));
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -52,8 +51,8 @@ public class TwoLineListItem extends VBox {
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
|
||||
FlowPane firstLine = new FlowPane();
|
||||
firstLine.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
@@ -63,7 +62,7 @@ public class TwoLineListItem extends VBox {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
FlowPane.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
});
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class URLValidator extends ValidatorBase {
|
||||
private final boolean nullable;
|
||||
|
||||
public URLValidator() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public URLValidator(@NamedArg("nullable") boolean nullable) {
|
||||
this(i18n("input.url"), nullable);
|
||||
}
|
||||
|
||||
public URLValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
|
||||
super(message);
|
||||
this.nullable = nullable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void eval() {
|
||||
if (srcControl.get() instanceof TextInputControl) {
|
||||
evalTextInputField();
|
||||
}
|
||||
}
|
||||
|
||||
private void evalTextInputField() {
|
||||
TextInputControl textField = ((TextInputControl) srcControl.get());
|
||||
|
||||
if (StringUtils.isBlank(textField.getText()))
|
||||
hasErrors.set(!nullable);
|
||||
else {
|
||||
try {
|
||||
new URL(textField.getText()).toURI();
|
||||
hasErrors.set(false);
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
hasErrors.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ public class Decorator extends Control {
|
||||
private final BooleanProperty canBack = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty canClose = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty showCloseAsHome = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
private final Stage primaryStage;
|
||||
private Navigation.NavigationDirection navigationDirection = Navigation.NavigationDirection.START;
|
||||
private StackPane drawerWrapper;
|
||||
@@ -204,6 +205,18 @@ public class Decorator extends Control {
|
||||
this.dragging.set(dragging);
|
||||
}
|
||||
|
||||
public boolean isTitleTransparent() {
|
||||
return titleTransparent.get();
|
||||
}
|
||||
|
||||
public BooleanProperty titleTransparentProperty() {
|
||||
return titleTransparent;
|
||||
}
|
||||
|
||||
public void setTitleTransparent(boolean titleTransparent) {
|
||||
this.titleTransparent.set(titleTransparent);
|
||||
}
|
||||
|
||||
public ObjectProperty<EventHandler<ActionEvent>> onBackNavButtonActionProperty() {
|
||||
return onBackNavButtonAction;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public class DecoratorAnimatedPage extends Control {
|
||||
|
||||
protected final VBox left = new VBox();
|
||||
protected final StackPane center = new StackPane();
|
||||
|
||||
{
|
||||
getStyleClass().add("gray-background");
|
||||
}
|
||||
|
||||
protected void setLeft(Node... children) {
|
||||
left.getChildren().setAll(children);
|
||||
}
|
||||
|
||||
protected void setCenter(Node... children) {
|
||||
center.getChildren().setAll(children);
|
||||
}
|
||||
|
||||
public VBox getLeft() {
|
||||
return left;
|
||||
}
|
||||
|
||||
public StackPane getCenter() {
|
||||
return center;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new DecoratorAnimatedPageSkin<>(this);
|
||||
}
|
||||
|
||||
public static class DecoratorAnimatedPageSkin<T extends DecoratorAnimatedPage> extends SkinBase<T> {
|
||||
|
||||
protected DecoratorAnimatedPageSkin(T control) {
|
||||
super(control);
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(control.left);
|
||||
FXUtils.setLimitWidth(control.left, 200);
|
||||
pane.setCenter(control.center);
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
|
||||
protected void setLeft(Node... children) {
|
||||
getSkinnable().setLeft(children);
|
||||
}
|
||||
|
||||
protected void setCenter(Node... children) {
|
||||
getSkinnable().setCenter(children);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 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;
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,13 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
@@ -43,6 +45,7 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -74,6 +77,7 @@ public class DecoratorController {
|
||||
public DecoratorController(Stage stage, Node mainPage) {
|
||||
decorator = new Decorator(stage);
|
||||
decorator.setOnCloseButtonAction(Launcher::stopApplication);
|
||||
decorator.titleTransparentProperty().bind(config().titleTransparentProperty());
|
||||
|
||||
navigator = new Navigator();
|
||||
navigator.setOnNavigated(this::onNavigated);
|
||||
@@ -138,13 +142,25 @@ public class DecoratorController {
|
||||
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
|
||||
.orElse(null);
|
||||
}
|
||||
if (config().getBackgroundImageType() == EnumBackgroundImage.NETWORK) {
|
||||
if (!NetworkUtils.isURL(config().getBackgroundImageUrl())) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
} else {
|
||||
image = new Image(config().getBackgroundImageUrl(), true);
|
||||
}
|
||||
} else if (config().getBackgroundImageType() == EnumBackgroundImage.CLASSIC) {
|
||||
image = newImage("/assets/img/background-classic.jpg");
|
||||
} else if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
if (image == null) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
}
|
||||
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
|
||||
},
|
||||
config().backgroundImageTypeProperty(),
|
||||
config().backgroundImageProperty()));
|
||||
config().backgroundImageProperty(),
|
||||
config().backgroundImageUrlProperty()));
|
||||
}
|
||||
|
||||
private Image defaultBackground = newImage("/assets/img/background.jpg");
|
||||
@@ -217,8 +233,10 @@ public class DecoratorController {
|
||||
|
||||
// ==== Navigation ====
|
||||
|
||||
public Navigator getNavigator() {
|
||||
return navigator;
|
||||
private static final DecoratorAnimationProducer animation = new DecoratorAnimationProducer();
|
||||
|
||||
public void navigate(Node node) {
|
||||
navigator.navigate(node, animation);
|
||||
}
|
||||
|
||||
private void close() {
|
||||
@@ -354,6 +372,10 @@ public class DecoratorController {
|
||||
if (dialog != null) {
|
||||
dialogPane.pop(node);
|
||||
|
||||
if (node instanceof DialogAware) {
|
||||
((DialogAware) node).onDialogClosed();
|
||||
}
|
||||
|
||||
if (dialogPane.getChildren().isEmpty()) {
|
||||
dialog.close();
|
||||
dialog = null;
|
||||
@@ -379,7 +401,7 @@ public class DecoratorController {
|
||||
public void startWizard(WizardProvider wizardProvider, String category) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
getNavigator().navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
|
||||
navigator.navigate(new DecoratorWizardDisplayer(wizardProvider, category), ContainerAnimations.FADE.getAnimationProducer());
|
||||
}
|
||||
|
||||
// ==== Authlib Injector DnD ====
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.svg.SVGGlyph;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -38,7 +37,6 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -57,7 +55,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane titleContainer;
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
private final StackPane leftPane;
|
||||
|
||||
private double xOffset, yOffset, newX, newY, initX, initY;
|
||||
private boolean titleBarTransparent = true;
|
||||
@@ -106,21 +103,10 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
parent.getChildren().add(wrapper);
|
||||
|
||||
// center node with a animation layer at bottom, a container layer at middle and a "welcome" layer at top.
|
||||
// center node with an animation layer at bottom, a container layer at middle and a "welcome" layer at top.
|
||||
StackPane container = new StackPane();
|
||||
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
// animation layer at bottom
|
||||
{
|
||||
HBox layer = new HBox();
|
||||
leftPane = new StackPane();
|
||||
leftPane.setPrefWidth(0);
|
||||
leftPane.getStyleClass().add("jfx-decorator-drawer");
|
||||
layer.getChildren().setAll(leftPane);
|
||||
container.getChildren().add(layer);
|
||||
}
|
||||
|
||||
// content layer at middle
|
||||
{
|
||||
StackPane contentPlaceHolder = new StackPane();
|
||||
@@ -153,7 +139,24 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
titleContainer = new StackPane();
|
||||
titleContainer.setPickOnBounds(false);
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar", "background");
|
||||
titleContainer.getStyleClass().addAll("jfx-tool-bar");
|
||||
|
||||
FXUtils.onChangeAndOperate(skinnable.titleTransparentProperty(), titleTransparent -> {
|
||||
if (titleTransparent) {
|
||||
wrapper.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
container.backgroundProperty().unbind();
|
||||
container.setBackground(null);
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
} else {
|
||||
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
wrapper.backgroundProperty().unbind();
|
||||
wrapper.setBackground(null);
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
}
|
||||
});
|
||||
|
||||
control.capableDraggingWindow(titleContainer);
|
||||
|
||||
BorderPane titleBar = new BorderPane();
|
||||
@@ -162,6 +165,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
|
||||
@@ -179,22 +183,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(node);
|
||||
}
|
||||
|
||||
leftPane.prefWidthProperty().unbind();
|
||||
if (s.getLeftPaneWidth() >= 0) {
|
||||
FXUtils.playAnimation(leftPane, "animation",
|
||||
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
|
||||
} else {
|
||||
Animation animation = FXUtils.playAnimation(leftPane, "animation1",
|
||||
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, container.getWidth(), FXUtils.SINE);
|
||||
if (animation != null) {
|
||||
animation.setOnFinished(action -> {
|
||||
if (animation.getStatus() != Animation.Status.STOPPED) {
|
||||
leftPane.prefWidthProperty().bind(container.widthProperty());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
titleBar.setCenter(navBarPane);
|
||||
titleBar.setRight(buttonsContainerPlaceHolder);
|
||||
|
||||
@@ -21,15 +21,13 @@ import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.mod.DownloadManager;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseModManager;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -44,6 +42,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
|
||||
@@ -58,12 +57,14 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
|
||||
private final TabHeader tab;
|
||||
private final TabHeader.Tab<VersionsPage> newGameTab = new TabHeader.Tab<>("newGameTab");
|
||||
@@ -78,10 +79,10 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
private WeakListenerHolder listenerHolder;
|
||||
|
||||
public DownloadPage() {
|
||||
newGameTab.setNodeSupplier(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected));
|
||||
modpackTab.setNodeSupplier(() -> {
|
||||
DownloadListPage page = new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl);
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = new DownloadListPage(CurseForgeRemoteModRepository.MODPACKS, Versions::downloadModpackImpl);
|
||||
|
||||
JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack"));
|
||||
installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
@@ -90,63 +91,59 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
return page;
|
||||
});
|
||||
modTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MOD, (profile, version, file) -> download(profile, version, file, "mods"), true));
|
||||
resourcePackTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_RESOURCE_PACK, (profile, version, file) -> download(profile, version, file, "resourcepacks")));
|
||||
// customizationTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_CUSTOMIZATION, this::download));
|
||||
worldTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_WORLD));
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.RESOURCE_PACKS, (profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
customizationTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.CUSTOMIZATIONS)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
|
||||
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
|
||||
tab.getSelectionModel().select(newGameTab);
|
||||
tab.select(newGameTab);
|
||||
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
if (newValue.initializeIfNeeded()) {
|
||||
if (newValue.getNode() instanceof VersionPage.VersionLoadable) {
|
||||
((VersionPage.VersionLoadable) newValue.getNode()).loadVersion(Profiles.getSelectedProfile(), null);
|
||||
}
|
||||
}
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
|
||||
});
|
||||
|
||||
{
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.startCategory(i18n("download.game"))
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("game"));
|
||||
item.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 24, 24)));
|
||||
item.setLeftGraphic(wrap(SVG::gamepad));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(newGameTab));
|
||||
item.setOnAction(e -> tab.getSelectionModel().select(newGameTab));
|
||||
})
|
||||
.startCategory(i18n("download"))
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("mods"));
|
||||
item.setLeftGraphic(wrap(SVG.puzzle(Theme.blackFillBinding(), 24, 24)));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab));
|
||||
item.setOnAction(e -> tab.getSelectionModel().select(modTab));
|
||||
item.setOnAction(e -> tab.select(newGameTab));
|
||||
})
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("modpack"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.pack(Theme.blackFillBinding(), 24, 24)));
|
||||
settingsItem.setLeftGraphic(wrap(SVG::pack));
|
||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modpackTab));
|
||||
settingsItem.setOnAction(e -> tab.getSelectionModel().select(modpackTab));
|
||||
settingsItem.setOnAction(e -> tab.select(modpackTab));
|
||||
})
|
||||
.startCategory(i18n("download.content"))
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("mods"));
|
||||
item.setLeftGraphic(wrap(SVG::puzzle));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(modTab));
|
||||
item.setOnAction(e -> tab.select(modTab));
|
||||
})
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("resourcepack"));
|
||||
item.setLeftGraphic(wrap(SVG.textureBox(Theme.blackFillBinding(), 24, 24)));
|
||||
item.setLeftGraphic(wrap(SVG::textureBox));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
|
||||
item.setOnAction(e -> tab.getSelectionModel().select(resourcePackTab));
|
||||
item.setOnAction(e -> tab.select(resourcePackTab));
|
||||
})
|
||||
// .addNavigationDrawerItem(item -> {
|
||||
// item.setTitle(i18n("download.curseforge.customization"));
|
||||
// item.setLeftGraphic(wrap(SVG.script(Theme.blackFillBinding(), 24, 24)));
|
||||
// item.setLeftGraphic(wrap(SVG::script));
|
||||
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
|
||||
// item.setOnAction(e -> tab.getSelectionModel().select(customizationTab));
|
||||
// item.setOnAction(e -> tab.select(customizationTab));
|
||||
// })
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("world"));
|
||||
item.setLeftGraphic(wrap(SVG.earth(Theme.blackFillBinding(), 24, 24)));
|
||||
item.setLeftGraphic(wrap(SVG::earth));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
|
||||
item.setOnAction(e -> tab.getSelectionModel().select(worldTab));
|
||||
item.setOnAction(e -> tab.select(worldTab));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -155,7 +152,17 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
private void download(Profile profile, @Nullable String version, DownloadManager.Version file, String subdirectoryName) {
|
||||
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (node instanceof VersionPage.VersionLoadable) {
|
||||
((VersionPage.VersionLoadable) node).loadVersion(Profiles.getSelectedProfile(), null);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
private void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
|
||||
@@ -168,9 +175,13 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
|
||||
task.setName(file.getName());
|
||||
return task;
|
||||
}).whenComplete(exception -> {
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
if (exception instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
} else {
|
||||
Controllers.showToast(i18n("install.success"));
|
||||
}
|
||||
@@ -182,24 +193,26 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void loadVersions(Profile profile) {
|
||||
WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
listenerHolder = new WeakListenerHolder();
|
||||
runInFX(() -> {
|
||||
if (profile == Profiles.getSelectedProfile()) {
|
||||
if (modTab.isInitialized()) {
|
||||
modTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (modpackTab.isInitialized()) {
|
||||
modpackTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (resourcePackTab.isInitialized()) {
|
||||
resourcePackTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (customizationTab.isInitialized()) {
|
||||
customizationTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (worldTab.isInitialized()) {
|
||||
worldTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(profile.selectedVersionProperty(), version -> {
|
||||
if (modTab.isInitialized()) {
|
||||
modTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (modpackTab.isInitialized()) {
|
||||
modpackTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (resourcePackTab.isInitialized()) {
|
||||
resourcePackTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (customizationTab.isInitialized()) {
|
||||
customizationTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (worldTab.isInitialized()) {
|
||||
worldTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,6 +222,18 @@ public class DownloadPage extends BorderPane implements DecoratorPage {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public void showGameDownloads() {
|
||||
tab.select(newGameTab);
|
||||
}
|
||||
|
||||
public void showModDownloads() {
|
||||
tab.select(modTab);
|
||||
}
|
||||
|
||||
public void showWorldDownloads() {
|
||||
tab.select(worldTab);
|
||||
}
|
||||
|
||||
private class DownloadNavigator implements Navigation {
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
@@ -139,22 +140,24 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPadding(new Insets(16));
|
||||
|
||||
VBox list = new VBox(8);
|
||||
ComponentList list = new ComponentList();
|
||||
list.getStyleClass().add("no-padding");
|
||||
root.setCenter(list);
|
||||
{
|
||||
HBox versionNamePane = new HBox(8);
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
versionNamePane.getStyleClass().add("card");
|
||||
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
|
||||
|
||||
versionNamePane.getChildren().add(new Label(i18n("archive.name")));
|
||||
versionNamePane.setPadding(new Insets(20, 8, 20, 16));
|
||||
|
||||
control.txtName.setMaxWidth(300);
|
||||
versionNamePane.getChildren().add(control.txtName);
|
||||
list.getChildren().add(versionNamePane);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("archive.name")), control.txtName);
|
||||
list.getContent().add(versionNamePane);
|
||||
}
|
||||
|
||||
{
|
||||
VBox libraryPane = new VBox(control.group.getLibraries());
|
||||
list.getContent().add(libraryPane);
|
||||
}
|
||||
|
||||
list.getChildren().addAll(control.group.getLibraries());
|
||||
|
||||
{
|
||||
JFXButton installButton = new JFXButton(i18n("button.install"));
|
||||
|
||||
@@ -20,34 +20,41 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
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.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
@@ -79,6 +86,9 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
@FXML
|
||||
private SpinnerPane spinnerPane;
|
||||
|
||||
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
|
||||
private Charset charset;
|
||||
|
||||
public LocalModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
@@ -93,10 +103,20 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
txtModpackName.setText(name.get());
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
txtModpackName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
FXUtils.onChangeAndOperate(installAsVersion, installAsVersion -> {
|
||||
if (installAsVersion) {
|
||||
txtModpackName.getValidators().setAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
} else {
|
||||
txtModpackName.getValidators().setAll(
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str) && Profiles.getProfiles().stream().noneMatch(p -> p.getName().equals(str))),
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
}
|
||||
});
|
||||
|
||||
btnInstall.disableProperty().bind(
|
||||
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
|
||||
.not());
|
||||
@@ -120,23 +140,46 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
|
||||
spinnerPane.showSpinner();
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
|
||||
.thenApplyAsync(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
|
||||
.whenComplete(Schedulers.javafx(), manifest -> {
|
||||
spinnerPane.hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
.thenApplyAsync(encoding -> {
|
||||
charset = encoding;
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding);
|
||||
return manifest;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
spinnerPane.hideSpinner();
|
||||
lblName.setText(selectedFile.getName());
|
||||
installAsVersion.set(false);
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
if (!name.isPresent()) {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
|
||||
}
|
||||
|
||||
if (!name.isPresent()) {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
Controllers.confirm(i18n("modpack.type.manual.warning"), i18n("install.modpack"), MessageDialogPane.MessageType.WARNING,
|
||||
() -> {},
|
||||
controller::onEnd);
|
||||
|
||||
controller.getSettings().put(MODPACK_MANUALLY_CREATED, true);
|
||||
} else if (exception != null) {
|
||||
LOG.log(Level.WARNING, "Failed to read modpack manifest", exception);
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
} else {
|
||||
spinnerPane.hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
}
|
||||
}
|
||||
}, e -> {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -149,6 +192,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
private void onInstall() {
|
||||
if (!txtModpackName.validate()) return;
|
||||
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.onFinish();
|
||||
}
|
||||
|
||||
@@ -167,4 +211,6 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
public static final String MODPACK_FILE = "MODPACK_FILE";
|
||||
public static final String MODPACK_NAME = "MODPACK_NAME";
|
||||
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
|
||||
public static final String MODPACK_CHARSET = "MODPACK_CHARSET";
|
||||
public static final String MODPACK_MANUALLY_CREATED = "MODPACK_MANUALLY_CREATED";
|
||||
}
|
||||
+12
-3
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
@@ -35,6 +36,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
@@ -72,11 +74,18 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
settings.put(PROFILE, profile);
|
||||
}
|
||||
|
||||
private Task<Void> finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
private Task<?> finishModpackInstallingAsync(Map<String, Object> settings) {
|
||||
File selected = tryCast(settings.get(LocalModpackPage.MODPACK_FILE), File.class).orElse(null);
|
||||
ServerModpackManifest serverModpackManifest = tryCast(settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST), ServerModpackManifest.class).orElse(null);
|
||||
Modpack modpack = tryCast(settings.get(LocalModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
|
||||
String name = tryCast(settings.get(LocalModpackPage.MODPACK_NAME), String.class).orElse(null);
|
||||
Charset charset = tryCast(settings.get(LocalModpackPage.MODPACK_CHARSET), Charset.class).orElse(null);
|
||||
boolean isManuallyCreated = tryCast(settings.get(LocalModpackPage.MODPACK_MANUALLY_CREATED), Boolean.class).orElse(false);
|
||||
|
||||
if (isManuallyCreated) {
|
||||
return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset);
|
||||
}
|
||||
|
||||
if ((selected == null && serverModpackManifest == null) || modpack == null || name == null) return null;
|
||||
|
||||
if (updateVersion != null) {
|
||||
@@ -90,7 +99,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
|
||||
}
|
||||
} catch (UnsupportedModpackException e) {
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
} catch (MismatchedModpackTypeException e) {
|
||||
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
|
||||
@@ -120,7 +129,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
if (exception.getCause() instanceof FileNotFoundException) {
|
||||
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.INFORMATION, next);
|
||||
Controllers.dialog(i18n("install.success"), i18n("install.success"), MessageType.SUCCESS, next);
|
||||
}
|
||||
} else {
|
||||
UpdateInstallerWizardProvider.alertFailureMessage(exception, next);
|
||||
|
||||
+9
-5
@@ -19,10 +19,8 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -157,9 +155,15 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException ||
|
||||
exception instanceof FabricInstallTask.UnsupportedFabricInstallationException) {
|
||||
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof UnsupportedInstallationException) {
|
||||
switch (((UnsupportedInstallationException) exception).getReason()) {
|
||||
case UnsupportedInstallationException.FORGE_1_17_OPTIFINE_H1_PRE2:
|
||||
Controllers.dialog(i18n("install.failed.optifine_forge_1.17"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
break;
|
||||
default:
|
||||
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
break;
|
||||
}
|
||||
} else if (exception instanceof DefaultDependencyManager.UnsupportedLibraryInstallerException) {
|
||||
Controllers.dialog(i18n("install.failed.install_online"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof ArtifactMalformedException || exception instanceof ZipException) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
@@ -279,6 +280,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
@FXML
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/bmclapi_sponsor");
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
@@ -47,12 +43,11 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
@@ -189,10 +184,9 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
});
|
||||
borderPane.setTop(hyperlink);
|
||||
} else {
|
||||
Label label = new Label(i18n("modpack.wizard.step.initialization.warning"));
|
||||
label.setWrapText(true);
|
||||
label.setTextAlignment(TextAlignment.JUSTIFY);
|
||||
borderPane.setTop(label);
|
||||
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
pane.setText(i18n("modpack.wizard.step.initialization.warning"));
|
||||
borderPane.setTop(pane);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -211,93 +205,127 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
{
|
||||
GridPane pane = new GridPane();
|
||||
list.getContent().add(pane);
|
||||
pane.setHgap(16);
|
||||
pane.setVgap(8);
|
||||
pane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
|
||||
|
||||
int rowIndex = 0;
|
||||
|
||||
JFXTextField txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.name);
|
||||
txtModpackName.setLabelFloat(true);
|
||||
txtModpackName.setPromptText(i18n("modpack.name"));
|
||||
txtModpackName.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackName, insets);
|
||||
list.getContent().add(txtModpackName);
|
||||
|
||||
validatingFields.add(txtModpackName);
|
||||
}
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.name")), txtModpackName);
|
||||
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
txtModpackFileApi.setLabelFloat(true);
|
||||
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new Validator(s -> {
|
||||
if (s.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new URL(s).toURI();
|
||||
return true;
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
StackPane.setMargin(txtModpackFileApi, insets);
|
||||
list.getContent().add(txtModpackFileApi);
|
||||
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
}
|
||||
|
||||
{
|
||||
JFXTextField txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
|
||||
txtModpackAuthor.setLabelFloat(true);
|
||||
txtModpackAuthor.setPromptText(i18n("archive.author"));
|
||||
txtModpackAuthor.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackAuthor, insets);
|
||||
list.getContent().add(txtModpackAuthor);
|
||||
|
||||
validatingFields.add(txtModpackAuthor);
|
||||
}
|
||||
pane.addRow(rowIndex++, new Label(i18n("archive.author")), txtModpackAuthor);
|
||||
|
||||
{
|
||||
JFXTextField txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
|
||||
txtModpackVersion.setLabelFloat(true);
|
||||
txtModpackVersion.setPromptText(i18n("archive.version"));
|
||||
txtModpackVersion.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackVersion, insets);
|
||||
list.getContent().add(txtModpackVersion);
|
||||
|
||||
validatingFields.add(txtModpackVersion);
|
||||
pane.addRow(rowIndex++, new Label(i18n("archive.version")), txtModpackVersion);
|
||||
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new URLValidator(true));
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
JFXTextField txtLaunchArguments = new JFXTextField();
|
||||
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
|
||||
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.minecraft_arguments")), txtLaunchArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
JFXTextField txtJavaArguments = new JFXTextField();
|
||||
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
|
||||
pane.addRow(rowIndex++, new Label(i18n("settings.advanced.jvm_args")), txtJavaArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
JFXTextField txtModpackUrl = new JFXTextField();
|
||||
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.url")), txtModpackUrl);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
JFXTextField txtMcbbs = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
|
||||
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
|
||||
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
|
||||
validatingFields.add(txtMcbbs);
|
||||
pane.addRow(rowIndex++, new Label(i18n("modpack.origin.mcbbs")), txtMcbbs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireMinMemory()) {
|
||||
VBox pane = new VBox();
|
||||
|
||||
Label title = new Label(i18n("settings.memory"));
|
||||
VBox.setMargin(title, new Insets(0, 0, 8, 0));
|
||||
|
||||
HBox lowerBoundPane = new HBox(8);
|
||||
lowerBoundPane.setAlignment(Pos.CENTER);
|
||||
VBox.setMargin(lowerBoundPane, new Insets(0, 0, 0, 16));
|
||||
|
||||
{
|
||||
Label label = new Label(i18n("settings.memory.lower_bound"));
|
||||
|
||||
JFXSlider slider = new JFXSlider(0, 1, 0);
|
||||
HBox.setMargin(slider, new Insets(0, 0, 0, 8));
|
||||
HBox.setHgrow(slider, Priority.ALWAYS);
|
||||
slider.setValueFactory(self -> Bindings.createStringBinding(() -> (int) (self.getValue() * 100) + "%", self.valueProperty()));
|
||||
AtomicBoolean changedByTextField = new AtomicBoolean(false);
|
||||
FXUtils.onChangeAndOperate(skinnable.minMemory, minMemory -> {
|
||||
changedByTextField.set(true);
|
||||
slider.setValue(minMemory.intValue() * 1.0 / OperatingSystem.TOTAL_MEMORY);
|
||||
changedByTextField.set(false);
|
||||
});
|
||||
slider.valueProperty().addListener((value, oldVal, newVal) -> {
|
||||
if (changedByTextField.get()) return;
|
||||
skinnable.minMemory.set((int) (value.getValue().doubleValue() * OperatingSystem.TOTAL_MEMORY));
|
||||
});
|
||||
|
||||
JFXTextField txtMinMemory = new JFXTextField();
|
||||
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
|
||||
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
|
||||
FXUtils.setLimitWidth(txtMinMemory, 60);
|
||||
validatingFields.add(txtMinMemory);
|
||||
|
||||
lowerBoundPane.getChildren().setAll(label, slider, txtMinMemory, new Label("MB"));
|
||||
}
|
||||
|
||||
pane.getChildren().setAll(title, lowerBoundPane);
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
{
|
||||
VBox pane = new VBox(8);
|
||||
JFXTextArea area = new JFXTextArea();
|
||||
area.textProperty().bindBidirectional(skinnable.description);
|
||||
area.setLabelFloat(true);
|
||||
area.setPromptText(i18n("modpack.desc"));
|
||||
area.setMinHeight(400);
|
||||
StackPane.setMargin(area, insets);
|
||||
list.getContent().add(area);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireForceUpdate()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
|
||||
pane.getChildren().setAll(new Label(i18n("modpack.desc")), area);
|
||||
list.getContent().add(pane);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireAuthlibInjectorServer()) {
|
||||
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
|
||||
cboServers.setMaxWidth(Double.MAX_VALUE);
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
|
||||
@@ -317,75 +345,17 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireMinMemory()) {
|
||||
JFXTextField txtMinMemory = new JFXTextField();
|
||||
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
|
||||
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
|
||||
FXUtils.setLimitWidth(txtMinMemory, 300);
|
||||
validatingFields.add(txtMinMemory);
|
||||
|
||||
if (skinnable.options.isRequireForceUpdate()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
Label label = new Label(i18n("settings.min_memory"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
pane.setLeft(label);
|
||||
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
|
||||
pane.setRight(txtMinMemory);
|
||||
|
||||
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
|
||||
list.getContent().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireLaunchArguments()) {
|
||||
JFXTextField txtLaunchArguments = new JFXTextField();
|
||||
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
|
||||
txtLaunchArguments.setLabelFloat(true);
|
||||
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
|
||||
StackPane.setMargin(txtLaunchArguments, insets);
|
||||
list.getContent().add(txtLaunchArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireJavaArguments()) {
|
||||
JFXTextField txtJavaArguments = new JFXTextField();
|
||||
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
|
||||
txtJavaArguments.setLabelFloat(true);
|
||||
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
|
||||
StackPane.setMargin(txtJavaArguments, insets);
|
||||
list.getContent().add(txtJavaArguments);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
|
||||
BorderPane originPane = new BorderPane();
|
||||
Label title = new Label(i18n("modpack.origin"));
|
||||
originPane.setTop(title);
|
||||
VBox container = new VBox();
|
||||
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
|
||||
originPane.setCenter(container);
|
||||
list.getContent().add(originPane);
|
||||
|
||||
if (skinnable.options.isRequireUrl()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
Label left = new Label(i18n("modpack.origin.url"));
|
||||
pane.setLeft(left);
|
||||
JFXTextField txtModpackUrl = new JFXTextField();
|
||||
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
|
||||
pane.setRight(txtModpackUrl);
|
||||
container.getChildren().add(pane);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireOrigins()) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8, 0, 8, 0));
|
||||
Label left = new Label(i18n("modpack.origin.mcbbs"));
|
||||
pane.setLeft(left);
|
||||
JFXTextField txtMcbbs = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
|
||||
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
|
||||
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
|
||||
pane.setRight(txtMcbbs);
|
||||
container.getChildren().add(pane);
|
||||
|
||||
validatingFields.add(txtMcbbs);
|
||||
}
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -25,7 +26,6 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -43,8 +43,8 @@ public class AboutPage extends StackPane {
|
||||
IconedTwoLineListItem author = new IconedTwoLineListItem();
|
||||
author.setImage(new Image("/assets/img/yellow_fish.jpg", 32, 32, false, true));
|
||||
author.setTitle("huanghongxun");
|
||||
author.setSubtitle("https://www.huangyuhui.net");
|
||||
author.setExternalLink("https://www.huangyuhui.net");
|
||||
author.setSubtitle(i18n("about.author.statement"));
|
||||
author.setExternalLink("https://space.bilibili.com/1445341");
|
||||
|
||||
about.getContent().setAll(launcher, author);
|
||||
}
|
||||
@@ -63,10 +63,16 @@ public class AboutPage extends StackPane {
|
||||
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
|
||||
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
|
||||
|
||||
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
|
||||
gamerteam.setTitle("gamerteam");
|
||||
gamerteam.setImage(new Image("/assets/img/gamerteam.jpg", 32, 32, false, true));
|
||||
gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
|
||||
gamerteam.setExternalLink("http://www.zhaisoul.com/");
|
||||
|
||||
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
|
||||
redLnn.setTitle("Red_lnn");
|
||||
redLnn.setImage(new Image("/assets/img/red_lnn.jpg", 32, 32, false, true));
|
||||
redLnn.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
|
||||
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
|
||||
|
||||
IconedTwoLineListItem mcbbs = new IconedTwoLineListItem();
|
||||
mcbbs.setImage(new Image("/assets/img/chest.png", 32, 32, false, true));
|
||||
@@ -80,19 +86,42 @@ public class AboutPage extends StackPane {
|
||||
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
|
||||
mcmod.setExternalLink("https://www.mcmod.cn/");
|
||||
|
||||
IconedTwoLineListItem noin = new IconedTwoLineListItem();
|
||||
noin.setImage(new Image("/assets/img/noin.png", 32, 32, false, true));
|
||||
noin.setTitle(i18n("about.thanks_to.noin"));
|
||||
noin.setSubtitle(i18n("about.thanks_to.noin.statement"));
|
||||
noin.setExternalLink("https://noin.cn/");
|
||||
|
||||
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
|
||||
contributors.setImage(new Image("/assets/img/github.png", 32, 32, false, true));
|
||||
contributors.setTitle(i18n("about.thanks_to.contributors"));
|
||||
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
|
||||
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
|
||||
|
||||
thanks.getContent().setAll(yushijinhun, bangbang93, mcbbs, mcmod, noin, gamerteam, redLnn, contributors);
|
||||
}
|
||||
|
||||
ComponentList community = new ComponentList();
|
||||
{
|
||||
IconedTwoLineListItem users = new IconedTwoLineListItem();
|
||||
users.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
|
||||
users.setTitle(i18n("about.thanks_to.users"));
|
||||
users.setSubtitle(i18n("about.thanks_to.users.statement"));
|
||||
users.setExternalLink("https://hmcl.huangyuhui.net/api/redirect/sponsor");
|
||||
|
||||
thanks.getContent().setAll(yushijinhun, bangbang93, mcbbs, mcmod, redLnn, users, contributors);
|
||||
IconedTwoLineListItem discord = new IconedTwoLineListItem();
|
||||
discord.setImage(new Image("/assets/img/discord.png", 32, 32, false, true));
|
||||
discord.setTitle(i18n("about.community.discord"));
|
||||
discord.setSubtitle(i18n("about.community.discord.statement"));
|
||||
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
||||
|
||||
IconedTwoLineListItem kaiheila = new IconedTwoLineListItem();
|
||||
kaiheila.setImage(new Image("/assets/img/kaiheila.jpeg", 32, 32, false, true));
|
||||
kaiheila.setTitle(i18n("about.community.kaiheila"));
|
||||
kaiheila.setSubtitle(i18n("about.community.kaiheila.statement"));
|
||||
kaiheila.setExternalLink("https://kaihei.co/Kx7n3t");
|
||||
|
||||
community.getContent().setAll(users, discord, kaiheila);
|
||||
}
|
||||
|
||||
ComponentList dep = new ComponentList();
|
||||
@@ -132,22 +161,35 @@ public class AboutPage extends StackPane {
|
||||
openNBT.setSubtitle("Copyright (C) 2013-2021 Steveice10.\nLicensed under the MIT License.");
|
||||
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
|
||||
|
||||
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT);
|
||||
IconedTwoLineListItem jstun = new IconedTwoLineListItem();
|
||||
jstun.setTitle("JSTUN");
|
||||
jstun.setSubtitle("Copyright (c) 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
|
||||
jstun.setExternalLink("https://github.com/tking/JSTUN");
|
||||
|
||||
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
|
||||
jstun.setTitle("minecraft-jfx-skin");
|
||||
jstun.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
|
||||
jstun.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
|
||||
|
||||
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
|
||||
}
|
||||
|
||||
ComponentList legal = new ComponentList();
|
||||
{
|
||||
TwoLineListItem copyright = new TwoLineListItem();
|
||||
IconedTwoLineListItem copyright = new IconedTwoLineListItem();
|
||||
copyright.setTitle(i18n("about.copyright"));
|
||||
copyright.setSubtitle(i18n("about.copyright.statement"));
|
||||
copyright.setExternalLink("https://hmcl.huangyuhui.net/about/");
|
||||
|
||||
TwoLineListItem claim = new TwoLineListItem();
|
||||
IconedTwoLineListItem claim = new IconedTwoLineListItem();
|
||||
claim.setTitle(i18n("about.claim"));
|
||||
claim.setSubtitle(i18n("about.claim.statement"));
|
||||
claim.setExternalLink(Metadata.EULA_URL);
|
||||
|
||||
TwoLineListItem openSource = new TwoLineListItem();
|
||||
IconedTwoLineListItem openSource = new IconedTwoLineListItem();
|
||||
openSource.setTitle(i18n("about.open_source"));
|
||||
openSource.setSubtitle(i18n("about.open_source.statement"));
|
||||
openSource.setExternalLink("https://github.com/huanghongxun/HMCL");
|
||||
|
||||
legal.getContent().setAll(copyright, claim, openSource);
|
||||
}
|
||||
@@ -161,6 +203,9 @@ public class AboutPage extends StackPane {
|
||||
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
|
||||
thanks,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.community")),
|
||||
community,
|
||||
|
||||
ComponentList.createComponentListTitle(i18n("about.dependency")),
|
||||
dep,
|
||||
|
||||
@@ -169,9 +214,9 @@ public class AboutPage extends StackPane {
|
||||
);
|
||||
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setContent(content);
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
getChildren().setAll(scrollPane);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ public class DownloadSettingsPage extends StackPane {
|
||||
content.setPadding(new Insets(10));
|
||||
content.setFillWidth(true);
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
@@ -153,7 +154,7 @@ public class DownloadSettingsPage extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
|
||||
hintPane.disableProperty().bind(config().autoDownloadThreadsProperty());
|
||||
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
|
||||
|
||||
@@ -18,17 +18,22 @@
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.HMCLAccounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -36,13 +41,17 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
@@ -50,8 +59,7 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class FeedbackPage extends VBox {
|
||||
private final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
||||
public class FeedbackPage extends VBox implements PageAware {
|
||||
private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList();
|
||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||
|
||||
@@ -66,11 +74,14 @@ public class FeedbackPage extends VBox {
|
||||
|
||||
TwoLineListItem accountInfo = new TwoLineListItem();
|
||||
HBox.setHgrow(accountInfo, Priority.ALWAYS);
|
||||
accountInfo.titleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
|
||||
accountInfo.subtitleProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
|
||||
accountInfo.titleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
|
||||
.map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname()));
|
||||
accountInfo.subtitleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
|
||||
.map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail()));
|
||||
|
||||
JFXButton logButton = new JFXButton();
|
||||
logButton.textProperty().bind(BindingMapping.of(account).map(account -> account == null ? i18n("account.login") : i18n("account.logout")));
|
||||
logButton.textProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty())
|
||||
.map(account -> account == null ? i18n("account.login") : i18n("account.logout")));
|
||||
logButton.setOnAction(e -> log());
|
||||
|
||||
loginPane.getChildren().setAll(accountInfo, logButton);
|
||||
@@ -83,16 +94,21 @@ public class FeedbackPage extends VBox {
|
||||
getChildren().add(searchPane);
|
||||
|
||||
JFXTextField searchField = new JFXTextField();
|
||||
searchField.setOnAction(e -> search(searchField.getText()));
|
||||
searchField.setOnAction(e -> search(searchField.getText(), "time", true));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchField.setPromptText(i18n("search"));
|
||||
|
||||
JFXButton searchButton = new JFXButton();
|
||||
searchButton.getStyleClass().add("toggle-icon4");
|
||||
searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1));
|
||||
searchButton.setOnAction(e -> addFeedback());
|
||||
searchButton.setOnAction(e -> search(searchField.getText(), "time", true));
|
||||
|
||||
searchPane.getChildren().setAll(searchField, searchButton);
|
||||
JFXButton addButton = new JFXButton();
|
||||
addButton.getStyleClass().add("toggle-icon4");
|
||||
addButton.setGraphic(SVG.plus(Theme.blackFillBinding(), -1, -1));
|
||||
addButton.setOnAction(e -> addFeedback());
|
||||
|
||||
searchPane.getChildren().setAll(searchField, searchButton, addButton);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -105,9 +121,10 @@ public class FeedbackPage extends VBox {
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton likeButton = new JFXButton();
|
||||
private final JFXButton unlikeButton = new JFXButton();
|
||||
private final HBox container;
|
||||
|
||||
{
|
||||
HBox container = new HBox(8);
|
||||
container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
@@ -115,10 +132,10 @@ public class FeedbackPage extends VBox {
|
||||
setSelectable();
|
||||
|
||||
likeButton.getStyleClass().add("toggle-icon4");
|
||||
likeButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
likeButton.setGraphic(FXUtils.limitingSize(SVG.thumbUpOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
unlikeButton.getStyleClass().add("toggle-icon4");
|
||||
unlikeButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
unlikeButton.setGraphic(FXUtils.limitingSize(SVG.thumbDownOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||
|
||||
container.getChildren().setAll(content, likeButton, unlikeButton);
|
||||
|
||||
@@ -128,31 +145,61 @@ public class FeedbackPage extends VBox {
|
||||
|
||||
@Override
|
||||
protected void updateControl(FeedbackResponse feedback, boolean empty) {
|
||||
if (empty) return;
|
||||
content.setTitle(feedback.getTitle());
|
||||
content.setSubtitle(feedback.getContent());
|
||||
content.getTags().add("#" + feedback.getId());
|
||||
content.getTags().add(feedback.getAuthor());
|
||||
content.getTags().add(feedback.getLauncherVersion());
|
||||
content.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
|
||||
content.setSubtitle(feedback.getAuthor());
|
||||
content.getTags().setAll(
|
||||
"#" + feedback.getId(),
|
||||
i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)),
|
||||
i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US)));
|
||||
container.setOnMouseClicked(e -> {
|
||||
getFeedback(feedback.getId())
|
||||
.thenAcceptAsync(Schedulers.javafx(), f -> {
|
||||
Controllers.dialog(new ViewFeedbackDialog(f));
|
||||
})
|
||||
.start();
|
||||
});
|
||||
}
|
||||
});
|
||||
listView.setOnMouseClicked(e -> {
|
||||
if (listView.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
FeedbackResponse selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
Controllers.dialog(new ViewFeedbackDialog(selectedItem));
|
||||
});
|
||||
|
||||
getChildren().add(spinnerPane);
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String keyword) {
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
search("", "time", false);
|
||||
}
|
||||
|
||||
private void search(String keyword, String order, boolean showAll) {
|
||||
HMCLAccounts.HMCLAccount account = HMCLAccounts.getAccount();
|
||||
Task.supplyAsync(() -> {
|
||||
return HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback", pair("s", keyword)).<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
|
||||
}).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
|
||||
Map<String, String> query = mapOf(
|
||||
pair("keyword", keyword),
|
||||
pair("order", order)
|
||||
);
|
||||
if (showAll) {
|
||||
query.put("showAll", "1");
|
||||
}
|
||||
HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query));
|
||||
if (account != null) {
|
||||
req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
|
||||
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider());
|
||||
}
|
||||
return req.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
spinnerPane.hideSpinner();
|
||||
if (exception != null) {
|
||||
if (exception instanceof ResponseCodeException) {
|
||||
int responseCode = ((ResponseCodeException) exception).getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
spinnerPane.setFailedReason(i18n("feedback.failed.permission"));
|
||||
return;
|
||||
} else if (responseCode == 429) {
|
||||
spinnerPane.setFailedReason(i18n("feedback.failed.too_frequently"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
spinnerPane.setFailedReason(i18n("feedback.failed"));
|
||||
} else {
|
||||
feedbacks.setAll(result);
|
||||
@@ -160,18 +207,22 @@ public class FeedbackPage extends VBox {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private Task<FeedbackResponse> getFeedback(int id) {
|
||||
return Task.supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback/" + id).getJson(FeedbackResponse.class));
|
||||
}
|
||||
|
||||
private void log() {
|
||||
if (account.get() == null) {
|
||||
if (HMCLAccounts.getAccount() == null) {
|
||||
// login
|
||||
Controllers.dialog(new LoginDialog());
|
||||
} else {
|
||||
// logout
|
||||
account.set(null);
|
||||
HMCLAccounts.setAccount(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void addFeedback() {
|
||||
if (account.get() == null) {
|
||||
if (HMCLAccounts.getAccount() == null) {
|
||||
Controllers.dialog(i18n("feedback.add.login"));
|
||||
return;
|
||||
}
|
||||
@@ -179,143 +230,71 @@ public class FeedbackPage extends VBox {
|
||||
Controllers.dialog(new AddFeedbackDialog());
|
||||
}
|
||||
|
||||
private static class HMCLAccount {
|
||||
private final String nickname;
|
||||
private final String email;
|
||||
private final String accessToken;
|
||||
|
||||
public HMCLAccount(String nickname, String email, String accessToken) {
|
||||
this.nickname = nickname;
|
||||
this.email = email;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class HMCLLoginResponse {
|
||||
private final int err;
|
||||
private final String nickname;
|
||||
private final String email;
|
||||
private final String accessToken;
|
||||
|
||||
public HMCLLoginResponse(int err, String nickname, String email, String accessToken) {
|
||||
this.err = err;
|
||||
this.nickname = nickname;
|
||||
this.email = email;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public int getErr() {
|
||||
return err;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public static final int ERR_OK = 0;
|
||||
public static final int ERR_WRONG = 400;
|
||||
}
|
||||
|
||||
private class LoginDialog extends JFXDialogLayout {
|
||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||
private final Label errorLabel = new Label();
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
|
||||
public LoginDialog() {
|
||||
setHeading(new Label(i18n("feedback.login")));
|
||||
|
||||
GridPane body = new GridPane();
|
||||
ColumnConstraints fieldColumn = new ColumnConstraints();
|
||||
fieldColumn.setFillWidth(true);
|
||||
body.getColumnConstraints().setAll(new ColumnConstraints(), fieldColumn);
|
||||
body.setVgap(8);
|
||||
body.setHgap(8);
|
||||
setBody(body);
|
||||
|
||||
JFXTextField usernameField = new JFXTextField();
|
||||
usernameField.setValidators(new RequiredValidator());
|
||||
body.addRow(0, new Label(i18n("account.username")), usernameField);
|
||||
|
||||
JFXPasswordField passwordField = new JFXPasswordField();
|
||||
passwordField.setValidators(new RequiredValidator());
|
||||
body.addRow(1, new Label(i18n("account.password")), passwordField);
|
||||
|
||||
JFXButton registerButton = new JFXButton();
|
||||
registerButton.setText(i18n("account.register"));
|
||||
registerButton.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/user/login"));
|
||||
VBox vbox = new VBox(8);
|
||||
setBody(vbox);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
|
||||
}
|
||||
});
|
||||
vbox.getChildren().setAll(hintPane);
|
||||
|
||||
JFXButton loginButton = new JFXButton();
|
||||
spinnerPane.setContent(loginButton);
|
||||
loginButton.setText(i18n("account.login"));
|
||||
loginButton.setOnAction(e -> login(usernameField.getText(), passwordField.getText()));
|
||||
loginButton.setOnAction(e -> login());
|
||||
|
||||
JFXButton cancelButton = new JFXButton();
|
||||
cancelButton.setText(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
setActions(errorLabel, registerButton, spinnerPane, cancelButton);
|
||||
setActions(errorLabel, spinnerPane, cancelButton);
|
||||
}
|
||||
|
||||
private void login(String username, String password) {
|
||||
private void login() {
|
||||
spinnerPane.showSpinner();
|
||||
errorLabel.setText("");
|
||||
Task.supplyAsync(() -> {
|
||||
return HttpRequest.POST("https://hmcl.huangyuhui.net/api/user/login")
|
||||
.json(mapOf(
|
||||
pair("username", username),
|
||||
pair("password", password)
|
||||
)).getJson(HMCLLoginResponse.class);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
logging.set(true);
|
||||
|
||||
HMCLAccounts.login().whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
logging.set(false);
|
||||
if (exception != null) {
|
||||
if (exception instanceof IOException) {
|
||||
if (exception instanceof ResponseCodeException && ((ResponseCodeException) exception).getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) {
|
||||
errorLabel.setText(i18n("account.failed.invalid_password"));
|
||||
} else {
|
||||
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
|
||||
}
|
||||
errorLabel.setText(i18n("account.failed.connect_authentication_server"));
|
||||
} else if (exception instanceof JsonParseException) {
|
||||
errorLabel.setText(i18n("account.failed.server_response_malformed"));
|
||||
} else {
|
||||
errorLabel.setText(exception.getClass().getName() + ": " + exception.getLocalizedMessage());
|
||||
errorLabel.setText(Accounts.localizeErrorMessage(exception));
|
||||
}
|
||||
} else {
|
||||
if (result.err == HMCLLoginResponse.ERR_OK) {
|
||||
account.setValue(new HMCLAccount(result.getNickname(), result.getEmail(), result.getAccessToken()));
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else if (result.err == HMCLLoginResponse.ERR_WRONG) {
|
||||
errorLabel.setText(i18n("account.failed.invalid_password"));
|
||||
} else {
|
||||
errorLabel.setText(i18n("account.failed", result.err));
|
||||
}
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddFeedbackDialog extends JFXDialogLayout {
|
||||
private static class AddFeedbackDialog extends DialogPane {
|
||||
|
||||
JFXTextField titleField = new JFXTextField();
|
||||
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
|
||||
JFXTextArea contentArea = new JFXTextArea();
|
||||
|
||||
public AddFeedbackDialog() {
|
||||
setHeading(new Label(i18n("feedback.add")));
|
||||
setTitle(i18n("feedback.add"));
|
||||
|
||||
GridPane body = new GridPane();
|
||||
body.setVgap(8);
|
||||
@@ -326,16 +305,14 @@ public class FeedbackPage extends VBox {
|
||||
searchHintPane.setText(i18n("feedback.add.hint.search_before_add"));
|
||||
body.addRow(0, searchHintPane);
|
||||
|
||||
HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFORMATION);
|
||||
HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
GridPane.setColumnSpan(titleHintPane, 2);
|
||||
titleHintPane.setText(i18n("feedback.add.hint.title"));
|
||||
body.addRow(1, titleHintPane);
|
||||
|
||||
JFXTextField titleField = new JFXTextField();
|
||||
titleField.setValidators(new RequiredValidator());
|
||||
body.addRow(2, new Label(i18n("feedback.title")), titleField);
|
||||
|
||||
JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>();
|
||||
comboBox.setMaxWidth(-1);
|
||||
comboBox.getItems().setAll(FeedbackType.values());
|
||||
comboBox.getSelectionModel().select(0);
|
||||
@@ -346,29 +323,46 @@ public class FeedbackPage extends VBox {
|
||||
GridPane.setColumnSpan(contentLabel, 2);
|
||||
body.addRow(4, contentLabel);
|
||||
|
||||
JFXTextArea contentArea = new JFXTextArea();
|
||||
contentArea.setValidators(new RequiredValidator());
|
||||
contentArea.setPromptText(i18n("feedback.add.hint.content"));
|
||||
GridPane.setColumnSpan(contentArea, 2);
|
||||
body.addRow(5, contentArea);
|
||||
|
||||
validProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
return titleField.validate() && contentArea.validate();
|
||||
}, titleField.textProperty(), contentArea.textProperty()));
|
||||
|
||||
setBody(body);
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.setText(i18n("button.ok"));
|
||||
okButton.setOnAction(e -> addFeedback(titleField.getText(), comboBox.getSelectionModel().getSelectedItem(), contentArea.getText()));
|
||||
|
||||
JFXButton cancelButton = new JFXButton();
|
||||
cancelButton.setText(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
setActions(okButton, cancelButton);
|
||||
}
|
||||
|
||||
private void addFeedback(String title, FeedbackType feedbackType, String content) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
// TODO
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
|
||||
addFeedback(titleField.getText(), comboBox.getValue(), contentArea.getText())
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
onFailure(exception.getLocalizedMessage());
|
||||
} else {
|
||||
onSuccess();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
private Task<?> addFeedback(String title, FeedbackType feedbackType, String content) {
|
||||
return Task.runAsync(() -> {
|
||||
HttpRequest.POST("https://hmcl.huangyuhui.net/api/feedback")
|
||||
.json(mapOf(
|
||||
pair("title", title),
|
||||
pair("content", content),
|
||||
pair("type", feedbackType.name().toLowerCase(Locale.ROOT)),
|
||||
pair("launcher_version", Metadata.VERSION)
|
||||
))
|
||||
.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
|
||||
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider())
|
||||
.getString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,9 +371,11 @@ public class FeedbackPage extends VBox {
|
||||
public ViewFeedbackDialog(FeedbackResponse feedback) {
|
||||
BorderPane heading = new BorderPane();
|
||||
TwoLineListItem left = new TwoLineListItem();
|
||||
heading.setLeft(left);
|
||||
left.setTitle(feedback.getTitle());
|
||||
left.setSubtitle(feedback.getAuthor());
|
||||
left.getTags().add("#" + feedback.getId());
|
||||
left.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)));
|
||||
left.getTags().add(feedback.getLauncherVersion());
|
||||
left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase()));
|
||||
|
||||
@@ -387,7 +383,17 @@ public class FeedbackPage extends VBox {
|
||||
|
||||
Label content = new Label(feedback.getContent());
|
||||
content.setWrapText(true);
|
||||
setBody(content);
|
||||
|
||||
TwoLineListItem response = new TwoLineListItem();
|
||||
response.getStyleClass().setAll("two-line-item-second-large");
|
||||
response.setTitle(i18n("feedback.response"));
|
||||
response.setSubtitle(StringUtils.isBlank(feedback.getReason())
|
||||
? i18n("feedback.response.empty")
|
||||
: feedback.getReason());
|
||||
|
||||
VBox body = new VBox(content, response);
|
||||
body.setSpacing(8);
|
||||
setBody(body);
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.setText(i18n("button.ok"));
|
||||
@@ -402,18 +408,21 @@ public class FeedbackPage extends VBox {
|
||||
private final String title;
|
||||
private final String content;
|
||||
private final String author;
|
||||
@SerializedName("launcher_version")
|
||||
private final String launcherVersion;
|
||||
private final String gameVersion;
|
||||
private final FeedbackType type;
|
||||
private final FeedbackState state;
|
||||
private final String reason;
|
||||
|
||||
public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, String gameVersion, FeedbackType type) {
|
||||
public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, FeedbackType type, FeedbackState state, String reason) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.author = author;
|
||||
this.launcherVersion = launcherVersion;
|
||||
this.gameVersion = gameVersion;
|
||||
this.type = type;
|
||||
this.state = state;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
@@ -436,17 +445,27 @@ public class FeedbackPage extends VBox {
|
||||
return launcherVersion;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
public FeedbackType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public FeedbackState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
private enum FeedbackType {
|
||||
FEATURE_REQUEST,
|
||||
BUG_REPORT
|
||||
FEATURE,
|
||||
BUG
|
||||
}
|
||||
|
||||
private enum FeedbackState {
|
||||
OPEN,
|
||||
REJECTED,
|
||||
ACCEPTED
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -43,7 +45,10 @@ public class HelpPage extends SpinnerPane {
|
||||
content.setPadding(new Insets(10));
|
||||
content.setSpacing(10);
|
||||
content.setFillWidth(true);
|
||||
setContent(content);
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
setContent(scrollPane);
|
||||
|
||||
IconedTwoLineListItem docPane = new IconedTwoLineListItem();
|
||||
docPane.setTitle(i18n("help.doc"));
|
||||
|
||||
@@ -19,24 +19,24 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
|
||||
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings")));
|
||||
private final TabHeader tab;
|
||||
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
|
||||
private final TabHeader.Tab<SettingsPage> settingsTab = new TabHeader.Tab<>("settingsPage");
|
||||
@@ -49,7 +49,7 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
public LauncherSettingsPage() {
|
||||
gameTab.setNodeSupplier(VersionSettingsPage::new);
|
||||
gameTab.setNodeSupplier(() -> new VersionSettingsPage(true));
|
||||
settingsTab.setNodeSupplier(SettingsPage::new);
|
||||
personalizationTab.setNodeSupplier(PersonalizationPage::new);
|
||||
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
|
||||
@@ -57,13 +57,12 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
|
||||
feedbackTab.setNodeSupplier(FeedbackPage::new);
|
||||
sponsorTab.setNodeSupplier(SponsorPage::new);
|
||||
aboutTab.setNodeSupplier(AboutPage::new);
|
||||
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, sponsorTab, aboutTab);
|
||||
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab);
|
||||
|
||||
tab.getSelectionModel().select(gameTab);
|
||||
tab.select(gameTab);
|
||||
gameTab.initializeIfNeeded();
|
||||
gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null);
|
||||
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
|
||||
newValue.initializeIfNeeded();
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
|
||||
});
|
||||
|
||||
@@ -71,53 +70,53 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("settings.type.global.manage"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.gamepad(Theme.blackFillBinding(), 24, 24)));
|
||||
settingsItem.setLeftGraphic(wrap(SVG::gamepad));
|
||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(gameTab));
|
||||
settingsItem.setOnAction(e -> tab.getSelectionModel().select(gameTab));
|
||||
settingsItem.setOnAction(e -> tab.select(gameTab));
|
||||
})
|
||||
.startCategory(i18n("launcher"))
|
||||
.addNavigationDrawerItem(settingsItem -> {
|
||||
settingsItem.setTitle(i18n("settings.launcher.general"));
|
||||
settingsItem.setLeftGraphic(wrap(SVG.applicationOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
settingsItem.setLeftGraphic(wrap(SVG::applicationOutline));
|
||||
settingsItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(settingsTab));
|
||||
settingsItem.setOnAction(e -> tab.getSelectionModel().select(settingsTab));
|
||||
settingsItem.setOnAction(e -> tab.select(settingsTab));
|
||||
})
|
||||
.addNavigationDrawerItem(personalizationItem -> {
|
||||
personalizationItem.setTitle(i18n("settings.launcher.appearance"));
|
||||
personalizationItem.setLeftGraphic(wrap(SVG.styleOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
personalizationItem.setLeftGraphic(wrap(SVG::styleOutline));
|
||||
personalizationItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(personalizationTab));
|
||||
personalizationItem.setOnAction(e -> tab.getSelectionModel().select(personalizationTab));
|
||||
personalizationItem.setOnAction(e -> tab.select(personalizationTab));
|
||||
})
|
||||
.addNavigationDrawerItem(downloadItem -> {
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setLeftGraphic(wrap(SVG.downloadOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
|
||||
downloadItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(downloadTab));
|
||||
downloadItem.setOnAction(e -> tab.getSelectionModel().select(downloadTab));
|
||||
downloadItem.setOnAction(e -> tab.select(downloadTab));
|
||||
})
|
||||
.startCategory(i18n("help"))
|
||||
.addNavigationDrawerItem(helpItem -> {
|
||||
helpItem.setTitle(i18n("help"));
|
||||
helpItem.setLeftGraphic(wrap(SVG.helpCircleOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
helpItem.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab));
|
||||
helpItem.setOnAction(e -> tab.getSelectionModel().select(helpTab));
|
||||
helpItem.setOnAction(e -> tab.select(helpTab));
|
||||
})
|
||||
.addNavigationDrawerItem(feedbackItem -> {
|
||||
feedbackItem.setTitle(i18n("feedback"));
|
||||
feedbackItem.setLeftGraphic(wrap(SVG.messageAlertOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
||||
feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab));
|
||||
feedbackItem.setOnAction(e -> tab.getSelectionModel().select(feedbackTab));
|
||||
feedbackItem.setOnAction(e -> tab.select(feedbackTab));
|
||||
})
|
||||
.addNavigationDrawerItem(sponsorItem -> {
|
||||
sponsorItem.setTitle(i18n("sponsor"));
|
||||
sponsorItem.setLeftGraphic(wrap(SVG.handHearOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline));
|
||||
sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab));
|
||||
sponsorItem.setOnAction(e -> tab.getSelectionModel().select(sponsorTab));
|
||||
sponsorItem.setOnAction(e -> tab.select(sponsorTab));
|
||||
})
|
||||
.addNavigationDrawerItem(aboutItem -> {
|
||||
aboutItem.setTitle(i18n("about"));
|
||||
aboutItem.setLeftGraphic(wrap(SVG.informationOutline(Theme.blackFillBinding(), 24, 24)));
|
||||
aboutItem.setLeftGraphic(wrap(SVG::informationOutline));
|
||||
aboutItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(aboutTab));
|
||||
aboutItem.setOnAction(e -> tab.getSelectionModel().select(aboutTab));
|
||||
aboutItem.setOnAction(e -> tab.select(aboutTab));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -126,9 +125,23 @@ public class LauncherSettingsPage extends BorderPane implements DecoratorPage {
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
tab.onPageShown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
tab.onPageHidden();
|
||||
}
|
||||
|
||||
public void showGameSettings(Profile profile) {
|
||||
gameTab.getNode().loadVersion(profile, null);
|
||||
tab.getSelectionModel().select(gameTab);
|
||||
tab.select(gameTab);
|
||||
}
|
||||
|
||||
public void showFeedback() {
|
||||
tab.select(feedbackTab);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AnnouncementCard;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -73,8 +74,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private final ObservableList<Node> versionNodes;
|
||||
private Profile profile;
|
||||
|
||||
private StackPane updatePane;
|
||||
private JFXButton menuButton;
|
||||
private final VBox announcementPane;
|
||||
private final StackPane updatePane;
|
||||
private final JFXButton menuButton;
|
||||
|
||||
{
|
||||
HBox titleNode = new HBox(8);
|
||||
@@ -88,10 +90,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleNode.getChildren().setAll(titleIcon, titleLabel);
|
||||
|
||||
state.setValue(State.fromTitleNode(titleNode));
|
||||
state.setValue(new State(null, titleNode, false, false, true));
|
||||
|
||||
setPadding(new Insets(20));
|
||||
|
||||
announcementPane = new VBox(16);
|
||||
|
||||
updatePane = new StackPane();
|
||||
updatePane.setVisible(false);
|
||||
updatePane.getStyleClass().add("bubble");
|
||||
@@ -198,7 +202,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchPane.getChildren().setAll(launchButton, separator, menuButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(updatePane, launchPane);
|
||||
getChildren().setAll(announcementPane, updatePane, launchPane);
|
||||
|
||||
menu.setMaxHeight(365);
|
||||
menu.setMaxWidth(545);
|
||||
@@ -212,6 +216,14 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
Bindings.bindContent(menu.getContent(), versionNodes);
|
||||
}
|
||||
|
||||
public MainPage() {
|
||||
if (Metadata.isNightly()) {
|
||||
announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.nightly.title"), i18n("update.channel.nightly.hint")));
|
||||
} else if (Metadata.isDev()) {
|
||||
announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.dev.title"), i18n("update.channel.dev.hint")));
|
||||
}
|
||||
}
|
||||
|
||||
private void doAnimation(boolean show) {
|
||||
Duration duration = Duration.millis(320);
|
||||
Timeline nowAnimation = new Timeline();
|
||||
@@ -228,8 +240,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void launch() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion());
|
||||
Versions.launch(Profiles.getSelectedProfile());
|
||||
}
|
||||
|
||||
private void onMenu() {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.application.Platform;
|
||||
@@ -37,10 +39,11 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -52,13 +55,14 @@ public class PersonalizationPage extends StackPane {
|
||||
content.setPadding(new Insets(10));
|
||||
content.setFillWidth(true);
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
getChildren().setAll(scrollPane);
|
||||
|
||||
ComponentList themeList = new ComponentList();
|
||||
{
|
||||
ComponentList themeList = new ComponentList();
|
||||
BorderPane themePane = new BorderPane();
|
||||
themeList.getContent().setAll(themePane);
|
||||
themeList.getContent().add(themePane);
|
||||
|
||||
Label left = new Label(i18n("settings.launcher.theme"));
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
@@ -73,35 +77,46 @@ public class PersonalizationPage extends StackPane {
|
||||
picker.setOnAction(e -> {
|
||||
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
|
||||
config().setTheme(theme);
|
||||
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
|
||||
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets(config().getLauncherFontFamily()));
|
||||
});
|
||||
themeColorPickerContainer.getChildren().setAll(picker);
|
||||
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
|
||||
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")), themeList);
|
||||
}
|
||||
{
|
||||
OptionToggleButton titleTransparentButton = new OptionToggleButton();
|
||||
themeList.getContent().add(titleTransparentButton);
|
||||
titleTransparentButton.selectedProperty().bindBidirectional(config().titleTransparentProperty());
|
||||
titleTransparentButton.setTitle(i18n("settings.launcher.title_transparent"));
|
||||
}
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.appearance")), themeList);
|
||||
|
||||
{
|
||||
StackPane componentList = new StackPane();
|
||||
ComponentList componentList = new ComponentList();
|
||||
|
||||
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>(true);
|
||||
backgroundItem.setTitle(i18n("launcher.background"));
|
||||
backgroundItem.setChooserTitle(i18n("launcher.background.choose"));
|
||||
backgroundItem.setHasSubtitle(true);
|
||||
backgroundItem.setCustomText(i18n("settings.custom"));
|
||||
backgroundItem.setStyle("-fx-padding: 8 0 0 0");
|
||||
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>();
|
||||
ComponentSublist backgroundSublist = new ComponentSublist();
|
||||
backgroundSublist.getContent().add(backgroundItem);
|
||||
backgroundSublist.setTitle(i18n("launcher.background"));
|
||||
backgroundSublist.setHasSubtitle(true);
|
||||
|
||||
backgroundItem.loadChildren(Collections.singletonList(
|
||||
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
|
||||
), EnumBackgroundImage.CUSTOM);
|
||||
backgroundItem.customTextProperty().bindBidirectional(config().backgroundImageProperty());
|
||||
backgroundItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.classic"), EnumBackgroundImage.CLASSIC),
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.translucent"), EnumBackgroundImage.TRANSLUCENT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.background.choose"))
|
||||
.bindBidirectional(config().backgroundImageProperty()),
|
||||
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
|
||||
.setValidators(new URLValidator(true))
|
||||
.bindBidirectional(config().backgroundImageUrlProperty())
|
||||
));
|
||||
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
|
||||
backgroundItem.subtitleProperty().bind(
|
||||
backgroundSublist.subtitleProperty().bind(
|
||||
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
|
||||
.then(i18n("launcher.background.default"))
|
||||
.otherwise(config().backgroundImageProperty()));
|
||||
|
||||
componentList.getChildren().add(backgroundItem);
|
||||
componentList.getContent().add(backgroundItem);
|
||||
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
|
||||
}
|
||||
|
||||
@@ -176,21 +191,17 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
{
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
hBox.setSpacing(8);
|
||||
|
||||
FontComboBox cboFont = new FontComboBox(12);
|
||||
cboFont.valueProperty().bindBidirectional(config().fontFamilyProperty());
|
||||
cboFont.valueProperty().bindBidirectional(config().launcherFontFamilyProperty());
|
||||
|
||||
JFXTextField txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
txtFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
|
||||
SafeStringConverter.fromFiniteDouble()
|
||||
.restrict(it -> it > 0)
|
||||
.fallbackTo(12.0)
|
||||
.asPredicate(Validator.addTo(txtFontSize)));
|
||||
JFXButton clearButton = new JFXButton();
|
||||
clearButton.getStyleClass().add("toggle-icon4");
|
||||
clearButton.setGraphic(SVG.restore(Theme.blackFillBinding(), -1, -1));
|
||||
clearButton.setOnAction(e -> config().setLauncherFontFamily(null));
|
||||
|
||||
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
hBox.getChildren().setAll(cboFont, clearButton);
|
||||
|
||||
borderPane.setRight(hBox);
|
||||
}
|
||||
@@ -198,8 +209,11 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
Label lblFontDisplay = new Label("Hello Minecraft! Launcher");
|
||||
lblFontDisplay.fontProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Font.font(config().getFontFamily(), config().getFontSize()),
|
||||
config().fontFamilyProperty(), config().fontSizeProperty()));
|
||||
() -> Font.font(config().getLauncherFontFamily(), 12),
|
||||
config().launcherFontFamilyProperty()));
|
||||
config().launcherFontFamilyProperty().addListener((a, b, newValue) -> {
|
||||
Controllers.getScene().getStylesheets().setAll(config().getTheme().getStylesheets(newValue));
|
||||
});
|
||||
|
||||
vbox.getChildren().add(lblFontDisplay);
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
@@ -32,11 +30,12 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorTabPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
@@ -52,18 +51,14 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class RootPage extends DecoratorTabPage {
|
||||
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private MainPage mainPage = null;
|
||||
|
||||
private final TabHeader.Tab<MainPage> mainTab = new TabHeader.Tab<>("main");
|
||||
|
||||
public RootPage() {
|
||||
setLeftPaneWidth(200);
|
||||
|
||||
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
|
||||
.register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
|
||||
|
||||
@@ -71,25 +66,13 @@ public class RootPage extends DecoratorTabPage {
|
||||
if (profile != null && profile.getRepository().isLoaded())
|
||||
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
|
||||
|
||||
mainTab.setNodeSupplier(this::getMainPage);
|
||||
getTabs().setAll(mainTab);
|
||||
getStyleClass().remove("gray-background");
|
||||
getLeft().getStyleClass().add("gray-background");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean back() {
|
||||
if (mainTab.isSelected())
|
||||
return true;
|
||||
else {
|
||||
getSelectionModel().select(mainTab);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNavigated(Node to) {
|
||||
backableProperty().set(!(to instanceof MainPage));
|
||||
|
||||
super.onNavigated(to);
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return getMainPage().stateProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -131,7 +114,7 @@ public class RootPage extends DecoratorTabPage {
|
||||
return mainPage;
|
||||
}
|
||||
|
||||
private static class Skin extends SkinBase<RootPage> {
|
||||
private static class Skin extends DecoratorAnimatedPageSkin<RootPage> {
|
||||
|
||||
protected Skin(RootPage control) {
|
||||
super(control);
|
||||
@@ -155,30 +138,28 @@ public class RootPage extends DecoratorTabPage {
|
||||
|
||||
// third item in left sidebar
|
||||
AdvancedListItem gameItem = new AdvancedListItem();
|
||||
gameItem.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/bookshelf.png")).getKey());
|
||||
gameItem.setLeftGraphic(wrap(SVG::viewList));
|
||||
gameItem.setActionButtonVisible(false);
|
||||
gameItem.setTitle(i18n("version.manage"));
|
||||
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
|
||||
|
||||
// forth item in left sidebar
|
||||
AdvancedListItem downloadItem = new AdvancedListItem();
|
||||
downloadItem
|
||||
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/chest.png")).getKey());
|
||||
downloadItem.setLeftGraphic(wrap(SVG::downloadOutline));
|
||||
downloadItem.setActionButtonVisible(false);
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
|
||||
// fifth item in left sidebar
|
||||
AdvancedListItem multiplayerItem = new AdvancedListItem();
|
||||
multiplayerItem
|
||||
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
|
||||
multiplayerItem.setLeftGraphic(wrap(SVG::lan));
|
||||
multiplayerItem.setActionButtonVisible(false);
|
||||
multiplayerItem.setTitle(i18n("multiplayer"));
|
||||
multiplayerItem.setOnAction(e -> Controllers.navigate(Controllers.getMultiplayerPage()));
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem
|
||||
.setLeftGraphic(AdvancedListItem.createImageView(newImage("/assets/img/command.png")).getKey());
|
||||
launcherSettingsItem.setLeftGraphic(wrap(SVG::gearOutline));
|
||||
launcherSettingsItem.setActionButtonVisible(false);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
@@ -191,23 +172,13 @@ public class RootPage extends DecoratorTabPage {
|
||||
.add(gameListItem)
|
||||
.add(gameItem)
|
||||
.add(downloadItem)
|
||||
.startCategory(i18n("settings.launcher.general").toLowerCase())
|
||||
// .add(multiplayerItem)
|
||||
.startCategory(i18n("settings.launcher.general").toUpperCase())
|
||||
.add(multiplayerItem)
|
||||
.add(launcherSettingsItem);
|
||||
|
||||
// the root page, with the sidebar in left, navigator in center.
|
||||
BorderPane root = new BorderPane();
|
||||
sideBar.setPrefWidth(200);
|
||||
root.setLeft(sideBar);
|
||||
|
||||
{
|
||||
control.transitionPane.getStyleClass().add("jfx-decorator-content-container");
|
||||
control.transitionPane.getChildren().setAll(getSkinnable().getMainPage());
|
||||
FXUtils.setOverflowHidden(control.transitionPane, 8);
|
||||
root.setCenter(control.transitionPane);
|
||||
}
|
||||
|
||||
getChildren().setAll(root);
|
||||
setLeft(sideBar);
|
||||
setCenter(getSkinnable().getMainPage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -43,7 +43,6 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -65,12 +64,8 @@ public final class SettingsPage extends SettingsView {
|
||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||
// ====
|
||||
|
||||
fileCommonLocation.loadChildren(Collections.singletonList(
|
||||
fileCommonLocation.createChildren(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT)
|
||||
), EnumCommonDirectory.CUSTOM);
|
||||
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
|
||||
fileCommonLocation.customTextProperty().bindBidirectional(config().commonDirectoryProperty());
|
||||
fileCommonLocation.subtitleProperty().bind(
|
||||
fileCommonLocationSublist.subtitleProperty().bind(
|
||||
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
|
||||
.orElse(i18n("launcher.cache_directory.disabled")),
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
@@ -105,14 +100,16 @@ public final class SettingsPage extends SettingsView {
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
|
||||
lblUpdateNote.setWrappingWidth(470);
|
||||
|
||||
ToggleGroup updateChannelGroup = new ToggleGroup();
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateStable.setUserData(UpdateChannel.STABLE);
|
||||
selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class).bindBidirectional(config().updateChannelProperty());
|
||||
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
|
||||
updateChannel.set(UpdateChannel.getChannel());
|
||||
updateChannel.addListener((a, b, newValue) -> {
|
||||
UpdateChecker.requestCheckUpdate(newValue);
|
||||
});
|
||||
// ====
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -38,6 +39,8 @@ import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -45,9 +48,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public abstract class SettingsView extends StackPane {
|
||||
protected final JFXComboBox<SupportedLocale> cboLanguage;
|
||||
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
|
||||
protected final ComponentSublist fileCommonLocationSublist;
|
||||
protected final Label lblUpdate;
|
||||
protected final Label lblUpdateSub;
|
||||
protected final Text lblUpdateNote;
|
||||
protected final JFXRadioButton chkUpdateStable;
|
||||
protected final JFXRadioButton chkUpdateDev;
|
||||
protected final JFXButton btnUpdate;
|
||||
@@ -130,10 +133,8 @@ public abstract class SettingsView extends StackPane {
|
||||
chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
|
||||
chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
|
||||
|
||||
VBox noteWrapper = new VBox();
|
||||
noteWrapper.setStyle("-fx-padding: 10 0 0 0;");
|
||||
lblUpdateNote = new Text(i18n("update.note"));
|
||||
noteWrapper.getChildren().setAll(lblUpdateNote);
|
||||
TextFlow noteWrapper = new TextFlow(new Text(i18n("update.note")));
|
||||
VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0));
|
||||
|
||||
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
|
||||
|
||||
@@ -143,22 +144,28 @@ public abstract class SettingsView extends StackPane {
|
||||
}
|
||||
|
||||
{
|
||||
fileCommonLocation = new MultiFileItem<>(true);
|
||||
fileCommonLocation.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocation.setDirectory(true);
|
||||
fileCommonLocation.setChooserTitle(i18n("launcher.cache_directory.choose"));
|
||||
fileCommonLocation.setHasSubtitle(true);
|
||||
fileCommonLocation.setCustomText("settings.custom");
|
||||
fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocationSublist = new ComponentSublist();
|
||||
fileCommonLocationSublist.getContent().add(fileCommonLocation);
|
||||
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
|
||||
fileCommonLocationSublist.setHasSubtitle(true);
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.cache_directory.choose"))
|
||||
.setDirectory(true)
|
||||
.bindBidirectional(config().commonDirectoryProperty())
|
||||
));
|
||||
|
||||
{
|
||||
JFXButton cleanButton = new JFXButton(i18n("launcher.cache_directory.clean"));
|
||||
cleanButton.setOnMouseClicked(e -> clearCacheDirectory());
|
||||
cleanButton.getStyleClass().add("jfx-button-border");
|
||||
|
||||
fileCommonLocation.setHeaderRight(cleanButton);
|
||||
fileCommonLocationSublist.setHeaderRight(cleanButton);
|
||||
}
|
||||
|
||||
settingsPane.getContent().add(fileCommonLocation);
|
||||
settingsPane.getContent().add(fileCommonLocationSublist);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
Loaded 100 of 343 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user