Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470c3d6c24 | ||
|
|
6133e7e730 | ||
|
|
c5cbafea60 | ||
|
|
167e25e859 | ||
|
|
f47bd8a407 | ||
|
|
464be411d1 | ||
|
|
d3b6eb36d9 | ||
|
|
71458f5167 | ||
|
|
597730389f | ||
|
|
56b227f467 | ||
|
|
213b03ea56 | ||
|
|
959c4d0698 | ||
|
|
57b3bd9ff0 | ||
|
|
68f4038879 | ||
|
|
6010008409 | ||
|
|
4b2e857f08 | ||
|
|
0280a34d00 | ||
|
|
811707b611 | ||
|
|
9152550bda | ||
|
|
6cb4d24600 | ||
|
|
49287453eb | ||
|
|
e7c108d843 | ||
|
|
6c9bf6cb09 | ||
|
|
efc27c8e21 | ||
|
|
7c5ccc906e | ||
|
|
69a6ff6053 | ||
|
|
605d0c6a92 | ||
|
|
9d54ddf456 | ||
|
|
c6afd53e73 | ||
|
|
fd9bb88033 | ||
|
|
316607b333 | ||
|
|
af36a0ac2a | ||
|
|
7356335106 | ||
|
|
d8c96769ea | ||
|
|
d71815aa3d | ||
|
|
9a4286620f | ||
|
|
0eac7b5d12 | ||
|
|
af63c3aa3a | ||
|
|
72a8199453 | ||
|
|
090b83bf9c | ||
|
|
6f70da51f9 | ||
|
|
ed0a4c8cf4 | ||
|
|
5fa066de6b | ||
|
|
1454ea8358 | ||
|
|
ab264955f5 | ||
|
|
389d430dbd | ||
|
|
8b98932ae8 | ||
|
|
62f39a3280 | ||
|
|
b116186f42 | ||
|
|
40eeee7137 | ||
|
|
54eab87356 | ||
|
|
d828f7bc7b | ||
|
|
ae3e135e06 | ||
|
|
5ba8bdad10 | ||
|
|
bffbfd7af9 | ||
|
|
a8c1e8d2cb | ||
|
|
230d6b20ab | ||
|
|
109a14dd74 | ||
|
|
1ac1ff3c95 | ||
|
|
7e522ff136 | ||
|
|
90c261ddd1 | ||
|
|
b9bdc29dc2 | ||
|
|
13968fe3aa | ||
|
|
825df1981d | ||
|
|
dd56f4f8a6 | ||
|
|
b9ef10a73d | ||
|
|
0f68a7c703 | ||
|
|
f5b24b62f1 | ||
|
|
f9eca35422 | ||
|
|
394a74e566 | ||
|
|
870b73c101 | ||
|
|
d89a32f620 | ||
|
|
29410b951d | ||
|
|
461cde3282 | ||
|
|
94b54f3dce | ||
|
|
6e20ce4db3 | ||
|
|
e76d520cac | ||
|
|
9a3b8545ca | ||
|
|
bb66be7c3a | ||
|
|
8514eb904d | ||
|
|
a6c8867ec4 | ||
|
|
5753c2993c | ||
|
|
ec6637728a | ||
|
|
3198daa60a | ||
|
|
1a6dffab74 | ||
|
|
d54d9ae5cb | ||
|
|
e543ee3f55 | ||
|
|
f7526d4cdc | ||
|
|
cf136fe7df | ||
|
|
5d9d9b9cc4 | ||
|
|
6a05df2c35 | ||
|
|
34cfdb8073 | ||
|
|
259ed570ab | ||
|
|
c86302df58 | ||
|
|
3d9c9689e3 | ||
|
|
d4398764a1 | ||
|
|
fb573d8205 | ||
|
|
da5097ab78 | ||
|
|
59b5aabfe5 | ||
|
|
8355f02de0 | ||
|
|
bd4b334185 | ||
|
|
bbe0a3f567 | ||
|
|
41620dcd70 | ||
|
|
e865b31b62 | ||
|
|
92ce7c83ca | ||
|
|
753ba956fe | ||
|
|
0130676b42 | ||
|
|
0ec00ede70 | ||
|
|
42dd877b97 | ||
|
|
00e8ae691c | ||
|
|
1cc429c7ca | ||
|
|
bf822d7cf7 | ||
|
|
5dc1d14f73 | ||
|
|
15a93a9dc0 | ||
|
|
0c0cd97a6e | ||
|
|
b132aba1e3 | ||
|
|
7d97e5d104 | ||
|
|
7c8d75f5a4 | ||
|
|
1b5522958a | ||
|
|
593e70fa43 | ||
|
|
6475e03c01 | ||
|
|
2d4ca3100e | ||
|
|
3f0e8138b1 | ||
|
|
9e6d6ff95a | ||
|
|
6ae6020b73 | ||
|
|
4b14673aaf | ||
|
|
bd7365e944 | ||
|
|
8c38e4c628 | ||
|
|
2b4e476153 | ||
|
|
0f0f5cbc28 | ||
|
|
bed74dc66c | ||
|
|
ba196dcdf4 | ||
|
|
9f8ca0255b | ||
|
|
b6a156b0d6 | ||
|
|
d032b3c677 | ||
|
|
4c2d1063ec | ||
|
|
2e7e2d261c | ||
|
|
9750c69742 | ||
|
|
d67dd700e9 | ||
|
|
7f599638d7 | ||
|
|
bf04854e68 | ||
|
|
2bcf9ee39e | ||
|
|
7a90035278 | ||
|
|
a7108f318a | ||
|
|
a3eaccb45b | ||
|
|
e0c97da9fd | ||
|
|
3843002113 | ||
|
|
1ec9e41459 | ||
|
|
a6231ac593 | ||
|
|
ea91ee91ec | ||
|
|
d4c0c30283 | ||
|
|
9e2c46a6f8 | ||
|
|
ce31bcc6e0 | ||
|
|
d11e22026c | ||
|
|
a96fe298b8 | ||
|
|
8e28c2487f | ||
|
|
fda6f75333 | ||
|
|
e414450f3c | ||
|
|
95e15d7df4 | ||
|
|
7e826a5952 | ||
|
|
43e2644f0e | ||
|
|
a0ef16bef4 | ||
|
|
f5d83095cc | ||
|
|
a94bb407e5 | ||
|
|
6611774aa9 | ||
|
|
e1eb40c129 | ||
|
|
2e532be34c | ||
|
|
b9d0cfc12a | ||
|
|
3d88bc35bb | ||
|
|
8c9c8fd28f | ||
|
|
5e685b7f26 | ||
|
|
c76b562705 | ||
|
|
4c9f8d3dca | ||
|
|
0f26ae5cfc | ||
|
|
035469240d | ||
|
|
7ee81f7425 | ||
|
|
024c90d688 | ||
|
|
1253272626 | ||
|
|
73b938cfd9 | ||
|
|
ee843543bf | ||
|
|
fa8a479aac | ||
|
|
bac14db432 | ||
|
|
f6242ea868 | ||
|
|
16db35655a | ||
|
|
53336aedc9 | ||
|
|
eaddb67932 | ||
|
|
c7b7dd9500 | ||
|
|
9901bf738d |
No files matched your search
@@ -1,6 +1,8 @@
|
||||
name: Bug Feedback
|
||||
description:
|
||||
反馈一个 Bug。为了提高反馈效率,请优先在 KOOK 或者或 Discord 频道中反馈。若确认是 HMCL 问题,请发布 issue。
|
||||
反馈一个错误。
|
||||
为了提高反馈效率,请优先在 KOOK、赞助频道或者 Discord 频道中反馈。若确认是 HMCL 问题,请发布 issue。
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -8,8 +10,9 @@ body:
|
||||
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
|
||||
|
||||
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
|
||||
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [KOOK 频道](https://kook.top/Kx7n3t)
|
||||
或 [Discord 频道](https://discord.gg/jVvC7HfM6U)中反馈信息,**而不在 GitHub Issues 中反馈问题**。
|
||||
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在
|
||||
[KOOK 频道](https://kook.top/Kx7n3t) 或 [Discord 频道](https://discord.gg/jVvC7HfM6U)中反馈信息,
|
||||
**而不在 GitHub Issues 中反馈问题**。
|
||||
|
||||
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://hmcl.huangyuhui.net/api/redirect/sponsor) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
|
||||
- type: input
|
||||
@@ -28,6 +31,13 @@ body:
|
||||
placeholder: e.g. Windows 11
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-report
|
||||
attributes:
|
||||
label: 问题描述 Problem Description
|
||||
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。 Please describe the problem in as much detail as possible, especially how you encountered this bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: java-version
|
||||
attributes:
|
||||
@@ -61,9 +71,4 @@ body:
|
||||
attributes:
|
||||
label: 启动器日志文件 Launcher Log File
|
||||
description: 遇到问题后,请在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。 After encountering a problem, please click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
|
||||
- type: textarea
|
||||
id: bug-report
|
||||
attributes:
|
||||
label: 问题描述 Problem Description
|
||||
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。 Please describe the problem in as much detail as possible, especially how you encountered this bug.
|
||||
|
||||
@@ -10,5 +10,5 @@ contact_links:
|
||||
url: https://afdian.net/@huanghongxun
|
||||
about: 前往爱发电赞助 HMCL。赞助后可以申请加入官方 QQ 群反馈问题。
|
||||
- name: 其他反馈
|
||||
url: https://github.com/huanghongxun/HMCL/issues/new
|
||||
about: 通过 Issue 反馈其他问题。
|
||||
url: https://github.com/huanghongxun/HMCL/discussions/new/choose
|
||||
about: 通过 Discussions 反馈其他问题。
|
||||
@@ -0,0 +1,21 @@
|
||||
name: "Feature Request"
|
||||
description: "为 HMCL 提出新功能"
|
||||
labels: feature request
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "提交前请确保查看 issues 列表是否有重复的项目!"
|
||||
- type: textarea
|
||||
id: "yml-2"
|
||||
attributes:
|
||||
label: 描述 Description
|
||||
description: "详细描述你想加入的新功能"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: "yml-3"
|
||||
attributes:
|
||||
label: 原因 Why
|
||||
description: "描述该功能带来的好处及原因"
|
||||
validations:
|
||||
required: true
|
||||
@@ -8,15 +8,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain
|
||||
- name: Check style test
|
||||
|
||||
@@ -8,14 +8,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
|
||||
@@ -8,15 +8,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
env:
|
||||
@@ -26,7 +24,7 @@ jobs:
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
@@ -12,6 +12,7 @@ hs_err_pid*
|
||||
.mine*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
@@ -19,7 +20,6 @@ NVIDIA
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/log4j-patch/build/
|
||||
|
||||
# idea
|
||||
.idea
|
||||
@@ -27,14 +27,12 @@ NVIDIA
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/log4j-patch/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/log4j-patch/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
language: java
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
before_install:
|
||||
- chmod +x gradlew
|
||||
script: "bash ./gradlew clean build --stacktrace"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.gradle/caches/"
|
||||
@@ -1,54 +1,36 @@
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.URI
|
||||
|
||||
import org.glavo.pack200.Pack200
|
||||
import org.tukaani.xz.LZMA2Options
|
||||
import org.tukaani.xz.XZOutputStream
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.tukaani:xz:1.8")
|
||||
classpath("org.glavo:pack200:0.3.0")
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||
}
|
||||
|
||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
||||
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun" && System.getenv("GITHUB_BASE_REF").isNullOrEmpty())
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||
if (number != null) {
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7)
|
||||
if (!shortCommit.isNullOrEmpty()) "dev-$shortCommit" else "SNAPSHOT"
|
||||
val prefix = if (isOfficial) "dev" else "unofficial"
|
||||
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
}
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
|
||||
val versionType = System.getenv("VERSION_TYPE") ?: "nightly"
|
||||
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
|
||||
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val enableHiPer = System.getenv("ENABLE_HIPER") ?: "false"
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
dependencies {
|
||||
@@ -74,7 +56,12 @@ fun createChecksum(file: File) {
|
||||
}
|
||||
|
||||
fun attachSignature(jar: File) {
|
||||
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY") ?: return
|
||||
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY")
|
||||
if (keyLocation == null) {
|
||||
logger.warn("Missing signature key")
|
||||
return
|
||||
}
|
||||
|
||||
val privatekey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(File(keyLocation).readBytes()))
|
||||
val signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
@@ -93,21 +80,6 @@ fun attachSignature(jar: File) {
|
||||
}
|
||||
}
|
||||
|
||||
val packer = Pack200.newPacker().apply {
|
||||
properties()["pack.effort"] = "9"
|
||||
}
|
||||
|
||||
val unpacker = Pack200.newUnpacker()
|
||||
|
||||
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
|
||||
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
|
||||
// .class files remain the same.
|
||||
fun repack(file: File) {
|
||||
val packed = ByteArrayOutputStream()
|
||||
JarFile(file).use { packer.pack(it, packed) }
|
||||
JarOutputStream(file.outputStream()).use { unpacker.unpack(ByteArrayInputStream(packed.toByteArray()), it) }
|
||||
}
|
||||
|
||||
val java11 = sourceSets.create("java11") {
|
||||
java {
|
||||
srcDir("src/main/java11")
|
||||
@@ -143,7 +115,7 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2021 huangyuhui.",
|
||||
"Created-By" to "Copyright(c) 2013-2023 huangyuhui.",
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
"Implementation-Version" to project.version,
|
||||
@@ -151,34 +123,29 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Build-Channel" to versionType,
|
||||
"Enable-HiPer" to enableHiPer,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.graphics/com.sun.javafx.stage"
|
||||
).joinToString(" "),
|
||||
"Add-Exports" to listOf(
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.base/com.sun.javafx.event"
|
||||
"javafx.graphics/com.sun.prism",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin"
|
||||
).joinToString(" ")
|
||||
)
|
||||
|
||||
System.getenv("GITHUB_SHA")?.also {
|
||||
attributes("GitHub-SHA" to it)
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jarPath) // see repack()
|
||||
attachSignature(jarPath)
|
||||
createChecksum(jarPath)
|
||||
}
|
||||
@@ -225,46 +192,8 @@ tasks.processResources {
|
||||
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
|
||||
}
|
||||
|
||||
val packFile = File(jarPath.parentFile, jarPath.nameWithoutExtension + ".pack")
|
||||
|
||||
val makePack = tasks.create("makePack") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
doLast {
|
||||
packFile.outputStream().use { out ->
|
||||
JarFile(jarPath).use { jarFile -> packer.pack(jarFile, out) }
|
||||
}
|
||||
createChecksum(packFile)
|
||||
}
|
||||
}
|
||||
|
||||
val makePackXz = tasks.create("makePackXz") {
|
||||
dependsOn(makePack)
|
||||
|
||||
val packXz = File(packFile.parentFile, packFile.name + ".xz")
|
||||
|
||||
doLast {
|
||||
// Our CI server does not have enough memory space to compress file at highest level.
|
||||
XZOutputStream(packXz.outputStream(), LZMA2Options(5))
|
||||
.use { it.write(packFile.readBytes()) }
|
||||
createChecksum(packXz)
|
||||
}
|
||||
}
|
||||
|
||||
val makePackGz = tasks.create("makePackGz") {
|
||||
dependsOn(makePack)
|
||||
|
||||
val packGz = File(packFile.parentFile, packFile.name + ".gz")
|
||||
|
||||
doLast {
|
||||
GZIPOutputStream(packGz.outputStream()).use { it.write(packFile.readBytes()) }
|
||||
createChecksum(packGz)
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables = tasks.create("makeExecutables") {
|
||||
dependsOn(makePack)
|
||||
|
||||
dependsOn(tasks.jar)
|
||||
doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
|
||||
@@ -272,7 +201,7 @@ val makeExecutables = tasks.create("makeExecutables") {
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(makePackXz, makePackGz, makeExecutables)
|
||||
dependsOn(makeExecutables)
|
||||
}
|
||||
|
||||
tasks.create<JavaExec>("run") {
|
||||
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero">
|
||||
␍</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 -0.5 52 56" shape-rendering="crispEdges" xmlns="http://www.w3.org/2000/svg"><path stroke="#38342a" d="M28 0h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m4 0h4M24 5h4m4 0h4M24 6h4m4 0h4M24 7h4m4 0h4M24 8h4m8 0h4M24 9h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-20 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-28 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-36 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8M8 24h4m28 0h4m4 0h4M8 25h4m28 0h4m4 0h4M8 26h4m28 0h4m4 0h4M8 27h4m28 0h4m4 0h4M4 28h4m32 0h8M4 29h4m32 0h8M4 30h4m32 0h8M4 31h4m32 0h8M0 32h4m32 0h4M0 33h4m32 0h4M0 34h4m32 0h4M0 35h4m32 0h4M0 36h4m28 0h4M0 37h4m28 0h4M0 38h4m28 0h4M0 39h4m28 0h4M4 40h4m20 0h4M4 41h4m20 0h4M4 42h4m20 0h4M4 43h4m20 0h4M8 44h4m12 0h4M8 45h4m12 0h4M8 46h4m12 0h4M8 47h4m12 0h4m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-12 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#dbd0b4" d="M28 4h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-28 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-24 1h16m-16 1h16m-16 1h16m-16 1h16M8 28h8m4 0h12M8 29h8m4 0h12M8 30h8m4 0h12M8 31h8m4 0h12M8 32h12m8 0h4M8 33h12m8 0h4M8 34h12m8 0h4M8 35h12m8 0h4m-20 1h12m-12 1h12m-12 1h12m-12 1h12m-8 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#c6bca5" d="M28 8h4m-4 1h4m-4 1h4m-4 1h4m-12 5h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8m-20 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#bcb29c" d="M32 12h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#807a6d" d="M44 24h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#aea694" d="M4 32h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#9a927e" d="M4 36h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
@@ -16,8 +16,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
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 Image ALEX = new Image("/assets/img/skin/alex.png");
|
||||
public static final Image STEVE = new Image("/assets/img/skin/steve.png");
|
||||
|
||||
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
|
||||
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||
|
||||
@@ -4,8 +4,6 @@ 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 {
|
||||
@@ -77,13 +75,23 @@ public class SkinCube extends MeshView {
|
||||
};
|
||||
|
||||
int[] copy = faces.clone();
|
||||
ArrayUtils.reverse(copy);
|
||||
|
||||
for (int i = 0, mid = copy.length >> 1, j = copy.length - 1; i < mid; i++, j--) {
|
||||
int tmp = copy[i];
|
||||
copy[i] = copy[j];
|
||||
copy[j] = tmp;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
int[] result = new int[faces.length + copy.length];
|
||||
System.arraycopy(faces, 0, result, 0, faces.length);
|
||||
System.arraycopy(copy, 0, result, faces.length, copy.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class SkinTransition extends Transition {
|
||||
return count;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public SkinTransition(Duration duration, Function<Double, Double> expression, WritableValue<Number>... observables) {
|
||||
setCycleDuration(duration);
|
||||
this.expression = expression;
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
@@ -28,7 +29,6 @@ import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
@@ -40,7 +40,7 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.CookieHandler;
|
||||
@@ -66,6 +66,14 @@ public final class Launcher extends Application {
|
||||
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to get prism pipeline", e);
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
@@ -73,54 +81,27 @@ public final class Launcher extends Application {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load config", e);
|
||||
checkConfigInTempDir();
|
||||
checkConfigOwner();
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
}
|
||||
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
|
||||
String owner = Files.getOwner(ConfigHolder.configLocation()).getName();
|
||||
String userName = System.getProperty("user.name");
|
||||
if (!Files.isWritable(ConfigHolder.configLocation())
|
||||
&& !userName.equals("root")
|
||||
&& !userName.equals(owner)) {
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
{
|
||||
files.add(ConfigHolder.configLocation().toString());
|
||||
if (Files.exists(Metadata.HMCL_DIRECTORY))
|
||||
files.add(Metadata.HMCL_DIRECTORY.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
}
|
||||
|
||||
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
ButtonType res = new Alert(Alert.AlertType.ERROR, i18n("fatal.config_loading_failure.unix", owner, command), copyAndExit, ButtonType.CLOSE)
|
||||
.showAndWait()
|
||||
.orElse(null);
|
||||
if (res == copyAndExit) {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
|
||||
}
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize().toString()));
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
|
||||
&& ConfigHolder.isNewlyCreated()
|
||||
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
|
||||
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||
return;
|
||||
} else {
|
||||
checkConfigInTempDir();
|
||||
}
|
||||
|
||||
if (ConfigHolder.isOwnerChanged()) {
|
||||
ButtonType res = new Alert(Alert.AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO)
|
||||
.showAndWait()
|
||||
.orElse(null);
|
||||
if (res == ButtonType.NO)
|
||||
if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
|
||||
return;
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_DIRECTORY.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
@@ -131,8 +112,6 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -142,18 +121,97 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
private static boolean isConfigInTempDir() {
|
||||
String configPath = ConfigHolder.configLocation().toString();
|
||||
|
||||
String tmpdir = System.getProperty("java.io.tmpdir");
|
||||
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
|
||||
return true;
|
||||
|
||||
String[] tempFolderNames = {"Temp", "Cache", "Caches"};
|
||||
for (String name : tempFolderNames) {
|
||||
if (configPath.contains(File.separator + name + File.separator))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
return configPath.contains("\\Temporary Internet Files\\")
|
||||
|| configPath.contains("\\INetCache\\")
|
||||
|| configPath.contains("\\$Recycle.Bin\\")
|
||||
|| configPath.contains("\\recycler\\");
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
return configPath.startsWith("/tmp/")
|
||||
|| configPath.startsWith("/var/tmp/")
|
||||
|| configPath.startsWith("/var/cache/")
|
||||
|| configPath.startsWith("/dev/shm/")
|
||||
|| configPath.contains("/Trash/");
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
return configPath.startsWith("/var/folders/")
|
||||
|| configPath.startsWith("/private/var/folders/")
|
||||
|| configPath.startsWith("/tmp/")
|
||||
|| configPath.startsWith("/private/tmp/")
|
||||
|| configPath.startsWith("/var/tmp/")
|
||||
|| configPath.startsWith("/private/var/tmp/")
|
||||
|| configPath.contains("/.Trash/");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkConfigInTempDir() {
|
||||
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkConfigOwner() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
return;
|
||||
|
||||
String userName = System.getProperty("user.name");
|
||||
String owner;
|
||||
try {
|
||||
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
|
||||
} catch (IOException ioe) {
|
||||
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
|
||||
return;
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
files.add(ConfigHolder.configLocation().toString());
|
||||
if (Files.exists(Metadata.HMCL_DIRECTORY))
|
||||
files.add(Metadata.HMCL_DIRECTORY.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
|
||||
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
if (showAlert(AlertType.ERROR,
|
||||
i18n("fatal.config_loading_failure.unix", owner, command),
|
||||
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
super.stop();
|
||||
Controllers.onApplicationStop();
|
||||
}
|
||||
|
||||
private void initIcon() {
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
@@ -170,12 +228,17 @@ public final class Launcher extends Application {
|
||||
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"));
|
||||
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
|
||||
LOG.info("Current Directory: " + System.getProperty("user.dir"));
|
||||
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
LOG.info("HMCL Jar Path: " + JarUtils.thisJar().map(it -> it.toAbsolutePath().toString()).orElse("Not Found"));
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
|
||||
.filter(bean -> bean.getName().equals("Metaspace"))
|
||||
.findAny()
|
||||
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
|
||||
.orElse("Unknown"));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
|
||||
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
|
||||
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
|
||||
@@ -19,14 +19,18 @@ package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.FractureiserDetector;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
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;
|
||||
import java.io.InputStream;
|
||||
@@ -53,33 +57,29 @@ public final class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
|
||||
// Use System look and feel
|
||||
initLookAndFeel();
|
||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
initIcon();
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkJavaFX();
|
||||
detectFractureiser();
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
private static void initLookAndFeel() {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
@@ -91,6 +91,13 @@ public final class Main {
|
||||
}
|
||||
}
|
||||
|
||||
private static void detectFractureiser() {
|
||||
if (FractureiserDetector.detect()) {
|
||||
LOG.log(Level.SEVERE, "Detected that this computer is infected by fractureiser");
|
||||
showErrorAndExit(i18n("fatal.fractureiser"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
@@ -126,7 +133,7 @@ public final class Main {
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
SwingUtils.showErrorDialog(message);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@@ -144,7 +151,8 @@ public final class Main {
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
static void fixLetsEncrypt() {
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
@@ -36,16 +38,35 @@ public final class Metadata {
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://github.com/huanghongxun/HMCL/issues";
|
||||
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
|
||||
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
|
||||
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
||||
public static final String PUBLISH_URL = "https://www.mcbbs.net/thread-142335-1-1.html";
|
||||
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
|
||||
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.html";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
public static final Path HMCL_DIRECTORY;
|
||||
|
||||
static {
|
||||
String hmclHome = System.getProperty("hmcl.home");
|
||||
if (hmclHome == null) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||
if (StringUtils.isNotBlank(xdgData)) {
|
||||
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
|
||||
} else {
|
||||
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
|
||||
}
|
||||
} else {
|
||||
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
} else {
|
||||
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isStable() {
|
||||
return "stable".equals(BUILD_CHANNEL);
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
@@ -47,11 +48,13 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
@@ -172,17 +175,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
File srcGameDir = getRunDirectory(srcId);
|
||||
File dstGameDir = getRunDirectory(dstId);
|
||||
|
||||
List<String> blackList = new ArrayList<>(Arrays.asList(
|
||||
"regex:(.*?)\\.log",
|
||||
"usernamecache.json", "usercache.json", // Minecraft
|
||||
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
|
||||
"backup", "pack.json", "launcher.jar", "cache", // HMCL
|
||||
".curseclient", // Curse
|
||||
".fabric", ".mixin.out", // Fabric
|
||||
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
|
||||
"downloads", // Curse
|
||||
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
|
||||
));
|
||||
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||
blackList.add(srcId + ".jar");
|
||||
blackList.add(srcId + ".json");
|
||||
if (!copySaves)
|
||||
@@ -336,14 +329,24 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setVersionName(version)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory((int)(getAllocatedMemory(
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
) / 1024 / 1024))
|
||||
.setMinMemory(vs.getMinMemory())
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
|
||||
.setEnvironmentVariables(
|
||||
Lang.mapOf(StringUtils.tokenize(vs.getEnvironmentVariables())
|
||||
.stream()
|
||||
.map(it -> {
|
||||
int idx = it.indexOf('=');
|
||||
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
)
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.isFullscreen())
|
||||
@@ -355,7 +358,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setUseSoftwareRenderer(vs.isUseSoftwareRenderer())
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
|
||||
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
|
||||
@@ -380,6 +383,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||
builder.setMaxMemory(null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@@ -426,7 +432,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return false;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase()))
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return OperatingSystem.isNameValid(id);
|
||||
@@ -451,7 +457,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
|
||||
if (auto) {
|
||||
available -= 256 * 1024 * 1024; // Reserve 256MB memory for off-heap memory and HMCL itself
|
||||
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
|
||||
if (available <= 0) {
|
||||
return minimum;
|
||||
}
|
||||
@@ -460,7 +466,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
final long suggested = Math.min(available <= threshold
|
||||
? (long) (available * 0.8)
|
||||
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
|
||||
32736L * 1024 * 1024); // Limit the maximum suggested memory to ensure that compressed oops are available
|
||||
16384L * 1024 * 1024);
|
||||
return Math.max(minimum, suggested);
|
||||
} else {
|
||||
return minimum;
|
||||
|
||||
@@ -21,11 +21,11 @@ import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.*;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
@@ -47,13 +47,9 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -61,6 +57,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -134,8 +131,6 @@ public final class LauncherHelper {
|
||||
|
||||
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
|
||||
|
||||
getLog4jPatch(version.get()).ifPresent(javaAgents::add);
|
||||
|
||||
TaskExecutor executor = checkGameState(profile, setting, version.get())
|
||||
.thenComposeAsync(javaVersion -> {
|
||||
javaVersionRef.set(Objects.requireNonNull(javaVersion));
|
||||
@@ -155,8 +150,9 @@ public final class LauncherHelper {
|
||||
}
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
if (setting.isUseSoftwareRenderer() && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getSoftwareRendererLoader(javaVersion);
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getMesaLoader(javaVersion, renderer);
|
||||
if (lib == null)
|
||||
return null;
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
@@ -165,11 +161,13 @@ public final class LauncherHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
String agent = file.getAbsolutePath() + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(file.getAbsolutePath()));
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(file.getAbsolutePath());
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@@ -412,26 +410,26 @@ public final class LauncherHelper {
|
||||
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
|
||||
|
||||
JavaVersionConstraint violatedMandatoryConstraint = null;
|
||||
JavaVersionConstraint violatedSuggestedConstraint = null;
|
||||
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion)) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
|
||||
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
|
||||
violatedMandatoryConstraint = constraint;
|
||||
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
|
||||
violatedSuggestedConstraint = constraint;
|
||||
if (violatedSuggestedConstraints == null)
|
||||
violatedSuggestedConstraints = new ArrayList<>(1);
|
||||
violatedSuggestedConstraints.add(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"));
|
||||
};
|
||||
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
|
||||
|
||||
if (violatedMandatoryConstraint != null) {
|
||||
if (suggestedJavaVersion != null) {
|
||||
@@ -454,14 +452,14 @@ public final class LauncherHelper {
|
||||
}, Schedulers.javafx());
|
||||
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(OPENJDK_DOWNLOAD_LINK);
|
||||
}, breakAction);
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"),
|
||||
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
|
||||
breakAction.run();
|
||||
return Task.fromCompletableFuture(future);
|
||||
case VANILLA_JAVA_17:
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"), () -> {
|
||||
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
|
||||
}, breakAction);
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"),
|
||||
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
|
||||
breakAction.run();
|
||||
return Task.fromCompletableFuture(future);
|
||||
case VANILLA_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
@@ -480,83 +478,99 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
List<String> suggestions = new ArrayList<>(0);
|
||||
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
|
||||
suggestions.add(i18n("launch.advice.different_platform"));
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (javaVersion.getBits() == Bits.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
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;
|
||||
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
|
||||
}
|
||||
|
||||
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);
|
||||
break;
|
||||
case MODDED_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
break;
|
||||
case MODDED_JAVA_16:
|
||||
Controllers.dialog(i18n("launch.advice.forge37_0_60"), 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;
|
||||
case MODLAUNCHER_8:
|
||||
Controllers.dialog(i18n("launch.advice.modlauncher8"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
break;
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
|
||||
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
} else {
|
||||
continueAction.run();
|
||||
}
|
||||
break;
|
||||
if (violatedSuggestedConstraints != null) {
|
||||
for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) {
|
||||
switch (violatedSuggestedConstraint) {
|
||||
case MODDED_JAVA_7:
|
||||
suggestions.add(i18n("launch.advice.java.modded_java_7"));
|
||||
break;
|
||||
case MODDED_JAVA_8:
|
||||
// Minecraft>=1.7.10+Forge accepts Java 8
|
||||
if (javaVersion.getParsedVersion() < 8)
|
||||
suggestions.add(i18n("launch.advice.newer_java"));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion));
|
||||
break;
|
||||
case MODDED_JAVA_16:
|
||||
// Minecraft<=1.17.1+Forge[37.0.0,37.0.60) not compatible with Java 17
|
||||
String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
|
||||
.map(LibraryAnalyzer.LibraryType.FORGE::patchVersion)
|
||||
.orElse(null);
|
||||
if (forgePatchVersion != null && VersionNumber.VERSION_COMPARATOR.compare(forgePatchVersion, "37.0.60") < 0)
|
||||
suggestions.add(i18n("launch.advice.forge37_0_60"));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.modded_java", 16, gameVersion));
|
||||
break;
|
||||
case MODDED_JAVA_17:
|
||||
suggestions.add(i18n("launch.advice.modded_java", 17, gameVersion));
|
||||
break;
|
||||
case VANILLA_JAVA_8_51:
|
||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||
break;
|
||||
case MODLAUNCHER_8:
|
||||
suggestions.add(i18n("launch.advice.modlauncher8"));
|
||||
break;
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
|
||||
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
|
||||
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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, breakAction);
|
||||
suggested = true;
|
||||
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
|
||||
}
|
||||
|
||||
VersionNumber forgeVersion = version.getLibraries().stream()
|
||||
.filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.findFirst()
|
||||
.map(library -> VersionNumber.asVersion(library.getVersion()))
|
||||
.orElse(null);
|
||||
|
||||
// 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, breakAction);
|
||||
suggested = true;
|
||||
}
|
||||
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
|
||||
}
|
||||
|
||||
// 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, breakAction);
|
||||
suggested = true;
|
||||
}
|
||||
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
|
||||
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
|
||||
}
|
||||
|
||||
if (!suggested && !future.isDone()) {
|
||||
future.complete(javaVersion);
|
||||
if (suggestions.isEmpty()) {
|
||||
if (!future.isDone()) {
|
||||
future.complete(javaVersion);
|
||||
}
|
||||
} else {
|
||||
String message;
|
||||
if (suggestions.size() == 1) {
|
||||
message = i18n("launch.advice", suggestions.get(0));
|
||||
} else {
|
||||
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction);
|
||||
}
|
||||
|
||||
return Task.fromCompletableFuture(future);
|
||||
@@ -585,9 +599,7 @@ public final class LauncherHelper {
|
||||
.addAction(link)
|
||||
.yesOrNo(() -> {
|
||||
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
|
||||
.thenAcceptAsync(downloadedJava -> {
|
||||
future.complete(downloadedJava);
|
||||
})
|
||||
.thenAcceptAsync(future::complete)
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolvedException = resolveException(throwable);
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
@@ -597,9 +609,7 @@ public final class LauncherHelper {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
return null;
|
||||
});
|
||||
}, () -> {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}).build());
|
||||
}, () -> future.completeExceptionally(new CancellationException())).build());
|
||||
|
||||
return future;
|
||||
}
|
||||
@@ -663,47 +673,6 @@ public final class LauncherHelper {
|
||||
});
|
||||
}
|
||||
|
||||
private static Optional<String> getLog4jPatch(Version version) {
|
||||
Optional<String> log4jVersion = version.getLibraries().stream()
|
||||
.filter(it -> it.is("org.apache.logging.log4j", "log4j-core")
|
||||
&& (VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "2.17") < 0 || "2.0-beta9".equals(it.getVersion())))
|
||||
.map(Library::getVersion)
|
||||
.findFirst();
|
||||
|
||||
if (log4jVersion.isPresent()) {
|
||||
final String agentFileName = "log4j-patch-agent-1.0.jar";
|
||||
|
||||
Path agentFile = Metadata.HMCL_DIRECTORY.resolve(agentFileName).toAbsolutePath();
|
||||
String agentFilePath = agentFile.toString();
|
||||
if (agentFilePath.indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the HMCL directory path, unable to attach log4j-patch");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (Files.notExists(agentFile)) {
|
||||
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + agentFileName)) {
|
||||
LOG.info("Extract log4j patch to " + agentFilePath);
|
||||
Files.createDirectories(agentFile.getParent());
|
||||
Files.copy(input, agentFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to extract log4j patch");
|
||||
try {
|
||||
Files.deleteIfExists(agentFile);
|
||||
} catch (IOException ex) {
|
||||
LOG.log(Level.WARNING, "Failed to delete incomplete log4j patch", ex);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isBeta = log4jVersion.get().startsWith("2.0-beta");
|
||||
return Optional.of(agentFilePath + "=" + isBeta);
|
||||
} else {
|
||||
LOG.info("No log4j with security vulnerabilities found");
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkExit() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
@@ -733,7 +702,7 @@ public final class LauncherHelper {
|
||||
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||
* Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||
*/
|
||||
class HMCLProcessListener implements ProcessListener {
|
||||
private final class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final Version version;
|
||||
@@ -742,9 +711,11 @@ public final class LauncherHelper {
|
||||
private boolean lwjgl;
|
||||
private LogWindow logWindow;
|
||||
private final boolean detectWindow;
|
||||
private final LinkedList<Pair<String, Log4jLevel>> logs;
|
||||
private final ArrayDeque<String> logs;
|
||||
private final ArrayDeque</*Log4jLevel*/Object> levels;
|
||||
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch launchingLatch;
|
||||
private final String forbiddenAccessToken;
|
||||
|
||||
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
@@ -752,8 +723,11 @@ public final class LauncherHelper {
|
||||
this.launchOptions = launchOptions;
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
|
||||
|
||||
logs = new LinkedList<>();
|
||||
final int numLogs = config().getLogLines() + 1;
|
||||
this.logs = new ArrayDeque<>(numLogs);
|
||||
this.levels = new ArrayDeque<>(numLogs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -804,6 +778,7 @@ public final class LauncherHelper {
|
||||
Controllers.getStage().close();
|
||||
Controllers.shutdown();
|
||||
Schedulers.shutdown();
|
||||
System.gc();
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -811,17 +786,28 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onLog(String log, Log4jLevel level) {
|
||||
String filteredLog = Logging.filterForbiddenToken(log);
|
||||
public void onLog(String log, boolean isErrorStream) {
|
||||
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
|
||||
|
||||
if (level.lessOrEqual(Log4jLevel.ERROR))
|
||||
if (isErrorStream)
|
||||
System.err.println(filteredLog);
|
||||
else
|
||||
System.out.println(filteredLog);
|
||||
|
||||
logs.add(pair(filteredLog, level));
|
||||
if (logs.size() > config().getLogLines())
|
||||
logs.removeFirst();
|
||||
Log4jLevel level;
|
||||
if (isErrorStream)
|
||||
level = Log4jLevel.ERROR;
|
||||
else
|
||||
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
|
||||
|
||||
synchronized (this) {
|
||||
logs.add(filteredLog);
|
||||
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
|
||||
if (logs.size() > config().getLogLines()) {
|
||||
logs.removeFirst();
|
||||
levels.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
if (showLogs) {
|
||||
try {
|
||||
@@ -834,9 +820,12 @@ public final class LauncherHelper {
|
||||
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
|
||||
}
|
||||
|
||||
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
if (!lwjgl) {
|
||||
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
|
||||
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,8 +840,11 @@ public final class LauncherHelper {
|
||||
if (!lwjgl) finishLaunch();
|
||||
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
|
||||
Lang.forEachZipped(logs, levels,
|
||||
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
|
||||
repository.markVersionLaunchedAbnormally(version.getId());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
|
||||
}
|
||||
|
||||
checkExit();
|
||||
|
||||
@@ -37,7 +37,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
@Override
|
||||
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
String newSearchFilter;
|
||||
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
|
||||
if (StringUtils.containsChinese(searchFilter)) {
|
||||
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
|
||||
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
|
||||
List<String> searchFilters = new ArrayList<>();
|
||||
|
||||
@@ -19,17 +19,25 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public final class LogExporter {
|
||||
private LogExporter() {
|
||||
@@ -57,16 +65,37 @@ public final class LogExporter {
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Zipper zipper = new Zipper(zipFile)) {
|
||||
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
|
||||
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
|
||||
Path logsDir = runDirectory.resolve("logs");
|
||||
if (Files.exists(logsDir.resolve("debug.log"))) {
|
||||
zipper.putFile(logsDir.resolve("debug.log"), "debug.log");
|
||||
}
|
||||
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
|
||||
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
|
||||
if (Files.exists(logsDir.resolve("latest.log"))) {
|
||||
zipper.putFile(logsDir.resolve("latest.log"), "latest.log");
|
||||
}
|
||||
if (Files.exists(logsDir.resolve("fml-client-latest.log"))) {
|
||||
zipper.putFile(logsDir.resolve("fml-client-latest.log"), "fml-client-latest.log");
|
||||
}
|
||||
|
||||
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
|
||||
zipper.putTextFile(logs, "minecraft.log");
|
||||
zipper.putTextFile(Logging.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(runDirectory, "hs_err_*.log")) {
|
||||
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
|
||||
|
||||
for (Path file : stream) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).creationTime();
|
||||
if (time.toMillis() >= processStartTime) {
|
||||
String crashLog = Logging.filterForbiddenToken(FileUtils.readText(file));
|
||||
zipper.putTextFile(crashLog, file.getFileName().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to find vm crash log", e);
|
||||
}
|
||||
|
||||
for (String id : versions) {
|
||||
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
|
||||
if (Files.exists(versionJson)) {
|
||||
|
||||
@@ -30,7 +30,6 @@ 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;
|
||||
@@ -104,7 +103,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
|
||||
@@ -17,32 +17,31 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -64,15 +63,15 @@ public final class TexturesLoader {
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Image image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
public LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -96,7 +95,7 @@ public final class TexturesLoader {
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
|
||||
if (StringUtils.isBlank(texture.getUrl())) {
|
||||
throw new IOException("Texture url is empty");
|
||||
}
|
||||
@@ -117,12 +116,13 @@ public final class TexturesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
Image img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
img = new Image(in);
|
||||
}
|
||||
if (img == null)
|
||||
throw new IOException("Texture is malformed");
|
||||
|
||||
if (img.isError())
|
||||
throw img.getException();
|
||||
|
||||
Map<String, String> metadata = texture.getMetadata();
|
||||
if (metadata == null) {
|
||||
@@ -136,16 +136,21 @@ public final class TexturesLoader {
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
loadDefaultSkin("/assets/img/skin/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/skin/alex.png", TextureModel.ALEX);
|
||||
}
|
||||
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
||||
Image skin;
|
||||
try {
|
||||
skin = new Image(path);
|
||||
if (skin.isError())
|
||||
throw skin.getException();
|
||||
} catch (Throwable e) {
|
||||
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
|
||||
throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
|
||||
}
|
||||
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(skin, singletonMap("model", model.modelName)));
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
@@ -172,7 +177,7 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
@@ -195,7 +200,7 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
@@ -209,38 +214,117 @@ public final class TexturesLoader {
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = avatar.createGraphics();
|
||||
public static void drawAvatar(Canvas canvas, Image skin) {
|
||||
GraphicsContext g = canvas.getGraphicsContext2D();
|
||||
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
|
||||
int scale = skin.getWidth() / 64;
|
||||
int size = (int) canvas.getWidth();
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
g.drawImage(skin,
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
g.drawImage(skin,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
|
||||
// In order to prevent the blurring caused by bilinear interpolation,
|
||||
// we use the self-implemented nearest neighbor interpolation to scale
|
||||
drawAvatarSlow(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
g.drawImage(skin,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
g.drawImage(skin,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
PixelReader reader = skin.getPixelReader();
|
||||
PixelWriter writer = g.getPixelWriter();
|
||||
drawImage(writer, reader,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
drawImage(writer, reader,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Scale and draw image using nearest-neighbor interpolation
|
||||
*/
|
||||
private static void drawImage(PixelWriter writer, PixelReader reader,
|
||||
int sx, int sy, int sw, int sh,
|
||||
int dx, int dy, int dw, int dh) {
|
||||
double xScale = ((double) sw) / dw;
|
||||
double yScale = ((double) sh) / dh;
|
||||
|
||||
for (int xOffset = 0; xOffset < dw; xOffset++) {
|
||||
for (int yOffset = 0; yOffset < dh; yOffset++) {
|
||||
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
|
||||
|
||||
// Draw only non-transparent pixels
|
||||
if ((color >>> 24) != 0)
|
||||
writer.setArgb(dx + xOffset, dy + yOffset, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
final WeakReference<Canvas> canvasRef;
|
||||
final ObjectBinding<LoadedTexture> binding;
|
||||
|
||||
SkinBindingChangeListener(Canvas canvas, ObjectBinding<LoadedTexture> binding) {
|
||||
this.canvasRef = new WeakReference<>(canvas);
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends LoadedTexture> observable,
|
||||
LoadedTexture oldValue, LoadedTexture loadedTexture) {
|
||||
Canvas canvas = canvasRef.get();
|
||||
if (canvas != null)
|
||||
drawAvatar(canvas, loadedTexture.image);
|
||||
}
|
||||
}
|
||||
|
||||
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<LoadedTexture> skinBinding) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
|
||||
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
|
||||
listener.changed(skinBinding, null, skinBinding.get());
|
||||
skinBinding.addListener(listener);
|
||||
|
||||
SkinBindingChangeListener.hole.put(canvas, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
|
||||
fxAvatarBinding(canvas, skinBinding(service, uuid));
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, Account account) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount)
|
||||
fxAvatarBinding(canvas, skinBinding(account));
|
||||
else {
|
||||
unbindAvatar(canvas);
|
||||
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unbindAvatar(Canvas canvas) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
}
|
||||
}
|
||||
// ====
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyListProperty;
|
||||
import javafx.beans.property.ReadOnlyListWrapper;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
@@ -36,14 +37,18 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
@@ -81,7 +86,6 @@ public final class Accounts {
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
@@ -130,59 +134,20 @@ public final class Accounts {
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
}
|
||||
|
||||
private static final String GLOBAL_PREFIX = "$GLOBAL:";
|
||||
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
|
||||
|
||||
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
|
||||
|
||||
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
|
||||
{
|
||||
accounts.addListener(onInvalidating(this::invalidated));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
// this methods first checks whether the current selection is valid
|
||||
// if it's valid, the underlying storage will be updated
|
||||
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
|
||||
Account selected = get();
|
||||
if (accounts.isEmpty()) {
|
||||
if (selected == null) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone, we can only set it to null here
|
||||
set(null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (accounts.contains(selected)) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone
|
||||
set(accounts.get(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// selection is valid, store it
|
||||
if (!initialized)
|
||||
return;
|
||||
updateAccountStorages();
|
||||
}
|
||||
};
|
||||
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
|
||||
|
||||
/**
|
||||
* True if {@link #init()} hasn't been called.
|
||||
*/
|
||||
private static boolean initialized = false;
|
||||
|
||||
static {
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
}
|
||||
|
||||
private static Map<Object, Object> getAccountStorage(Account account) {
|
||||
Map<Object, Object> storage = account.toStorage();
|
||||
storage.put("type", getLoginType(getAccountFactory(account)));
|
||||
if (account == selectedAccount.get()) {
|
||||
storage.put("selected", true);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
@@ -192,7 +157,67 @@ public final class Accounts {
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
|
||||
|
||||
ArrayList<Map<Object, Object>> global = new ArrayList<>();
|
||||
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
|
||||
|
||||
for (Account account : accounts) {
|
||||
Map<Object, Object> storage = getAccountStorage(account);
|
||||
if (account.isPortable())
|
||||
portable.add(storage);
|
||||
else
|
||||
global.add(storage);
|
||||
}
|
||||
|
||||
if (!global.equals(globalAccountStorages))
|
||||
globalAccountStorages.setAll(global);
|
||||
if (!portable.equals(config().getAccountStorages()))
|
||||
config().getAccountStorages().setAll(portable);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void loadGlobalAccountStorages() {
|
||||
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
|
||||
if (Files.exists(globalAccountsFile)) {
|
||||
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
|
||||
globalAccountStorages.setAll((List<Map<Object, Object>>)
|
||||
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
|
||||
}.getType()));
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load global accounts", e);
|
||||
}
|
||||
}
|
||||
|
||||
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
|
||||
LOG.info("Saving global accounts");
|
||||
synchronized (globalAccountsFile) {
|
||||
try {
|
||||
synchronized (globalAccountsFile) {
|
||||
FileUtils.saveSafely(globalAccountsFile, json);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to save global accounts", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalAccountStorages.addListener(onInvalidating(() ->
|
||||
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
|
||||
}
|
||||
|
||||
private static Account parseAccount(Map<Object, Object> storage) {
|
||||
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
||||
if (factory == null) {
|
||||
LOG.warning("Unrecognized account type: " + storage);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return factory.fromStorage(storage);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,53 +227,112 @@ public final class Accounts {
|
||||
if (initialized)
|
||||
throw new IllegalStateException("Already initialized");
|
||||
|
||||
// load accounts
|
||||
config().getAccountStorages().forEach(storage -> {
|
||||
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
||||
if (factory == null) {
|
||||
LOG.warning("Unrecognized account type: " + storage);
|
||||
return;
|
||||
}
|
||||
Account account;
|
||||
try {
|
||||
account = factory.fromStorage(storage);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
|
||||
return;
|
||||
}
|
||||
accounts.add(account);
|
||||
if (!config().isAddedLittleSkin()) {
|
||||
AuthlibInjectorServer littleSkin = new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/");
|
||||
|
||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||
selectedAccount.set(account);
|
||||
if (config().getAuthlibInjectorServers().stream().noneMatch(it -> littleSkin.getUrl().equals(it.getUrl()))) {
|
||||
config().getAuthlibInjectorServers().add(0, littleSkin);
|
||||
}
|
||||
});
|
||||
|
||||
config().setAddedLittleSkin(true);
|
||||
}
|
||||
|
||||
loadGlobalAccountStorages();
|
||||
|
||||
// load accounts
|
||||
Account selected = null;
|
||||
for (Map<Object, Object> storage : config().getAccountStorages()) {
|
||||
Account account = parseAccount(storage);
|
||||
if (account != null) {
|
||||
account.setPortable(true);
|
||||
accounts.add(account);
|
||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||
selected = account;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Map<Object, Object> storage : globalAccountStorages) {
|
||||
Account account = parseAccount(storage);
|
||||
if (account != null) {
|
||||
accounts.add(account);
|
||||
}
|
||||
}
|
||||
|
||||
String selectedAccountIdentifier = config().getSelectedAccount();
|
||||
if (selected == null && selectedAccountIdentifier != null) {
|
||||
boolean portable = true;
|
||||
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
|
||||
portable = false;
|
||||
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
|
||||
}
|
||||
|
||||
for (Account account : accounts) {
|
||||
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
|
||||
if (portable == account.isPortable()) {
|
||||
selected = account;
|
||||
break;
|
||||
} else if (selected == null) {
|
||||
selected = account;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null && !accounts.isEmpty()) {
|
||||
selected = accounts.get(0);
|
||||
}
|
||||
|
||||
selectedAccount.set(selected);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
// this method first checks whether the current selection is valid
|
||||
// if it's valid, the underlying storage will be updated
|
||||
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
|
||||
Account account = selectedAccount.get();
|
||||
if (accounts.isEmpty()) {
|
||||
if (account == null) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone, we can only set it to null here
|
||||
selectedAccount.set(null);
|
||||
}
|
||||
} else {
|
||||
if (accounts.contains(account)) {
|
||||
// valid
|
||||
} else {
|
||||
// the previously selected account is gone
|
||||
selectedAccount.set(accounts.get(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
selectedAccount.addListener(listener);
|
||||
selectedAccount.addListener(onInvalidating(() -> {
|
||||
Account account = selectedAccount.get();
|
||||
if (account != null)
|
||||
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
|
||||
else
|
||||
config().setSelectedAccount(null);
|
||||
}));
|
||||
accounts.addListener(listener);
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
|
||||
initialized = true;
|
||||
|
||||
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
||||
|
||||
Account selected = selectedAccount.get();
|
||||
if (selected != null) {
|
||||
Account finalSelected = selected;
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
selected.logIn();
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
|
||||
finalSelected.logIn();
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!config().getAuthlibInjectorServers().isEmpty()) {
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
}
|
||||
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
|
||||
}
|
||||
});
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
@@ -267,10 +351,6 @@ public final class Accounts {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
public static ReadOnlyListProperty<Account> accountsProperty() {
|
||||
return accountsWrapper.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static Account getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
@@ -348,7 +428,11 @@ public final class Accounts {
|
||||
if (exception instanceof NoCharacterException) {
|
||||
return i18n("account.failed.no_character");
|
||||
} else if (exception instanceof ServerDisconnectException) {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
if (exception.getCause() instanceof SSLException) {
|
||||
return i18n("account.failed.ssl");
|
||||
} else {
|
||||
return i18n("account.failed.connect_authentication_server");
|
||||
}
|
||||
} else if (exception instanceof ServerResponseMalformedException) {
|
||||
return i18n("account.failed.server_response_malformed");
|
||||
} else if (exception instanceof RemoteAuthenticationException) {
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
@@ -52,7 +51,7 @@ public final class Config implements Cloneable, Observable {
|
||||
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
|
||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
public static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||
@@ -120,7 +119,7 @@ public final class Config implements Cloneable, Observable {
|
||||
private DoubleProperty height = new SimpleDoubleProperty();
|
||||
|
||||
@SerializedName("theme")
|
||||
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
|
||||
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>();
|
||||
|
||||
@SerializedName("localization")
|
||||
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
||||
@@ -141,13 +140,16 @@ public final class Config implements Cloneable, Observable {
|
||||
private StringProperty versionListSource = new SimpleStringProperty("balanced");
|
||||
|
||||
@SerializedName("configurations")
|
||||
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
||||
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
|
||||
|
||||
@SerializedName("selectedAccount")
|
||||
private StringProperty selectedAccount = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("accounts")
|
||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||
|
||||
@SerializedName("fontFamily")
|
||||
private StringProperty fontFamily = new SimpleStringProperty(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace");
|
||||
private StringProperty fontFamily = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("fontSize")
|
||||
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
|
||||
@@ -164,6 +166,9 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
|
||||
|
||||
@SerializedName("addedLittleSkin")
|
||||
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("promptedVersion")
|
||||
private StringProperty promptedVersion = new SimpleStringProperty();
|
||||
|
||||
@@ -186,6 +191,12 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("preferredLoginType")
|
||||
private StringProperty preferredLoginType = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("animationDisabled")
|
||||
private BooleanProperty animationDisabled = new SimpleBooleanProperty();
|
||||
|
||||
@SerializedName("shownTips")
|
||||
private ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public Config() {
|
||||
@@ -476,10 +487,22 @@ public final class Config implements Cloneable, Observable {
|
||||
return versionListSource;
|
||||
}
|
||||
|
||||
public ObservableMap<String, Profile> getConfigurations() {
|
||||
public MapProperty<String, Profile> getConfigurations() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public String getSelectedAccount() {
|
||||
return selectedAccount.get();
|
||||
}
|
||||
|
||||
public void setSelectedAccount(String selectedAccount) {
|
||||
this.selectedAccount.set(selectedAccount);
|
||||
}
|
||||
|
||||
public StringProperty selectedAccountProperty() {
|
||||
return selectedAccount;
|
||||
}
|
||||
|
||||
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
||||
return accountStorages;
|
||||
}
|
||||
@@ -536,6 +559,18 @@ public final class Config implements Cloneable, Observable {
|
||||
return authlibInjectorServers;
|
||||
}
|
||||
|
||||
public boolean isAddedLittleSkin() {
|
||||
return addedLittleSkin.get();
|
||||
}
|
||||
|
||||
public BooleanProperty addedLittleSkinProperty() {
|
||||
return addedLittleSkin;
|
||||
}
|
||||
|
||||
public void setAddedLittleSkin(boolean addedLittleSkin) {
|
||||
this.addedLittleSkin.set(addedLittleSkin);
|
||||
}
|
||||
|
||||
public int getConfigVersion() {
|
||||
return configVersion.get();
|
||||
}
|
||||
@@ -572,6 +607,18 @@ public final class Config implements Cloneable, Observable {
|
||||
return preferredLoginType;
|
||||
}
|
||||
|
||||
public boolean isAnimationDisabled() {
|
||||
return animationDisabled.get();
|
||||
}
|
||||
|
||||
public BooleanProperty animationDisabledProperty() {
|
||||
return animationDisabled;
|
||||
}
|
||||
|
||||
public void setAnimationDisabled(boolean animationDisabled) {
|
||||
this.animationDisabled.set(animationDisabled);
|
||||
}
|
||||
|
||||
public boolean isTitleTransparent() {
|
||||
return titleTransparent.get();
|
||||
}
|
||||
@@ -595,4 +642,8 @@ public final class Config implements Cloneable, Observable {
|
||||
public void setPromptedVersion(String promptedVersion) {
|
||||
this.promptedVersion.set(promptedVersion);
|
||||
}
|
||||
|
||||
public ObservableMap<String, Object> getShownTips() {
|
||||
return shownTips;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ 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.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -121,9 +121,8 @@ public final class ConfigHolder {
|
||||
private static Path locateConfig() {
|
||||
Path exePath = Paths.get("").toAbsolutePath();
|
||||
try {
|
||||
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toURI()).toAbsolutePath();
|
||||
if (Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
Path jarPath = JarUtils.thisJar().orElse(null);
|
||||
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
jarPath = jarPath.getParent();
|
||||
exePath = jarPath;
|
||||
|
||||
@@ -208,28 +207,6 @@ public final class ConfigHolder {
|
||||
// Global Config
|
||||
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
// Migrate from old directory
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Files.notExists(GLOBAL_CONFIG_PATH)) {
|
||||
Path oldHome;
|
||||
String xdgCache = System.getenv("XDG_CACHE_HOME");
|
||||
if (StringUtils.isNotBlank(xdgCache)) {
|
||||
oldHome = Paths.get(xdgCache, "hmcl");
|
||||
} else {
|
||||
oldHome = Paths.get(System.getProperty("user.home", "."), ".cache", "hmcl");
|
||||
}
|
||||
|
||||
if (Files.exists(oldHome)) {
|
||||
Path oldConfigPath = oldHome.resolve("config.json");
|
||||
if (Files.isRegularFile(oldConfigPath)) {
|
||||
try {
|
||||
Files.copy(oldConfigPath, GLOBAL_CONFIG_PATH);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to migrate global config", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
|
||||
@@ -108,22 +108,5 @@ final class ConfigUpgrader {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tryCast(rawJson.get("selectedAccount"), String.class)
|
||||
.ifPresent(selected -> {
|
||||
deserialized.getAccountStorages().stream()
|
||||
.filter(storage -> {
|
||||
Object type = storage.get("type");
|
||||
if ("offline".equals(type)) {
|
||||
return selected.equals(storage.get("username") + ":" + storage.get("username"));
|
||||
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
|
||||
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.ifPresent(storage -> storage.put("selected", true));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,42 +21,23 @@ 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 javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
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);
|
||||
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -70,8 +51,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
|
||||
private IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private StringProperty multiplayerToken = new SimpleStringProperty();
|
||||
|
||||
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
|
||||
|
||||
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
||||
@@ -95,7 +74,7 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return CONFIG_GSON.toJson(this);
|
||||
return Config.CONFIG_GSON.toJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,18 +130,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
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",
|
||||
@@ -181,7 +148,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("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()) {
|
||||
@@ -200,7 +166,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.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));
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
@@ -29,8 +30,8 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
@@ -130,8 +131,11 @@ public final class Profiles {
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getConfigurations().clear();
|
||||
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
|
||||
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
|
||||
for (Profile profile : profiles) {
|
||||
newConfigurations.put(profile.getName(), profile);
|
||||
}
|
||||
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
@@ -54,6 +55,7 @@ public final class Settings {
|
||||
Accounts.init();
|
||||
Profiles.init();
|
||||
AuthlibInjectorServers.init();
|
||||
AnimationUtils.init();
|
||||
|
||||
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
|
||||
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
|
||||
|
||||
@@ -23,17 +23,21 @@ import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
@@ -50,6 +54,39 @@ public class Theme {
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
private static Charset cssCharset;
|
||||
|
||||
private static Charset getCssCharset() {
|
||||
if (cssCharset != null)
|
||||
return cssCharset;
|
||||
|
||||
Charset defaultCharset = Charset.defaultCharset();
|
||||
if (defaultCharset != StandardCharsets.UTF_8) {
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
// For JavaFX 17 or earlier, native encoding should be used
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
if (jfxVersion != null) {
|
||||
Matcher matcher = Pattern.compile("^(?<version>[0-9]+)").matcher(jfxVersion);
|
||||
if (matcher.find()) {
|
||||
int v = Lang.parseInt(matcher.group(), -1);
|
||||
if (v >= 18) {
|
||||
cssCharset = StandardCharsets.UTF_8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cssCharset == null)
|
||||
cssCharset = defaultCharset;
|
||||
|
||||
return cssCharset;
|
||||
}
|
||||
|
||||
public static Theme getTheme() {
|
||||
Theme theme = config().getTheme();
|
||||
return theme == null ? BLUE : theme;
|
||||
}
|
||||
|
||||
private final Color paint;
|
||||
private final String color;
|
||||
private final String name;
|
||||
@@ -73,7 +110,7 @@ public class Theme {
|
||||
}
|
||||
|
||||
public boolean isLight() {
|
||||
return Color.web(color).grayscale().getRed() >= 0.5;
|
||||
return paint.grayscale().getRed() >= 0.5;
|
||||
}
|
||||
|
||||
public Color getForegroundColor() {
|
||||
@@ -81,33 +118,32 @@ public class Theme {
|
||||
}
|
||||
|
||||
public String[] getStylesheets(String overrideFontFamily) {
|
||||
Color textFill = getForegroundColor();
|
||||
String css = "/assets/css/blue.css";
|
||||
|
||||
String fontFamily = System.getProperty("hmcl.font.override", overrideFontFamily);
|
||||
|
||||
String css;
|
||||
try {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
|
||||
.replace("%base-color%", color)
|
||||
.replace("%base-red%", Integer.toString((int)Math.ceil(paint.getRed() * 256)))
|
||||
.replace("%base-green%", Integer.toString((int)Math.ceil(paint.getGreen() * 256)))
|
||||
.replace("%base-blue%", Integer.toString((int)Math.ceil(paint.getBlue() * 256)))
|
||||
.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(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse("")));
|
||||
temp.deleteOnExit();
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
css = "/assets/css/blue.css";
|
||||
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
|
||||
Color textFill = getForegroundColor();
|
||||
try {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
String themeText = IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
|
||||
.replace("%base-color%", color)
|
||||
.replace("%base-red%", Integer.toString((int) Math.ceil(paint.getRed() * 256)))
|
||||
.replace("%base-green%", Integer.toString((int) Math.ceil(paint.getGreen() * 256)))
|
||||
.replace("%base-blue%", Integer.toString((int) Math.ceil(paint.getBlue() * 256)))
|
||||
.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(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse(""));
|
||||
FileUtils.writeText(temp, themeText, getCssCharset());
|
||||
temp.deleteOnExit();
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{
|
||||
css,
|
||||
"/assets/css/root.css"
|
||||
};
|
||||
return new String[]{css, "/assets/css/root.css"};
|
||||
}
|
||||
|
||||
public static Theme custom(String color) {
|
||||
@@ -119,50 +155,69 @@ public class Theme {
|
||||
public static Optional<Theme> getTheme(String name) {
|
||||
if (name == null)
|
||||
return Optional.empty();
|
||||
else if (name.equalsIgnoreCase("blue"))
|
||||
return Optional.of(custom("#5C6BC0"));
|
||||
else if (name.equalsIgnoreCase("darker_blue"))
|
||||
return Optional.of(custom("#283593"));
|
||||
else if (name.equalsIgnoreCase("green"))
|
||||
return Optional.of(custom("#43A047"));
|
||||
else if (name.equalsIgnoreCase("orange"))
|
||||
return Optional.of(custom("#E67E22"));
|
||||
else if (name.equalsIgnoreCase("purple"))
|
||||
return Optional.of(custom("#9C27B0"));
|
||||
else if (name.equalsIgnoreCase("red"))
|
||||
return Optional.of(custom("#F44336"));
|
||||
|
||||
if (name.startsWith("#"))
|
||||
else if (name.startsWith("#"))
|
||||
try {
|
||||
Color.web(name);
|
||||
return Optional.of(custom(name));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
else {
|
||||
String color = null;
|
||||
switch (name.toLowerCase(Locale.ROOT)) {
|
||||
case "blue":
|
||||
return Optional.of(BLUE);
|
||||
case "darker_blue":
|
||||
color = "#283593";
|
||||
break;
|
||||
case "green":
|
||||
color = "#43A047";
|
||||
break;
|
||||
case "orange":
|
||||
color = "#E67E22";
|
||||
break;
|
||||
case "purple":
|
||||
color = "#9C27B0";
|
||||
break;
|
||||
case "red":
|
||||
color = "#F44336";
|
||||
}
|
||||
if (color != null)
|
||||
return Optional.of(new Theme(name, color));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static String getColorDisplayName(Color c) {
|
||||
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
|
||||
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase(Locale.ROOT) : null;
|
||||
}
|
||||
|
||||
private static final ObjectBinding<Color> BLACK_FILL = Bindings.createObjectBinding(() -> BLACK);
|
||||
private static final ObjectBinding<Color> WHITE_FILL = Bindings.createObjectBinding(() -> Color.WHITE);
|
||||
private static ObjectBinding<Color> FOREGROUND_FILL;
|
||||
|
||||
public static ObjectBinding<Color> foregroundFillBinding() {
|
||||
return BindingMapping.of(config().themeProperty())
|
||||
.map(Theme::getForegroundColor);
|
||||
if (FOREGROUND_FILL == null)
|
||||
FOREGROUND_FILL = Bindings.createObjectBinding(
|
||||
() -> Theme.getTheme().getForegroundColor(),
|
||||
config().themeProperty()
|
||||
);
|
||||
|
||||
return FOREGROUND_FILL;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> blackFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> BLACK);
|
||||
return BLACK_FILL;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Color> whiteFillBinding() {
|
||||
return Bindings.createObjectBinding(() -> Color.WHITE);
|
||||
return WHITE_FILL;
|
||||
}
|
||||
|
||||
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
|
||||
@Override
|
||||
public void write(JsonWriter out, Theme value) throws IOException {
|
||||
out.value(value.getName().toLowerCase());
|
||||
out.value(value.getName().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,7 +27,8 @@ public enum VersionIconType {
|
||||
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||
FABRIC("/assets/img/fabric.png"),
|
||||
FORGE("/assets/img/forge.png"),
|
||||
FURNACE("/assets/img/furnace.png");
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
@@ -37,12 +38,11 @@ import java.lang.reflect.Type;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -341,6 +341,20 @@ public final class VersionSetting implements Cloneable {
|
||||
minecraftArgsProperty.set(minecraftArgs);
|
||||
}
|
||||
|
||||
private final StringProperty environmentVariablesProperty = new SimpleStringProperty(this, "environmentVariables", "");
|
||||
|
||||
public StringProperty environmentVariablesProperty() {
|
||||
return environmentVariablesProperty;
|
||||
}
|
||||
|
||||
public String getEnvironmentVariables() {
|
||||
return environmentVariablesProperty.get();
|
||||
}
|
||||
|
||||
public void setEnvironmentVariables(String env) {
|
||||
environmentVariablesProperty.set(env);
|
||||
}
|
||||
|
||||
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
|
||||
|
||||
public BooleanProperty noJVMArgsProperty() {
|
||||
@@ -483,7 +497,6 @@ public final class VersionSetting implements Cloneable {
|
||||
widthProperty.set(width);
|
||||
}
|
||||
|
||||
|
||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
|
||||
|
||||
public IntegerProperty heightProperty() {
|
||||
@@ -554,18 +567,18 @@ public final class VersionSetting implements Cloneable {
|
||||
processPriorityProperty.set(processPriority);
|
||||
}
|
||||
|
||||
private final BooleanProperty useSoftwareRenderer = new SimpleBooleanProperty(this, "softwareRenderer", false);
|
||||
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
|
||||
|
||||
public boolean isUseSoftwareRenderer() {
|
||||
return useSoftwareRenderer.get();
|
||||
public Renderer getRenderer() {
|
||||
return rendererProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty useSoftwareRendererProperty() {
|
||||
return useSoftwareRenderer;
|
||||
public ObjectProperty<Renderer> rendererProperty() {
|
||||
return rendererProperty;
|
||||
}
|
||||
|
||||
public void setUseSoftwareRenderer(boolean useSoftwareRenderer) {
|
||||
this.useSoftwareRenderer.set(useSoftwareRenderer);
|
||||
public void setRenderer(Renderer renderer) {
|
||||
this.rendererProperty.set(renderer);
|
||||
}
|
||||
|
||||
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
|
||||
@@ -660,9 +673,7 @@ public final class VersionSetting implements Cloneable {
|
||||
.filter(java -> java.getVersion().equals(getJava()))
|
||||
.collect(Collectors.toList());
|
||||
if (matchedJava.isEmpty()) {
|
||||
runInFX(() -> {
|
||||
setJava("Auto");
|
||||
});
|
||||
FXUtils.runInFX(() -> setJava("Auto"));
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else {
|
||||
return matchedJava.stream()
|
||||
@@ -695,6 +706,7 @@ public final class VersionSetting implements Cloneable {
|
||||
postExitCommand.addListener(listener);
|
||||
javaArgsProperty.addListener(listener);
|
||||
minecraftArgsProperty.addListener(listener);
|
||||
environmentVariablesProperty.addListener(listener);
|
||||
noJVMArgsProperty.addListener(listener);
|
||||
notCheckGameProperty.addListener(listener);
|
||||
notCheckJVMProperty.addListener(listener);
|
||||
@@ -707,7 +719,7 @@ public final class VersionSetting implements Cloneable {
|
||||
gameDirTypeProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
processPriorityProperty.addListener(listener);
|
||||
useSoftwareRenderer.addListener(listener);
|
||||
rendererProperty.addListener(listener);
|
||||
useNativeGLFW.addListener(listener);
|
||||
useNativeOpenAL.addListener(listener);
|
||||
launcherVisibilityProperty.addListener(listener);
|
||||
@@ -733,6 +745,7 @@ public final class VersionSetting implements Cloneable {
|
||||
versionSetting.setPostExitCommand(getPostExitCommand());
|
||||
versionSetting.setJavaArgs(getJavaArgs());
|
||||
versionSetting.setMinecraftArgs(getMinecraftArgs());
|
||||
versionSetting.setEnvironmentVariables(getEnvironmentVariables());
|
||||
versionSetting.setNoJVMArgs(isNoJVMArgs());
|
||||
versionSetting.setNotCheckGame(isNotCheckGame());
|
||||
versionSetting.setNotCheckJVM(isNotCheckJVM());
|
||||
@@ -745,7 +758,7 @@ public final class VersionSetting implements Cloneable {
|
||||
versionSetting.setGameDirType(getGameDirType());
|
||||
versionSetting.setGameDir(getGameDir());
|
||||
versionSetting.setProcessPriority(getProcessPriority());
|
||||
versionSetting.setUseSoftwareRenderer(isUseSoftwareRenderer());
|
||||
versionSetting.setRenderer(getRenderer());
|
||||
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
|
||||
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
|
||||
versionSetting.setLauncherVisibility(getLauncherVisibility());
|
||||
@@ -763,6 +776,7 @@ public final class VersionSetting implements Cloneable {
|
||||
obj.addProperty("usesGlobal", src.isUsesGlobal());
|
||||
obj.addProperty("javaArgs", src.getJavaArgs());
|
||||
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
|
||||
obj.addProperty("environmentVariables", src.getEnvironmentVariables());
|
||||
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
|
||||
obj.addProperty("minMemory", src.getMinMemory());
|
||||
obj.addProperty("autoMemory", src.isAutoMemory());
|
||||
@@ -784,7 +798,6 @@ public final class VersionSetting implements Cloneable {
|
||||
obj.addProperty("gameDir", src.getGameDir());
|
||||
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
|
||||
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
|
||||
obj.addProperty("useSoftwareRenderer", src.isUseSoftwareRenderer());
|
||||
obj.addProperty("useNativeGLFW", src.isUseNativeGLFW());
|
||||
obj.addProperty("useNativeOpenAL", src.isUseNativeOpenAL());
|
||||
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
|
||||
@@ -793,12 +806,24 @@ public final class VersionSetting implements Cloneable {
|
||||
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
|
||||
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
|
||||
|
||||
obj.addProperty("renderer", src.getRenderer().name());
|
||||
if (src.getRenderer() == Renderer.LLVMPIPE)
|
||||
obj.addProperty("useSoftwareRenderer", true);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
|
||||
if (index == null)
|
||||
return defaultValue;
|
||||
|
||||
int idx = index.getAsInt();
|
||||
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
|
||||
if (!(json instanceof JsonObject))
|
||||
return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
@@ -810,6 +835,7 @@ public final class VersionSetting implements Cloneable {
|
||||
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setEnvironmentVariables(Optional.ofNullable(obj.get("environmentVariables")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setMaxMemory(maxMemoryN);
|
||||
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
|
||||
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
|
||||
@@ -829,15 +855,26 @@ public final class VersionSetting implements Cloneable {
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(LauncherVisibility.HIDE.ordinal())]);
|
||||
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
|
||||
vs.setUseSoftwareRenderer(Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
|
||||
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
|
||||
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setVersionIcon(VersionIconType.values()[Optional.ofNullable(obj.get("versionIcon")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
|
||||
|
||||
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
|
||||
.flatMap(name -> {
|
||||
try {
|
||||
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ public final class AwtUtils {
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
|
||||
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
|
||||
@@ -19,7 +19,10 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
@@ -35,6 +38,7 @@ import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
@@ -45,7 +49,6 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
@@ -61,12 +64,12 @@ import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
private static InvalidationListener stageSizeChangeListener;
|
||||
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
|
||||
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
|
||||
|
||||
@@ -89,11 +92,10 @@ public final class Controllers {
|
||||
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
|
||||
AccountListPage accountListPage = new AccountListPage();
|
||||
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
|
||||
accountListPage.accountsProperty().bindContent(Accounts.accountsProperty());
|
||||
accountListPage.accountsProperty().bindContent(Accounts.getAccounts());
|
||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
|
||||
private Controllers() {
|
||||
@@ -122,11 +124,6 @@ public final class Controllers {
|
||||
return rootPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static MultiplayerPage getMultiplayerPage() {
|
||||
return multiplayerPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static LauncherSettingsPage getSettingsPage() {
|
||||
return settingsPage.get();
|
||||
@@ -148,10 +145,15 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void onApplicationStop() {
|
||||
config().setHeight(stageHeight.get());
|
||||
config().setWidth(stageWidth.get());
|
||||
stageHeight = null;
|
||||
stageWidth = null;
|
||||
stageSizeChangeListener = null;
|
||||
if (stageHeight != null) {
|
||||
config().setHeight(stageHeight.get());
|
||||
stageHeight = null;
|
||||
}
|
||||
if (stageWidth != null) {
|
||||
config().setWidth(stageWidth.get());
|
||||
stageWidth = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
@@ -159,10 +161,28 @@ public final class Controllers {
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
stage.setWidth(config().getWidth());
|
||||
stageWidth.bind(stage.widthProperty());
|
||||
stageSizeChangeListener = o -> {
|
||||
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
|
||||
DoubleProperty targetProperty = "width".equals(sourceProperty.getName()) ? stageWidth : stageHeight;
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
|
||||
|
||||
double initHeight = config().getHeight();
|
||||
double initWidth = config().getWidth();
|
||||
|
||||
stage.setHeight(initHeight);
|
||||
stage.setWidth(initWidth);
|
||||
stageHeight.set(initHeight);
|
||||
stageWidth.set(initWidth);
|
||||
stage.heightProperty().addListener(weakListener);
|
||||
stage.widthProperty().addListener(weakListener);
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
@@ -182,7 +202,7 @@ 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(config().getLauncherFontFamily()));
|
||||
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
|
||||
stage.getIcons().add(newImage("/assets/img/icon.png"));
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
@@ -207,7 +227,7 @@ public final class Controllers {
|
||||
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));
|
||||
agreementLink.setExternalLink(Metadata.EULA_URL);
|
||||
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
yesButton.setOnAction(e -> {
|
||||
@@ -216,9 +236,7 @@ public final class Controllers {
|
||||
});
|
||||
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
noButton.setOnAction(e -> {
|
||||
System.exit(1);
|
||||
});
|
||||
noButton.setOnAction(e -> System.exit(1));
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
@@ -305,9 +323,14 @@ public final class Controllers {
|
||||
|
||||
public static void onHyperlinkAction(String href) {
|
||||
if (href.startsWith("hmcl://")) {
|
||||
if ("hmcl://settings/feedback".equals(href)) {
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
switch (href) {
|
||||
case "hmcl://settings/feedback":
|
||||
Controllers.getSettingsPage().showFeedback();
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
break;
|
||||
case "hmcl://hide-announcement":
|
||||
Controllers.getRootPage().getMainPage().hideAnnouncementPane();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
FXUtils.openLink(href);
|
||||
@@ -322,9 +345,12 @@ public final class Controllers {
|
||||
rootPage = null;
|
||||
versionPage = null;
|
||||
gameListPage = null;
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
onApplicationStop();
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,13 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.image.Image;
|
||||
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;
|
||||
@@ -50,14 +47,16 @@ import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
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.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
@@ -70,8 +69,8 @@ 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.*;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
@@ -95,6 +94,8 @@ public final class FXUtils {
|
||||
private FXUtils() {
|
||||
}
|
||||
|
||||
public static String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
|
||||
public static void runInFX(Runnable runnable) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
@@ -275,18 +276,8 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void smoothScrolling(ScrollPane scrollPane) {
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public static void loadFXML(Node node, String absolutePath) {
|
||||
FXMLLoader loader = new FXMLLoader(node.getClass().getResource(absolutePath), I18n.getResourceBundle());
|
||||
loader.setRoot(node);
|
||||
loader.setController(node);
|
||||
try {
|
||||
loader.load();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
if (AnimationUtils.isAnimationEnabled())
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
@@ -357,53 +348,77 @@ public final class FXUtils {
|
||||
|
||||
public static void openFolder(File file) {
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = file.getAbsolutePath();
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case OSX:
|
||||
String openCommand;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && new File("/usr/bin/xdg-open").exists())
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
else
|
||||
openCommand = null;
|
||||
|
||||
thread(() -> {
|
||||
if (openCommand != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Open " + path + " failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Unable to open " + path + " by executing " + openCommand, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
thread(() -> {
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to java.awt.Desktop::open
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
String path = file.toAbsolutePath().toString();
|
||||
|
||||
String[] openCommands;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommands = new String[]{"explorer.exe", "/select,", path};
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommands = new String[]{"/usr/bin/open", "-R", path};
|
||||
else
|
||||
openCommands = null;
|
||||
|
||||
if (openCommands != null) {
|
||||
thread(() -> {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select," + file, e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommands);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Unable to show " + path + " in explorer", e);
|
||||
}
|
||||
break;
|
||||
case OSX:
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", "-R", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing /usr/bin/open -R " + file, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// We do not have an universal method to show file in file manager.
|
||||
|
||||
// Fallback to open folder
|
||||
openFolder(file.getParent().toFile());
|
||||
break;
|
||||
});
|
||||
} else {
|
||||
// We do not have a universal method to show file in file manager.
|
||||
openFolder(file.getParent().toFile());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,34 +441,38 @@ public final class FXUtils {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showWebDialog(String title, String content) {
|
||||
@@ -469,12 +488,12 @@ public final class FXUtils {
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
|
||||
|
||||
SwingUtils.initLookAndFeel();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
final JFrame frame = new JFrame(title);
|
||||
frame.setSize(width, height);
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setLocationByPlatform(true);
|
||||
//noinspection ConstantConditions
|
||||
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
||||
frame.setLayout(new BorderLayout());
|
||||
|
||||
@@ -502,20 +521,85 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
|
||||
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
|
||||
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
|
||||
textField.focusedProperty().addListener((ChangeListener<Boolean>) listener);
|
||||
property.addListener(listener);
|
||||
}
|
||||
|
||||
public static void unbindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().unbindBidirectional(property);
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
bind(textField, property, SafeStringConverter.fromInteger());
|
||||
}
|
||||
|
||||
public static void bindString(JFXTextField textField, Property<String> property) {
|
||||
textField.textProperty().bindBidirectional(property);
|
||||
bind(textField, property, null);
|
||||
}
|
||||
|
||||
public static void unbindString(JFXTextField textField, Property<String> property) {
|
||||
textField.textProperty().unbindBidirectional(property);
|
||||
public static void unbind(JFXTextField textField, Property<?> property) {
|
||||
TextFieldBindingListener<?> listener = new TextFieldBindingListener<>(textField, property, null);
|
||||
textField.focusedProperty().removeListener((ChangeListener<Boolean>) listener);
|
||||
property.removeListener(listener);
|
||||
}
|
||||
|
||||
private static final class TextFieldBindingListener<T> implements ChangeListener<Boolean>, InvalidationListener {
|
||||
private final int hashCode;
|
||||
private final WeakReference<JFXTextField> textFieldRef;
|
||||
private final WeakReference<Property<T>> propertyRef;
|
||||
private final StringConverter<T> converter;
|
||||
|
||||
TextFieldBindingListener(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
this.textFieldRef = new WeakReference<>(textField);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.converter = converter;
|
||||
this.hashCode = System.identityHashCode(textField) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean focused) { // On TextField changed
|
||||
JFXTextField textField = textFieldRef.get();
|
||||
Property<T> property = this.propertyRef.get();
|
||||
|
||||
if (textField != null && property != null && oldValue == Boolean.TRUE && focused == Boolean.FALSE) {
|
||||
if (textField.validate()) {
|
||||
String newText = textField.getText();
|
||||
@SuppressWarnings("unchecked")
|
||||
T newValue = converter == null ? (T) newText : converter.fromString(newText);
|
||||
|
||||
if (!Objects.equals(newValue, property.getValue()))
|
||||
property.setValue(newValue);
|
||||
} else {
|
||||
// Rollback to old value
|
||||
invalidated(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) { // On property change
|
||||
JFXTextField textField = textFieldRef.get();
|
||||
Property<T> property = this.propertyRef.get();
|
||||
|
||||
if (textField != null && property != null) {
|
||||
T value = property.getValue();
|
||||
textField.setText(converter == null ? (String) value : converter.toString(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof TextFieldBindingListener))
|
||||
return false;
|
||||
TextFieldBindingListener<?> other = (TextFieldBindingListener<?>) obj;
|
||||
return this.hashCode == other.hashCode
|
||||
&& this.textFieldRef.get() == other.textFieldRef.get()
|
||||
&& this.propertyRef.get() == other.propertyRef.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
|
||||
@@ -541,16 +625,17 @@ public final class FXUtils {
|
||||
* @param comboBox the combo box being bound with {@code property}.
|
||||
* @param property the property being bound with {@code combo box}.
|
||||
* @see #unbindEnum(JFXComboBox)
|
||||
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
|
||||
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public static void bindEnum(JFXComboBox<?> comboBox, Property<? extends Enum<?>> property) {
|
||||
public static <T extends Enum<T>> void bindEnum(JFXComboBox<T> comboBox, Property<T> property) {
|
||||
unbindEnum(comboBox);
|
||||
@SuppressWarnings("rawtypes")
|
||||
ChangeListener<Number> listener = (a, b, newValue) ->
|
||||
((Property) property).setValue(property.getValue().getClass().getEnumConstants()[newValue.intValue()]);
|
||||
comboBox.getSelectionModel().select(property.getValue().ordinal());
|
||||
|
||||
T currentValue = property.getValue();
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] enumConstants = (T[]) currentValue.getClass().getEnumConstants();
|
||||
ChangeListener<Number> listener = (a, b, newValue) -> property.setValue(enumConstants[newValue.intValue()]);
|
||||
|
||||
comboBox.getSelectionModel().select(currentValue.ordinal());
|
||||
comboBox.getProperties().put("FXUtils.bindEnum.listener", listener);
|
||||
comboBox.getSelectionModel().selectedIndexProperty().addListener(listener);
|
||||
}
|
||||
@@ -561,15 +646,13 @@ public final class FXUtils {
|
||||
*
|
||||
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
|
||||
* @see #bindEnum(JFXComboBox, Property)
|
||||
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
|
||||
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public static void unbindEnum(JFXComboBox<?> comboBox) {
|
||||
@SuppressWarnings("rawtypes")
|
||||
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
|
||||
if (listener == null) return;
|
||||
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
|
||||
public static void unbindEnum(JFXComboBox<? extends Enum<?>> comboBox) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ChangeListener<Number> listener = (ChangeListener<Number>) comboBox.getProperties().remove("FXUtils.bindEnum.listener");
|
||||
if (listener != null)
|
||||
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,6 +671,13 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static JFXButton newRaisedButton(String text) {
|
||||
JFXButton button = new JFXButton(text);
|
||||
button.getStyleClass().add("jfx-button-raised");
|
||||
button.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
applyDragListener(node, filter, callback, null);
|
||||
}
|
||||
@@ -632,10 +722,17 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
@@ -685,22 +782,6 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
public static WritableImage toFXImage(BufferedImage image) {
|
||||
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
final int iw = image.getWidth();
|
||||
final int ih = image.getHeight();
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
pw.setArgb(x, y, image.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
return wr;
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -38,17 +39,18 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.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;
|
||||
@@ -71,6 +73,7 @@ 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 total_memory;
|
||||
private final String java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
|
||||
@@ -85,9 +88,9 @@ public class GameCrashWindow extends Stage {
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final List<Pair<String, Log4jLevel>> logs;
|
||||
private final Collection<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
@@ -98,6 +101,8 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
|
||||
total_memory = Optional.ofNullable(OperatingSystem.TOTAL_MEMORY).map(i -> i + " MB").orElse("-");
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
|
||||
@@ -107,7 +112,7 @@ public class GameCrashWindow extends Stage {
|
||||
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
|
||||
|
||||
setScene(new Scene(view, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("game.crash.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
@@ -223,7 +228,7 @@ public class GameCrashWindow extends Stage {
|
||||
private void showLogWindow() {
|
||||
LogWindow logWindow = new LogWindow();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
|
||||
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
|
||||
if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
@@ -239,15 +244,11 @@ public class GameCrashWindow extends Stage {
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.thenRunAsync(() -> {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
|
||||
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);
|
||||
@@ -294,6 +295,11 @@ public class GameCrashWindow extends Stage {
|
||||
version.setTitle(i18n("archive.game_version"));
|
||||
version.setSubtitle(GameCrashWindow.this.version.getId());
|
||||
|
||||
TwoLineListItem total_memory = new TwoLineListItem();
|
||||
total_memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
total_memory.setTitle(i18n("settings.physical_memory"));
|
||||
total_memory.setSubtitle(GameCrashWindow.this.total_memory);
|
||||
|
||||
TwoLineListItem memory = new TwoLineListItem();
|
||||
memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
memory.setTitle(i18n("settings.memory"));
|
||||
@@ -314,7 +320,7 @@ public class GameCrashWindow extends Stage {
|
||||
arch.setTitle(i18n("system.architecture"));
|
||||
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
|
||||
|
||||
infoPane.getChildren().setAll(launcher, version, memory, java, os, arch);
|
||||
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
|
||||
}
|
||||
|
||||
HBox moddedPane = new HBox(8);
|
||||
@@ -350,21 +356,28 @@ public class GameCrashWindow extends Stage {
|
||||
Label reasonTitle = new Label(i18n("game.crash.reason"));
|
||||
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
|
||||
|
||||
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
|
||||
reasonPane.setFitToWidth(true);
|
||||
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
|
||||
gameDirPane.setPadding(new Insets(8));
|
||||
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonTextFlow, feedbackTextFlow));
|
||||
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
|
||||
if (visible) {
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane, feedbackTextFlow));
|
||||
} else {
|
||||
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnMouseClicked(e -> exportGameCrashInfo());
|
||||
|
||||
JFXButton logButton = new JFXButton(i18n("logwindow.title"));
|
||||
logButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
logButton.getStyleClass().add("jfx-button-raised");
|
||||
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
|
||||
logButton.setOnMouseClicked(e -> showLogWindow());
|
||||
|
||||
toolBar.setPadding(new Insets(8));
|
||||
|
||||
@@ -207,7 +207,7 @@ public class InstallerItem extends Control {
|
||||
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
|
||||
|
||||
if (control.imageUrl != null) {
|
||||
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
|
||||
ImageView view = new ImageView(new Image(control.imageUrl));
|
||||
Node node = FXUtils.limitingSize(view, 32, 32);
|
||||
node.setMouseTransparent(true);
|
||||
node.getStyleClass().add("installer-item-image");
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -56,7 +55,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
|
||||
@@ -26,6 +26,7 @@ import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -35,22 +36,20 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -92,15 +91,15 @@ public final class LogWindow extends Stage {
|
||||
|
||||
public LogWindow() {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
setTitle(i18n("logwindow.title"));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
|
||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||
}
|
||||
|
||||
public void logLine(String line, Log4jLevel level) {
|
||||
Log log = new Log(Logging.filterForbiddenToken(parseEscapeSequence(line)), level);
|
||||
public void logLine(String filteredLine, Log4jLevel level) {
|
||||
Log log = new Log(parseEscapeSequence(filteredLine), level);
|
||||
logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
@@ -137,7 +136,6 @@ public final class LogWindow extends Stage {
|
||||
while (logs.size() > config().getLogLines()) {
|
||||
Log removedLog = logs.removeFirst();
|
||||
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
|
||||
// TODO: fix O(n)
|
||||
impl.listView.getItems().remove(0);
|
||||
}
|
||||
}
|
||||
@@ -155,31 +153,33 @@ public final class LogWindow extends Stage {
|
||||
|
||||
public class LogWindowImpl extends Control {
|
||||
|
||||
private ListView<Log> listView = new JFXListView<>();
|
||||
private BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
private final ListView<Log> listView = new JFXListView<>();
|
||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
|
||||
|
||||
boolean flag = false;
|
||||
cboLines.getItems().setAll("500", "2000", "5000");
|
||||
cboLines.getItems().setAll("500", "2000", "5000", "10000");
|
||||
for (String i : cboLines.getItems())
|
||||
if (Integer.toString(config().getLogLines()).equals(i)) {
|
||||
cboLines.getSelectionModel().select(i);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(2);
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
});
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(0);
|
||||
|
||||
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
@@ -207,13 +207,13 @@ public final class LogWindow extends Stage {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
}
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
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();
|
||||
});
|
||||
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,13 +229,13 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
|
||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
@@ -291,7 +291,10 @@ public final class LogWindow extends Stage {
|
||||
if (!listView.getItems().isEmpty() && control.autoScroll.get())
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
});
|
||||
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
|
||||
listView.setStyle("-fx-font-family: " + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
|
||||
+ "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
@@ -308,6 +311,12 @@ public final class LogWindow extends Stage {
|
||||
@Override
|
||||
protected void updateItem(Log item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
pseudoClassStateChanged(EMPTY, empty);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
||||
|
||||
@@ -54,6 +54,8 @@ public final class SVG {
|
||||
return svg;
|
||||
}
|
||||
|
||||
public static final String ARROW_RIGHT = "M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z";
|
||||
|
||||
// default fill: white, width: 24, height 24
|
||||
|
||||
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
@@ -273,7 +275,7 @@ public final class SVG {
|
||||
}
|
||||
|
||||
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
|
||||
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width,
|
||||
return createSVGPath(ARROW_RIGHT, fill, width,
|
||||
height);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copy from https://github.com/palexdev/MaterialFX/blob/c8038ce2090f5cddf923a19d79cc601db86a4d17/materialfx/src/main/java/io/github/palexdev/materialfx/utils/ScrollUtils.java
|
||||
|
||||
/*
|
||||
* Copyright (C) 2022 Parisi Alessandro
|
||||
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
|
||||
*
|
||||
* MaterialFX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* MaterialFX 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Animation.Status;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Utility class for ScrollPanes.
|
||||
*/
|
||||
final class ScrollUtils {
|
||||
|
||||
public enum ScrollDirection {
|
||||
UP(-1), RIGHT(-1), DOWN(1), LEFT(1);
|
||||
|
||||
final int intDirection;
|
||||
|
||||
ScrollDirection(int intDirection) {
|
||||
this.intDirection = intDirection;
|
||||
}
|
||||
|
||||
public int intDirection() {
|
||||
return intDirection;
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given ScrollEvent comes from a trackpad.
|
||||
* <p></p>
|
||||
* Although this method works in most cases, it is not very accurate.
|
||||
* Since in JavaFX there's no way to tell if a ScrollEvent comes from a trackpad or a mouse
|
||||
* we use this trick: I noticed that a mouse scroll has a delta of 32 (don't know if it changes depending on the device or OS)
|
||||
* and trackpad scrolls have a way smaller delta. So depending on the scroll direction we check if the delta is lesser than 10
|
||||
* (trackpad event) or greater(mouse event).
|
||||
*
|
||||
* @see ScrollEvent#getDeltaX()
|
||||
* @see ScrollEvent#getDeltaY()
|
||||
*/
|
||||
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
|
||||
switch (scrollDirection) {
|
||||
case UP:
|
||||
case DOWN:
|
||||
return Math.abs(event.getDeltaY()) < 10;
|
||||
case LEFT:
|
||||
case RIGHT:
|
||||
return Math.abs(event.getDeltaX()) < 10;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the scroll direction of the given ScrollEvent.
|
||||
* <p></p>
|
||||
* Although this method works fine, it is not very accurate.
|
||||
* In JavaFX there's no concept of scroll direction, if you try to scroll with a trackpad
|
||||
* you'll notice that you can scroll in both directions at the same time, both deltaX and deltaY won't be 0.
|
||||
* <p></p>
|
||||
* For this method to work we assume that this behavior is not possible.
|
||||
* <p></p>
|
||||
* If deltaY is 0 we return LEFT or RIGHT depending on deltaX (respectively if lesser or greater than 0).
|
||||
* <p>
|
||||
* Else we return DOWN or UP depending on deltaY (respectively if lesser or greater than 0).
|
||||
*
|
||||
* @see ScrollEvent#getDeltaX()
|
||||
* @see ScrollEvent#getDeltaY()
|
||||
*/
|
||||
public static ScrollDirection determineScrollDirection(ScrollEvent event) {
|
||||
double deltaX = event.getDeltaX();
|
||||
double deltaY = event.getDeltaY();
|
||||
|
||||
if (deltaY == 0.0) {
|
||||
return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
|
||||
} else {
|
||||
return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
|
||||
}
|
||||
}
|
||||
|
||||
//================================================================================
|
||||
// ScrollPanes
|
||||
//================================================================================
|
||||
|
||||
/**
|
||||
* Adds a smooth scrolling effect to the given scroll pane,
|
||||
* calls {@link #addSmoothScrolling(ScrollPane, double)} with a
|
||||
* default speed value of 1.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane) {
|
||||
addSmoothScrolling(scrollPane, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
|
||||
* Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
|
||||
* with a default trackPadAdjustment of 7.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
|
||||
addSmoothScrolling(scrollPane, speed, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a smooth scrolling effect to the given scroll pane with the given
|
||||
* scroll speed and the given trackPadAdjustment.
|
||||
* <p></p>
|
||||
* The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
|
||||
* This is kind of a workaround and it's not perfect, but at least it's way better than before.
|
||||
* The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
|
||||
*/
|
||||
public static void addSmoothScrolling(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
|
||||
smoothScroll(scrollPane, speed, trackPadAdjustment);
|
||||
}
|
||||
|
||||
private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
|
||||
final double[] frictions = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
|
||||
final double[] derivatives = new double[frictions.length];
|
||||
AtomicReference<Double> atomicSpeed = new AtomicReference<>(speed);
|
||||
|
||||
Timeline timeline = new Timeline();
|
||||
AtomicReference<ScrollDirection> scrollDirection = new AtomicReference<>();
|
||||
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
|
||||
final EventHandler<ScrollEvent> scrollHandler = event -> {
|
||||
if (event.getEventType() == ScrollEvent.SCROLL) {
|
||||
scrollDirection.set(determineScrollDirection(event));
|
||||
if (isTrackPad(event, scrollDirection.get())) {
|
||||
atomicSpeed.set(speed / trackPadAdjustment);
|
||||
} else {
|
||||
atomicSpeed.set(speed);
|
||||
}
|
||||
derivatives[0] += scrollDirection.get().intDirection * atomicSpeed.get();
|
||||
if (timeline.getStatus() == Status.STOPPED) {
|
||||
timeline.play();
|
||||
}
|
||||
event.consume();
|
||||
}
|
||||
};
|
||||
if (scrollPane.getContent().getParent() != null) {
|
||||
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (oldValue != null) {
|
||||
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
if (newValue != null) {
|
||||
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
|
||||
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
|
||||
}
|
||||
});
|
||||
|
||||
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), event -> {
|
||||
for (int i = 0; i < derivatives.length; i++) {
|
||||
derivatives[i] *= frictions[i];
|
||||
}
|
||||
for (int i = 1; i < derivatives.length; i++) {
|
||||
derivatives[i] += derivatives[i - 1];
|
||||
}
|
||||
|
||||
double dy = derivatives[derivatives.length - 1];
|
||||
Function<Bounds, Double> sizeFunction = (scrollDirection.get() == ScrollDirection.UP || scrollDirection.get() == ScrollDirection.DOWN) ? Bounds::getHeight : Bounds::getWidth;
|
||||
double size = sizeFunction.apply(scrollPane.getContent().getLayoutBounds());
|
||||
double value;
|
||||
switch (scrollDirection.get()) {
|
||||
case LEFT:
|
||||
case RIGHT:
|
||||
value = Math.min(Math.max(scrollPane.hvalueProperty().get() + dy / size, 0), 1);
|
||||
scrollPane.hvalueProperty().set(value);
|
||||
break;
|
||||
case UP:
|
||||
case DOWN:
|
||||
value = Math.min(Math.max(scrollPane.vvalueProperty().get() + dy / size, 0), 1);
|
||||
scrollPane.vvalueProperty().set(value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Math.abs(dy) < 0.001) {
|
||||
timeline.stop();
|
||||
}
|
||||
}));
|
||||
timeline.setCycleCount(Animation.INDEFINITE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public final class SwingUtils {
|
||||
static {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SwingUtils() {
|
||||
}
|
||||
|
||||
public static void initLookAndFeel() {
|
||||
// Make sure the static constructor is called
|
||||
}
|
||||
|
||||
public static void showInfoDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -69,7 +68,7 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
root.getContent().add(scrollPane);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class UpgradeDialog extends JFXDialogLayout {
|
||||
}
|
||||
|
||||
{
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
|
||||
try {
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
|
||||
@@ -24,11 +24,11 @@ import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WeakListenerHolder {
|
||||
private List<Object> refs = new LinkedList<>();
|
||||
private final List<Object> refs = new ArrayList<>(0);
|
||||
|
||||
public WeakListenerHolder() {
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
@@ -42,7 +43,7 @@ public class WebStage extends Stage {
|
||||
|
||||
public WebStage(int width, int height) {
|
||||
setScene(new Scene(pane, width, height));
|
||||
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
getIcons().add(newImage("/assets/img/icon.png"));
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
webView.setContextMenuEnabled(false);
|
||||
|
||||
@@ -22,9 +22,8 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
@@ -34,20 +33,18 @@ import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createStringBinding;
|
||||
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
|
||||
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
private final Tooltip tooltip;
|
||||
private final ImageView imageView;
|
||||
private final Canvas canvas;
|
||||
|
||||
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
@@ -55,17 +52,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
if (account == null) {
|
||||
titleProperty().unbind();
|
||||
subtitleProperty().unbind();
|
||||
imageView.imageProperty().unbind();
|
||||
tooltip.textProperty().unbind();
|
||||
setTitle(i18n("account.missing"));
|
||||
setSubtitle(i18n("account.missing.add"));
|
||||
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
|
||||
tooltip.setText(i18n("account.create"));
|
||||
|
||||
TexturesLoader.unbindAvatar(canvas);
|
||||
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
|
||||
|
||||
} else {
|
||||
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
|
||||
subtitleProperty().bind(accountSubtitle(account));
|
||||
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
tooltip.textProperty().bind(accountTooltip(account));
|
||||
TexturesLoader.bindAvatar(canvas, account);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,9 +73,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
tooltip = new Tooltip();
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
Pair<Node, ImageView> view = createImageView(null);
|
||||
setLeftGraphic(view.getKey());
|
||||
imageView = view.getValue();
|
||||
canvas = new Canvas(32, 32);
|
||||
setLeftGraphic(canvas);
|
||||
|
||||
setActionButtonVisible(false);
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ 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.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
@@ -39,7 +37,6 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -51,9 +48,8 @@ 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.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -70,7 +66,6 @@ public class AccountListItem extends RadioButton {
|
||||
private final Account account;
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
|
||||
|
||||
public AccountListItem(Account account) {
|
||||
this.account = account;
|
||||
@@ -78,13 +73,14 @@ public class AccountListItem extends RadioButton {
|
||||
setUserData(account);
|
||||
|
||||
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
||||
String portableSuffix = account.isPortable() ? ", " + i18n("account.portable") : "";
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
||||
subtitle.bind(Bindings.concat(
|
||||
loginTypeName, ", ", i18n("account.injector.server"), ": ",
|
||||
Bindings.createStringBinding(server::getName, server)));
|
||||
Bindings.createStringBinding(server::getName, server), portableSuffix));
|
||||
} else {
|
||||
subtitle.set(loginTypeName);
|
||||
subtitle.set(loginTypeName + portableSuffix);
|
||||
}
|
||||
|
||||
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
|
||||
@@ -95,8 +91,6 @@ public class AccountListItem extends RadioButton {
|
||||
account.getUsername().isEmpty() ? characterName :
|
||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -173,14 +167,14 @@ public class AccountListItem extends RadioButton {
|
||||
|
||||
return refreshAsync()
|
||||
.thenRunAsync(() -> {
|
||||
BufferedImage skinImg;
|
||||
try {
|
||||
skinImg = ImageIO.read(selectedFile);
|
||||
Image skinImg;
|
||||
try (FileInputStream input = new FileInputStream(selectedFile)) {
|
||||
skinImg = new Image(input);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidSkinException("Failed to read skin image", e);
|
||||
}
|
||||
if (skinImg == null) {
|
||||
throw new InvalidSkinException("Failed to read skin image");
|
||||
if (skinImg.isError()) {
|
||||
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
@@ -226,16 +220,4 @@ public class AccountListItem extends RadioButton {
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image.get();
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
this.image.set(image);
|
||||
}
|
||||
|
||||
public ObjectProperty<Image> imageProperty() {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,17 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -64,9 +66,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
||||
Canvas canvas = new Canvas(32, 32);
|
||||
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
|
||||
|
||||
Label title = new Label();
|
||||
title.getStyleClass().add("title");
|
||||
@@ -84,12 +85,47 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
item.getStyleClass().add("two-line-list-item");
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
center.getChildren().setAll(imageView, item);
|
||||
center.getChildren().setAll(canvas, item);
|
||||
root.setCenter(center);
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnMove = new JFXButton();
|
||||
SpinnerPane spinnerMove = new SpinnerPane();
|
||||
spinnerMove.getStyleClass().add("small-spinner-pane");
|
||||
btnMove.setOnMouseClicked(e -> {
|
||||
Account account = skinnable.getAccount();
|
||||
Accounts.getAccounts().remove(account);
|
||||
if (account.isPortable()) {
|
||||
account.setPortable(false);
|
||||
if (!Accounts.getAccounts().contains(account))
|
||||
Accounts.getAccounts().add(account);
|
||||
} else {
|
||||
account.setPortable(true);
|
||||
if (!Accounts.getAccounts().contains(account)) {
|
||||
int idx = 0;
|
||||
for (int i = Accounts.getAccounts().size() - 1; i >= 0; i--) {
|
||||
if (Accounts.getAccounts().get(i).isPortable()) {
|
||||
idx = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Accounts.getAccounts().add(idx, account);
|
||||
}
|
||||
}
|
||||
});
|
||||
btnMove.getStyleClass().add("toggle-icon4");
|
||||
if (skinnable.getAccount().isPortable()) {
|
||||
btnMove.setGraphic(SVG.earth(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_global")));
|
||||
} else {
|
||||
btnMove.setGraphic(SVG.export(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnMove, i18n("account.move_to_portable")));
|
||||
}
|
||||
spinnerMove.setContent(btnMove);
|
||||
right.getChildren().add(spinnerMove);
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -124,14 +123,6 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
boxMethods.getChildren().add(microsoftItem);
|
||||
|
||||
AdvancedListItem littleSkinItem = new AdvancedListItem();
|
||||
littleSkinItem.getStyleClass().add("navigation-drawer-item");
|
||||
littleSkinItem.setActionButtonVisible(false);
|
||||
littleSkinItem.setTitle(i18n("account.methods.little_skin"));
|
||||
littleSkinItem.setLeftGraphic(wrap(SVG::server));
|
||||
littleSkinItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_LITTLE_SKIN)));
|
||||
boxMethods.getChildren().add(littleSkinItem);
|
||||
|
||||
VBox boxAuthServers = new VBox();
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
AdvancedListItem item = new AdvancedListItem();
|
||||
@@ -195,7 +186,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
Bindings.bindContent(list.getChildren(), skinnable.items);
|
||||
|
||||
scrollPane.setContent(list);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
|
||||
setCenter(scrollPane);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,8 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -32,29 +30,29 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
|
||||
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 AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
|
||||
public final class AddAuthlibInjectorServerPane extends TransitionPane implements DialogAware {
|
||||
|
||||
@FXML private TransitionPane root;
|
||||
@FXML private Label lblServerUrl;
|
||||
@FXML private Label lblServerName;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private Label lblServerWarning;
|
||||
@FXML private JFXTextField txtServerUrl;
|
||||
@FXML private JFXDialogLayout addServerPane;
|
||||
@FXML private JFXDialogLayout confirmServerPane;
|
||||
@FXML private SpinnerPane nextPane;
|
||||
@FXML private JFXButton btnAddNext;
|
||||
private final Label lblServerUrl;
|
||||
private final Label lblServerName;
|
||||
private final Label lblCreationWarning;
|
||||
private final Label lblServerWarning;
|
||||
private final JFXTextField txtServerUrl;
|
||||
private final JFXDialogLayout addServerPane;
|
||||
private final JFXDialogLayout confirmServerPane;
|
||||
private final SpinnerPane nextPane;
|
||||
private final JFXButton btnAddNext;
|
||||
|
||||
private AuthlibInjectorServer serverBeingAdded;
|
||||
|
||||
@@ -65,8 +63,92 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
}
|
||||
|
||||
public AddAuthlibInjectorServerPane() {
|
||||
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
|
||||
root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
|
||||
addServerPane = new JFXDialogLayout();
|
||||
addServerPane.setHeading(new Label(i18n("account.injector.add")));
|
||||
{
|
||||
txtServerUrl = new JFXTextField();
|
||||
txtServerUrl.setPromptText(i18n("account.injector.server_url"));
|
||||
txtServerUrl.setOnAction(e -> onAddNext());
|
||||
|
||||
lblCreationWarning = new Label();
|
||||
lblCreationWarning.setWrapText(true);
|
||||
HBox actions = new HBox();
|
||||
{
|
||||
JFXButton cancel = new JFXButton(i18n("button.cancel"));
|
||||
cancel.getStyleClass().add("dialog-accept");
|
||||
cancel.setOnAction(e -> onAddCancel());
|
||||
|
||||
nextPane = new SpinnerPane();
|
||||
nextPane.getStyleClass().add("small-spinner-pane");
|
||||
btnAddNext = new JFXButton(i18n("wizard.next"));
|
||||
btnAddNext.getStyleClass().add("dialog-accept");
|
||||
btnAddNext.setOnAction(e -> onAddNext());
|
||||
nextPane.setContent(btnAddNext);
|
||||
|
||||
actions.getChildren().setAll(cancel, nextPane);
|
||||
}
|
||||
|
||||
addServerPane.setBody(txtServerUrl);
|
||||
addServerPane.setActions(lblCreationWarning, actions);
|
||||
}
|
||||
|
||||
confirmServerPane = new JFXDialogLayout();
|
||||
confirmServerPane.setHeading(new Label(i18n("account.injector.add")));
|
||||
{
|
||||
GridPane body = new GridPane();
|
||||
body.setStyle("-fx-padding: 15 0 0 0;");
|
||||
body.setVgap(15);
|
||||
body.setHgap(15);
|
||||
{
|
||||
body.getColumnConstraints().setAll(
|
||||
Lang.apply(new ColumnConstraints(), c -> c.setMaxWidth(100)),
|
||||
new ColumnConstraints()
|
||||
);
|
||||
|
||||
lblServerUrl = new Label();
|
||||
GridPane.setColumnIndex(lblServerUrl, 1);
|
||||
GridPane.setRowIndex(lblServerUrl, 0);
|
||||
|
||||
lblServerName = new Label();
|
||||
GridPane.setColumnIndex(lblServerName, 1);
|
||||
GridPane.setRowIndex(lblServerName, 1);
|
||||
|
||||
lblServerWarning = new Label(i18n("account.injector.http"));
|
||||
lblServerWarning.setStyle("-fx-text-fill: red;");
|
||||
GridPane.setColumnIndex(lblServerWarning, 0);
|
||||
GridPane.setRowIndex(lblServerWarning, 2);
|
||||
GridPane.setColumnSpan(lblServerWarning, 2);
|
||||
|
||||
body.getChildren().setAll(
|
||||
Lang.apply(new Label(i18n("account.injector.server_url")), l -> {
|
||||
GridPane.setColumnIndex(l, 0);
|
||||
GridPane.setRowIndex(l, 0);
|
||||
}),
|
||||
Lang.apply(new Label(i18n("account.injector.server_name")), l -> {
|
||||
GridPane.setColumnIndex(l, 0);
|
||||
GridPane.setRowIndex(l, 1);
|
||||
}),
|
||||
lblServerUrl, lblServerName, lblServerWarning
|
||||
);
|
||||
}
|
||||
|
||||
JFXButton prevButton = new JFXButton(i18n("wizard.prev"));
|
||||
prevButton.getStyleClass().add("dialog-cancel");
|
||||
prevButton.setOnAction(e -> onAddPrev());
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(e -> onAddCancel());
|
||||
|
||||
JFXButton finishButton = new JFXButton(i18n("wizard.finish"));
|
||||
finishButton.getStyleClass().add("dialog-accept");
|
||||
finishButton.setOnAction(e -> onAddFinish());
|
||||
|
||||
confirmServerPane.setBody(body);
|
||||
confirmServerPane.setActions(prevButton, cancelButton, finishButton);
|
||||
}
|
||||
|
||||
this.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
|
||||
|
||||
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
|
||||
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
|
||||
@@ -81,19 +163,19 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
}
|
||||
|
||||
private String resolveFetchExceptionMessage(Throwable exception) {
|
||||
if (exception instanceof IOException) {
|
||||
if (exception instanceof SSLException) {
|
||||
return i18n("account.failed.ssl");
|
||||
} else if (exception instanceof IOException) {
|
||||
return i18n("account.failed.connect_injector_server");
|
||||
} else {
|
||||
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAddCancel() {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAddNext() {
|
||||
if (btnAddNext.isDisabled())
|
||||
return;
|
||||
@@ -117,7 +199,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
|
||||
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
|
||||
|
||||
root.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
|
||||
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
|
||||
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
|
||||
@@ -126,12 +208,10 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAddPrev() {
|
||||
root.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
|
||||
this.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onAddFinish() {
|
||||
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
|
||||
config().getAuthlibInjectorServers().add(serverBeingAdded);
|
||||
|
||||
@@ -36,7 +36,6 @@ 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;
|
||||
@@ -91,10 +90,10 @@ public class ClassicAccountLoginDialog extends StackPane {
|
||||
body.getChildren().add(linkPane);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
|
||||
migrationLink.setExternalLink(YggdrasilService.PROFILE_URL);
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
|
||||
migrationHowLink.setExternalLink(YggdrasilService.MIGRATION_FAQ_URL);
|
||||
|
||||
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
@@ -58,6 +58,7 @@ 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.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
@@ -289,28 +290,37 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
FlowPane box = new FlowPane();
|
||||
box.setHgap(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a"));
|
||||
birthLink.setExternalLink("https://support.microsoft.com/account-billing/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"));
|
||||
profileLink.setExternalLink("https://account.live.com/editprof.aspx");
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A"));
|
||||
deauthorizeLink.setExternalLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A");
|
||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
||||
forgotpasswordLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/password/forgot"));
|
||||
forgotpasswordLink.setExternalLink("https://www.minecraft.net/password/forgot");
|
||||
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
|
||||
createProfileLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editprofile"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, createProfileLink);
|
||||
createProfileLink.setExternalLink("https://www.minecraft.net/msaprofile/mygames/editprofile");
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
vbox.getChildren().setAll(hintPane, box);
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
HintPane unofficialHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
unofficialHint.setText(i18n("unofficial.hint"));
|
||||
vbox.getChildren().add(unofficialHint);
|
||||
}
|
||||
|
||||
vbox.getChildren().addAll(hintPane, box);
|
||||
|
||||
btnAccept.setDisable(false);
|
||||
} else {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setText(i18n("account.methods.microsoft.snapshot"));
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
|
||||
vbox.getChildren().setAll(hintPane);
|
||||
JFXHyperlink officialWebsite = new JFXHyperlink(i18n("account.methods.microsoft.snapshot.website"));
|
||||
officialWebsite.setExternalLink("https://hmcl.huangyuhui.net");
|
||||
|
||||
vbox.getChildren().setAll(hintPane, officialWebsite);
|
||||
btnAccept.setDisable(true);
|
||||
}
|
||||
|
||||
@@ -389,6 +399,15 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
|
||||
int rowIndex = 0;
|
||||
|
||||
if (!IntegrityChecker.isOfficial() && !(factory instanceof OfflineAccountFactory)) {
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
hintPane.setSegment(i18n("unofficial.hint"));
|
||||
GridPane.setColumnSpan(hintPane, 2);
|
||||
add(hintPane, 0, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
|
||||
this.server = ((BoundAuthlibInjectorAccountFactory) factory).getServer();
|
||||
|
||||
@@ -493,13 +512,13 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
|
||||
migrationLink.setOnAction(e -> FXUtils.openLink("https://aka.ms/MinecraftMigration"));
|
||||
migrationLink.setExternalLink("https://aka.ms/MinecraftMigration");
|
||||
|
||||
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
|
||||
migrationHowLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/articles/4411173197709"));
|
||||
migrationHowLink.setExternalLink("https://help.minecraft.net/hc/articles/4411173197709");
|
||||
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
|
||||
box.getChildren().setAll(migrationLink, migrationHowLink, purchaseLink);
|
||||
add(box, 0, rowIndex);
|
||||
@@ -508,8 +527,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
if (factory instanceof OfflineAccountFactory) {
|
||||
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
|
||||
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL);
|
||||
HBox linkPane = new HBox(purchaseLink);
|
||||
GridPane.setColumnSpan(linkPane, 2);
|
||||
add(linkPane, 0, rowIndex);
|
||||
@@ -652,12 +673,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
Canvas portraitCanvas = new Canvas(32, 32);
|
||||
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
|
||||
@@ -48,9 +48,15 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.login.refresh.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
hintPane.setSegment(
|
||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
||||
+ i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri())
|
||||
);
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.login.refresh.microsoft.hint"));
|
||||
hintPane.setSegment(
|
||||
"<b>" + i18n("account.login.refresh.microsoft.hint") + "</b>\n"
|
||||
+ i18n("account.methods.microsoft.hint")
|
||||
);
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
|
||||
@@ -141,9 +141,9 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
return;
|
||||
}
|
||||
canvas.updateSkin(
|
||||
result.getSkin() != null ? new Image(result.getSkin().getInputStream()) : getDefaultTexture(),
|
||||
result.getSkin() != null ? result.getSkin().getImage() : getDefaultTexture(),
|
||||
result.getModel() == TextureModel.ALEX,
|
||||
result.getCape() != null ? new Image(result.getCape().getInputStream()) : null);
|
||||
result.getCape() != null ? result.getCape().getImage() : null);
|
||||
}
|
||||
}).start();
|
||||
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
|
||||
public final class AnimationUtils {
|
||||
|
||||
private AnimationUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger initialization of this class.
|
||||
* Should be called from {@link org.jackhuang.hmcl.setting.Settings#init()}.
|
||||
*/
|
||||
@SuppressWarnings("JavadocReference")
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static final boolean enabled = !ConfigHolder.config().isAnimationDisabled();
|
||||
|
||||
public static boolean isAnimationEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -69,18 +69,23 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
transition.init(this);
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
transition.init(this);
|
||||
|
||||
// runLater or "init" will not work
|
||||
Platform.runLater(() -> {
|
||||
Timeline newAnimation = new Timeline();
|
||||
newAnimation.getKeyFrames().addAll(transition.animate(this));
|
||||
newAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
}));
|
||||
FXUtils.playAnimation(this, "transition_pane", newAnimation);
|
||||
});
|
||||
// runLater or "init" will not work
|
||||
Platform.runLater(() -> {
|
||||
Timeline newAnimation = new Timeline();
|
||||
newAnimation.getKeyFrames().addAll(transition.animate(this));
|
||||
newAnimation.getKeyFrames().add(new KeyFrame(duration, e -> {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
}));
|
||||
FXUtils.playAnimation(this, "transition_pane", newAnimation);
|
||||
});
|
||||
} else {
|
||||
setMouseTransparent(false);
|
||||
getChildren().remove(previousNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateContent(Node newView) {
|
||||
|
||||
@@ -38,7 +38,8 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public class ComponentList extends Control {
|
||||
@@ -47,13 +48,17 @@ public class ComponentList extends Control {
|
||||
private final IntegerProperty depth = new SimpleIntegerProperty(this, "depth", 0);
|
||||
private boolean hasSubtitle = false;
|
||||
public final ObservableList<Node> content = FXCollections.observableArrayList();
|
||||
private boolean expanded = false;
|
||||
private Consumer<ComponentList> lazyInitializer;
|
||||
private Supplier<List<? extends Node>> lazyInitializer;
|
||||
|
||||
public ComponentList() {
|
||||
getStyleClass().add("options-list");
|
||||
}
|
||||
|
||||
public ComponentList(Supplier<List<? extends Node>> lazyInitializer) {
|
||||
this();
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
@@ -102,17 +107,12 @@ public class ComponentList extends Control {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setLazyInitializer(Consumer<ComponentList> lazyInitializer) {
|
||||
this.lazyInitializer = lazyInitializer;
|
||||
}
|
||||
|
||||
public void onExpand() {
|
||||
if (!expanded && lazyInitializer != null) {
|
||||
lazyInitializer.accept(this);
|
||||
void doLazyInit() {
|
||||
if (lazyInitializer != null) {
|
||||
this.getContent().setAll(lazyInitializer.get());
|
||||
setNeedsLayout(true);
|
||||
lazyInitializer = null;
|
||||
}
|
||||
|
||||
expanded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,7 +125,7 @@ public class ComponentList extends Control {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
protected static class Skin extends ControlSkinBase<ComponentList> {
|
||||
private static final class Skin extends ControlSkinBase<ComponentList> {
|
||||
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
|
||||
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
@@ -133,7 +133,7 @@ public class ComponentList extends Control {
|
||||
private final ObjectBinding<Node> firstItem;
|
||||
private final ObjectBinding<Node> lastItem;
|
||||
|
||||
protected Skin(ComponentList control) {
|
||||
Skin(ComponentList control) {
|
||||
super(control);
|
||||
|
||||
list = MappedObservableList.create(control.getContent(), node -> {
|
||||
|
||||
@@ -25,6 +25,9 @@ import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -35,11 +38,12 @@ import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
class ComponentListCell extends StackPane {
|
||||
final class ComponentListCell extends StackPane {
|
||||
private final Node content;
|
||||
private Animation expandAnimation;
|
||||
private Rectangle clipRect;
|
||||
@@ -70,6 +74,7 @@ class ComponentListCell extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void updateLayout() {
|
||||
if (content instanceof ComponentList) {
|
||||
ComponentList list = (ComponentList) content;
|
||||
@@ -134,45 +139,53 @@ class ComponentListCell extends StackPane {
|
||||
container.getChildren().setAll(content);
|
||||
groupNode.getChildren().add(container);
|
||||
|
||||
Runnable onExpand = () -> {
|
||||
EventHandler<Event> onExpand = e -> {
|
||||
if (expandAnimation != null && expandAnimation.getStatus() == Animation.Status.RUNNING) {
|
||||
expandAnimation.stop();
|
||||
}
|
||||
|
||||
setExpanded(!isExpanded());
|
||||
|
||||
if (isExpanded()) {
|
||||
list.onExpand();
|
||||
boolean expanded = !isExpanded();
|
||||
setExpanded(expanded);
|
||||
if (expanded) {
|
||||
list.doLazyInit();
|
||||
list.layout();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
double newAnimatedHeight = (content.prefHeight(-1) + 8 + 10) * (isExpanded() ? 1 : -1);
|
||||
double newHeight = isExpanded() ? getHeight() + newAnimatedHeight : prefHeight(-1);
|
||||
double contentHeight = isExpanded() ? newAnimatedHeight : 0;
|
||||
double newAnimatedHeight = (list.prefHeight(-1) + 8 + 10) * (expanded ? 1 : -1);
|
||||
double newHeight = expanded ? getHeight() + newAnimatedHeight : prefHeight(-1);
|
||||
double contentHeight = expanded ? newAnimatedHeight : 0;
|
||||
|
||||
if (isExpanded()) {
|
||||
if (expanded) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
|
||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
||||
));
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
expandAnimation = new Timeline(new KeyFrame(new Duration(320.0),
|
||||
new KeyValue(container.minHeightProperty(), contentHeight, FXUtils.SINE),
|
||||
new KeyValue(container.maxHeightProperty(), contentHeight, FXUtils.SINE)
|
||||
));
|
||||
|
||||
if (!isExpanded()) {
|
||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||
if (!expanded) {
|
||||
expandAnimation.setOnFinished(e2 -> updateClip(newHeight));
|
||||
}
|
||||
|
||||
expandAnimation.play();
|
||||
} else {
|
||||
container.setMinHeight(contentHeight);
|
||||
container.setMaxHeight(contentHeight);
|
||||
|
||||
if (!expanded) {
|
||||
updateClip(newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
expandAnimation.play();
|
||||
});
|
||||
};
|
||||
|
||||
headerRippler.setOnMouseClicked(e -> onExpand.run());
|
||||
expandButton.setOnMouseClicked(e -> onExpand.run());
|
||||
headerRippler.setOnMouseClicked(onExpand);
|
||||
expandButton.setOnAction((EventHandler<ActionEvent>) (Object) onExpand);
|
||||
|
||||
expandedProperty().addListener((a, b, newValue) ->
|
||||
expandIcon.setRotate(newValue ? 180 : 0));
|
||||
expandedProperty().addListener((a, b, newValue) -> expandIcon.setRotate(newValue ? 180 : 0));
|
||||
|
||||
getChildren().setAll(groupNode);
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ 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 java.io.File;
|
||||
@@ -36,7 +37,7 @@ import java.io.File;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class FileSelector extends HBox {
|
||||
private StringProperty value = new SimpleStringProperty();
|
||||
private final StringProperty value = new SimpleStringProperty();
|
||||
private String chooserTitle = i18n("selector.choose_file");
|
||||
private boolean directory = false;
|
||||
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
|
||||
@@ -77,24 +78,30 @@ public class FileSelector extends HBox {
|
||||
|
||||
public FileSelector() {
|
||||
JFXTextField customField = new JFXTextField();
|
||||
customField.textProperty().bindBidirectional(valueProperty());
|
||||
FXUtils.bindString(customField, valueProperty());
|
||||
|
||||
JFXButton selectButton = new JFXButton();
|
||||
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
|
||||
selectButton.setOnMouseClicked(e -> {
|
||||
selectButton.setOnAction(e -> {
|
||||
if (directory) {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(chooserTitle);
|
||||
File dir = chooser.showDialog(Controllers.getStage());
|
||||
if (dir != null)
|
||||
customField.setText(dir.getAbsolutePath());
|
||||
if (dir != null) {
|
||||
String path = dir.getAbsolutePath();
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
} 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());
|
||||
if (file != null) {
|
||||
String path = file.getAbsolutePath();
|
||||
customField.setText(path);
|
||||
value.setValue(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListCell;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
@@ -34,7 +33,7 @@ public class FontComboBox extends JFXComboBox<String> {
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
|
||||
public FontComboBox() {
|
||||
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
|
||||
|
||||
setCellFactory(listView -> new JFXListCell<String>() {
|
||||
|
||||
@@ -31,6 +31,8 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class HintPane extends VBox {
|
||||
private final Text label = new Text();
|
||||
private final StringProperty text = new SimpleStringProperty(this, "text");
|
||||
@@ -42,7 +44,7 @@ public class HintPane extends VBox {
|
||||
|
||||
public HintPane(MessageDialogPane.MessageType type) {
|
||||
setFillWidth(true);
|
||||
getStyleClass().addAll("hint", type.name().toLowerCase());
|
||||
getStyleClass().addAll("hint", type.name().toLowerCase(Locale.ROOT));
|
||||
HBox hbox = new HBox();
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
|
||||
@@ -18,40 +18,45 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class InputDialogPane extends StackPane {
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class InputDialogPane extends JFXDialogLayout {
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
@FXML
|
||||
private JFXButton acceptButton;
|
||||
@FXML
|
||||
private JFXButton cancelButton;
|
||||
@FXML
|
||||
private Label title;
|
||||
@FXML
|
||||
private VBox vbox;
|
||||
@FXML
|
||||
private Label lblCreationWarning;
|
||||
@FXML
|
||||
private SpinnerPane acceptPane;
|
||||
private final JFXTextField textField;
|
||||
private final Label lblCreationWarning;
|
||||
private final SpinnerPane acceptPane;
|
||||
|
||||
public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
|
||||
title.setText(text);
|
||||
JFXTextField textField = new JFXTextField();
|
||||
textField.setText(initialValue);
|
||||
vbox.getChildren().setAll(textField);
|
||||
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
|
||||
acceptButton.setOnMouseClicked(e -> {
|
||||
textField = new JFXTextField(initialValue);
|
||||
|
||||
this.setHeading(new HBox(new Label(text)));
|
||||
this.setBody(new VBox(textField));
|
||||
|
||||
lblCreationWarning = new Label();
|
||||
|
||||
acceptPane = new SpinnerPane();
|
||||
acceptPane.getStyleClass().add("small-spinner-pane");
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.getStyleClass().add("dialog-accept");
|
||||
acceptPane.setContent(acceptButton);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
|
||||
this.setActions(lblCreationWarning, acceptPane, cancelButton);
|
||||
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
acceptButton.setOnAction(e -> {
|
||||
acceptPane.showSpinner();
|
||||
|
||||
onResult.call(textField.getText(), () -> {
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
public class JFXHyperlink extends Hyperlink {
|
||||
@@ -34,5 +35,9 @@ public class JFXHyperlink extends Hyperlink {
|
||||
|
||||
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
|
||||
}
|
||||
|
||||
public void setExternalLink(String externalLink) {
|
||||
this.setOnAction(e -> FXUtils.openLink(externalLink));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,18 @@ import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
private final Holder<Object> lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView, Holder<Object> lastCell) {
|
||||
this.lastCell = lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
@@ -50,6 +54,14 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (lastCell != null) {
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
}
|
||||
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
|
||||
@@ -19,7 +19,6 @@ 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;
|
||||
@@ -45,7 +44,7 @@ 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 final class MessageDialogPane extends HBox {
|
||||
|
||||
public enum MessageType {
|
||||
ERROR,
|
||||
@@ -59,35 +58,19 @@ public final class MessageDialogPane extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private StackPane content;
|
||||
@FXML
|
||||
private Label graphic;
|
||||
@FXML
|
||||
private Label title;
|
||||
@FXML
|
||||
private HBox actions;
|
||||
private final HBox actions;
|
||||
|
||||
private @Nullable ButtonBase cancelButton;
|
||||
|
||||
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
|
||||
|
||||
EnhancedTextFlow textFlow = new EnhancedTextFlow();
|
||||
textFlow.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction));
|
||||
|
||||
if (textFlow.computePrefHeight(400.0) <= 350.0)
|
||||
content.getChildren().setAll(textFlow);
|
||||
else {
|
||||
ScrollPane scrollPane = new ScrollPane(textFlow);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
content.getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
if (title != null)
|
||||
this.title.setText(title);
|
||||
this.setSpacing(8);
|
||||
this.getStyleClass().add("jfx-dialog-layout");
|
||||
|
||||
Label graphic = new Label();
|
||||
graphic.setTranslateX(10);
|
||||
graphic.setTranslateY(10);
|
||||
graphic.setMinSize(40, 40);
|
||||
graphic.setMaxSize(40, 40);
|
||||
switch (type) {
|
||||
case INFO:
|
||||
graphic.setGraphic(SVG.infoCircle(Theme.blackFillBinding(), 40, 40));
|
||||
@@ -108,6 +91,35 @@ public final class MessageDialogPane extends StackPane {
|
||||
throw new IllegalArgumentException("Unrecognized message box message type " + type);
|
||||
}
|
||||
|
||||
VBox vbox = new VBox();
|
||||
HBox.setHgrow(vbox, Priority.ALWAYS);
|
||||
{
|
||||
StackPane titlePane = new StackPane();
|
||||
titlePane.getStyleClass().addAll("jfx-layout-heading", "title");
|
||||
titlePane.getChildren().setAll(new Label(title != null ? title : i18n("message.info")));
|
||||
|
||||
StackPane content = new StackPane();
|
||||
content.getStyleClass().add("jfx-layout-body");
|
||||
EnhancedTextFlow textFlow = new EnhancedTextFlow(text);
|
||||
textFlow.setStyle("-fx-font-size: 14px;");
|
||||
if (textFlow.computePrefHeight(400.0) <= 350.0)
|
||||
content.getChildren().setAll(textFlow);
|
||||
else {
|
||||
ScrollPane scrollPane = new ScrollPane(textFlow);
|
||||
scrollPane.setPrefHeight(350);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
content.getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
actions = new HBox();
|
||||
actions.getStyleClass().add("jfx-layout-actions");
|
||||
|
||||
vbox.getChildren().setAll(titlePane, content, actions);
|
||||
}
|
||||
|
||||
this.getChildren().setAll(graphic, vbox);
|
||||
|
||||
onEscPressed(this, () -> {
|
||||
if (cancelButton != null) {
|
||||
cancelButton.fire();
|
||||
@@ -129,6 +141,10 @@ public final class MessageDialogPane extends StackPane {
|
||||
}
|
||||
|
||||
private static final class EnhancedTextFlow extends TextFlow {
|
||||
EnhancedTextFlow(String text) {
|
||||
this.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double computePrefHeight(double width) {
|
||||
return super.computePrefHeight(width);
|
||||
|
||||
@@ -39,6 +39,7 @@ import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class TabHeader extends Control implements TabControl, PageAware {
|
||||
|
||||
public TabHeader(Tab<?>... tabs) {
|
||||
|
||||
@@ -20,9 +20,12 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
@@ -35,29 +38,59 @@ import java.util.function.Consumer;
|
||||
|
||||
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 class TaskExecutorDialogPane extends StackPane {
|
||||
public class TaskExecutorDialogPane extends BorderPane {
|
||||
private TaskExecutor executor;
|
||||
private TaskCancellationAction onCancel;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
@FXML
|
||||
private Label lblTitle;
|
||||
@FXML
|
||||
private Label lblProgress;
|
||||
@FXML
|
||||
private JFXButton btnCancel;
|
||||
@FXML
|
||||
private TaskListPane taskListPane;
|
||||
private final Label lblTitle;
|
||||
private final Label lblProgress;
|
||||
private final JFXButton btnCancel;
|
||||
private final TaskListPane taskListPane;
|
||||
|
||||
public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/task-dialog.fxml");
|
||||
FXUtils.setLimitWidth(this, 500);
|
||||
FXUtils.setLimitHeight(this, 300);
|
||||
|
||||
VBox center = new VBox();
|
||||
this.setCenter(center);
|
||||
center.setPadding(new Insets(16));
|
||||
{
|
||||
lblTitle = new Label();
|
||||
lblTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: BOLD;");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
{
|
||||
taskListPane = new TaskListPane();
|
||||
scrollPane.setContent(taskListPane);
|
||||
}
|
||||
|
||||
center.getChildren().setAll(lblTitle, scrollPane);
|
||||
}
|
||||
|
||||
BorderPane bottom = new BorderPane();
|
||||
this.setBottom(bottom);
|
||||
bottom.setPadding(new Insets(0, 8, 8, 8));
|
||||
{
|
||||
lblProgress = new Label();
|
||||
bottom.setLeft(lblProgress);
|
||||
|
||||
btnCancel = new JFXButton(i18n("button.cancel"));
|
||||
bottom.setRight(btnCancel);
|
||||
}
|
||||
|
||||
setCancel(cancel);
|
||||
|
||||
btnCancel.setOnAction(e -> {
|
||||
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
|
||||
onCancel.getCancellationAction().accept(this);
|
||||
if (onCancel.getCancellationAction() != null) {
|
||||
onCancel.getCancellationAction().accept(this);
|
||||
}
|
||||
});
|
||||
|
||||
speedEventHandler = speedEvent -> {
|
||||
|
||||
@@ -37,6 +37,8 @@ import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.mod.ModpackInstallTask;
|
||||
@@ -129,6 +131,10 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
|
||||
} else if (task instanceof FabricAPIInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
|
||||
} else if (task instanceof QuiltInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.quilt")));
|
||||
} else if (task instanceof QuiltAPIInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.quilt-api")));
|
||||
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
|
||||
task.setName(i18n("modpack.completion"));
|
||||
} else if (task instanceof ModpackInstallTask) {
|
||||
|
||||
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.ui.decorator;
|
||||
import com.jfoenix.controls.JFXDialog;
|
||||
import com.jfoenix.controls.JFXSnackbar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
@@ -35,6 +36,7 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
@@ -52,8 +54,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -88,10 +92,23 @@ public class DecoratorController {
|
||||
decorator.onBackNavButtonActionProperty().set(e -> back());
|
||||
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
|
||||
|
||||
setupBackground();
|
||||
|
||||
setupAuthlibInjectorDnD();
|
||||
|
||||
// Setup background
|
||||
decorator.setContentBackground(getBackground());
|
||||
changeBackgroundListener = o -> {
|
||||
final int currentCount = ++this.changeBackgroundCount;
|
||||
CompletableFuture.supplyAsync(this::getBackground, Schedulers.io())
|
||||
.thenAcceptAsync(background -> {
|
||||
if (this.changeBackgroundCount == currentCount)
|
||||
decorator.setContentBackground(background);
|
||||
}, Schedulers.javafx());
|
||||
};
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener);
|
||||
config().backgroundImageTypeProperty().addListener(weakListener);
|
||||
config().backgroundImageProperty().addListener(weakListener);
|
||||
config().backgroundImageUrlProperty().addListener(weakListener);
|
||||
|
||||
// pass key events to current dialog / current page
|
||||
decorator.addEventFilter(KeyEvent.ANY, e -> {
|
||||
if (!(e.getTarget() instanceof Node)) {
|
||||
@@ -133,40 +150,43 @@ public class DecoratorController {
|
||||
|
||||
// ==== Background ====
|
||||
|
||||
private void setupBackground() {
|
||||
decorator.contentBackgroundProperty().bind(
|
||||
Bindings.createObjectBinding(
|
||||
() -> {
|
||||
Image image = null;
|
||||
if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) {
|
||||
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().backgroundImageUrlProperty()));
|
||||
//FXThread
|
||||
private int changeBackgroundCount = 0;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal") // Strong reference
|
||||
private final InvalidationListener changeBackgroundListener;
|
||||
|
||||
private Background getBackground() {
|
||||
EnumBackgroundImage imageType = config().getBackgroundImageType();
|
||||
|
||||
Image image = null;
|
||||
switch (imageType) {
|
||||
case CUSTOM:
|
||||
String backgroundImage = config().getBackgroundImage();
|
||||
if (backgroundImage != null)
|
||||
image = tryLoadImage(Paths.get(backgroundImage)).orElse(null);
|
||||
break;
|
||||
case NETWORK:
|
||||
String backgroundImageUrl = config().getBackgroundImageUrl();
|
||||
if (backgroundImageUrl != null && NetworkUtils.isURL(backgroundImageUrl))
|
||||
image = tryLoadImage(backgroundImageUrl).orElse(null);
|
||||
break;
|
||||
case CLASSIC:
|
||||
image = newImage("/assets/img/background-classic.jpg");
|
||||
break;
|
||||
case 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)));
|
||||
}
|
||||
|
||||
private Image defaultBackground = newImage("/assets/img/background.jpg");
|
||||
private volatile Image defaultBackground;
|
||||
|
||||
/**
|
||||
* Load background image from bg/, background.png, background.jpg
|
||||
* Load background image from bg/, background.png, background.jpg, background.gif
|
||||
*/
|
||||
private Image loadDefaultBackgroundImage() {
|
||||
Optional<Image> image = randomImageIn(Paths.get("bg"));
|
||||
@@ -176,7 +196,15 @@ public class DecoratorController {
|
||||
if (!image.isPresent()) {
|
||||
image = tryLoadImage(Paths.get("background.jpg"));
|
||||
}
|
||||
return image.orElse(defaultBackground);
|
||||
if (!image.isPresent()) {
|
||||
image = tryLoadImage(Paths.get("background.gif"));
|
||||
}
|
||||
|
||||
return image.orElseGet(() -> {
|
||||
if (defaultBackground == null)
|
||||
defaultBackground = newImage("/assets/img/background.jpg");
|
||||
return defaultBackground;
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<Image> randomImageIn(Path imageDir) {
|
||||
@@ -187,10 +215,10 @@ public class DecoratorController {
|
||||
List<Path> candidates;
|
||||
try (Stream<Path> stream = Files.list(imageDir)) {
|
||||
candidates = stream
|
||||
.filter(Files::isReadable)
|
||||
.filter(Files::isReadable)
|
||||
.filter(it -> {
|
||||
String ext = getExtension(it).toLowerCase();
|
||||
return ext.equals("png") || ext.equals("jpg");
|
||||
String ext = getExtension(it).toLowerCase(Locale.ROOT);
|
||||
return ext.equals("png") || ext.equals("jpg") || ext.equals("gif");
|
||||
})
|
||||
.collect(toList());
|
||||
} catch (IOException e) {
|
||||
@@ -215,9 +243,13 @@ public class DecoratorController {
|
||||
if (!Files.isReadable(path))
|
||||
return Optional.empty();
|
||||
|
||||
return tryLoadImage(path.toAbsolutePath().toUri().toString());
|
||||
}
|
||||
|
||||
private Optional<Image> tryLoadImage(String url) {
|
||||
Image img;
|
||||
try {
|
||||
img = new Image(path.toAbsolutePath().toUri().toString());
|
||||
img = new Image(url);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.log(WARNING, "Couldn't load background image", e);
|
||||
return Optional.empty();
|
||||
|
||||
@@ -261,7 +261,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
Label titleLabel = new Label();
|
||||
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
if (titleNode != null) {
|
||||
if (titleNode == null) {
|
||||
titleLabel.maxWidthProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> skinnable.getWidth() - 100 - navLeft.getWidth(),
|
||||
skinnable.widthProperty(), navLeft.widthProperty()));
|
||||
} else {
|
||||
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
// 8 (margin-left)
|
||||
return leftPaneWidth - 8 - navLeft.getWidth();
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TabControl;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -81,9 +82,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
ModpackDownloadListPage page = new ModpackDownloadListPage(Versions::downloadModpackImpl, false);
|
||||
|
||||
JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack"));
|
||||
installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
installLocalModpackButton.getStyleClass().add("jfx-button-raised");
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
|
||||
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
@@ -128,19 +127,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
item.setTitle(i18n("resourcepack"));
|
||||
item.setLeftGraphic(wrap(SVG::textureBox));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
|
||||
item.setOnAction(e -> tab.select(resourcePackTab));
|
||||
item.setOnAction(e -> selectTabIfCurseForgeAvailable(resourcePackTab));
|
||||
})
|
||||
// .addNavigationDrawerItem(item -> {
|
||||
// item.setTitle(i18n("download.curseforge.customization"));
|
||||
// item.setLeftGraphic(wrap(SVG::script));
|
||||
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
|
||||
// item.setOnAction(e -> tab.select(customizationTab));
|
||||
// item.setOnAction(e -> selectTabIfCurseForgeAvailable(customizationTab));
|
||||
// })
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("world"));
|
||||
item.setLeftGraphic(wrap(SVG::earth));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
|
||||
item.setOnAction(e -> tab.select(worldTab));
|
||||
item.setOnAction(e -> selectTabIfCurseForgeAvailable(worldTab));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -149,7 +148,14 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
private void selectTabIfCurseForgeAvailable(TabControl.Tab<?> newTab) {
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
tab.select(newTab);
|
||||
else
|
||||
Controllers.dialog(i18n("download.curseforge.unavailable"));
|
||||
}
|
||||
|
||||
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (node instanceof VersionPage.VersionLoadable) {
|
||||
@@ -159,7 +165,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
};
|
||||
}
|
||||
|
||||
private void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
private static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
|
||||
@@ -234,7 +240,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
tab.select(worldTab);
|
||||
}
|
||||
|
||||
private class DownloadNavigator implements Navigation {
|
||||
private static final class DownloadNavigator implements Navigation {
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Control;
|
||||
@@ -35,6 +34,7 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
@@ -119,7 +119,6 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
}
|
||||
|
||||
@FXML
|
||||
protected void onInstall() {
|
||||
controller.getSettings().put("name", txtName.getText());
|
||||
controller.onFinish();
|
||||
@@ -164,10 +163,8 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
|
||||
|
||||
{
|
||||
JFXButton installButton = new JFXButton(i18n("button.install"));
|
||||
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
installButton.disableProperty().bind(control.installable.not());
|
||||
installButton.getStyleClass().add("jfx-button-raised");
|
||||
installButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
installButton.setPrefWidth(100);
|
||||
installButton.setPrefHeight(40);
|
||||
installButton.setOnMouseClicked(e -> control.onInstall());
|
||||
|
||||
@@ -17,15 +17,9 @@
|
||||
*/
|
||||
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;
|
||||
@@ -39,10 +33,8 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.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;
|
||||
|
||||
@@ -52,52 +44,21 @@ 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 {
|
||||
private final WizardController controller;
|
||||
|
||||
private Modpack manifest = null;
|
||||
|
||||
@FXML
|
||||
private Region borderPane;
|
||||
|
||||
@FXML
|
||||
private Label lblName;
|
||||
|
||||
@FXML
|
||||
private Label lblVersion;
|
||||
|
||||
@FXML
|
||||
private Label lblAuthor;
|
||||
|
||||
@FXML
|
||||
private Label lblModpackLocation;
|
||||
|
||||
@FXML
|
||||
private JFXTextField txtModpackName;
|
||||
|
||||
@FXML
|
||||
private JFXButton btnInstall;
|
||||
|
||||
@FXML
|
||||
private SpinnerPane spinnerPane;
|
||||
public final class LocalModpackPage extends ModpackPage {
|
||||
|
||||
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
|
||||
private Modpack manifest = null;
|
||||
private Charset charset;
|
||||
|
||||
public LocalModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/modpack.fxml");
|
||||
super(controller);
|
||||
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
|
||||
File selectedFile;
|
||||
|
||||
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
@@ -116,12 +77,9 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
|
||||
}
|
||||
});
|
||||
|
||||
btnInstall.disableProperty().bind(
|
||||
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
|
||||
.not());
|
||||
}
|
||||
|
||||
File selectedFile;
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
selectedFile = filePath.get();
|
||||
@@ -138,7 +96,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
controller.getSettings().put(MODPACK_FILE, selectedFile);
|
||||
}
|
||||
|
||||
spinnerPane.showSpinner();
|
||||
showSpinner();
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
|
||||
.thenApplyAsync(encoding -> {
|
||||
charset = encoding;
|
||||
@@ -147,7 +105,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
spinnerPane.hideSpinner();
|
||||
hideSpinner();
|
||||
lblName.setText(selectedFile.getName());
|
||||
installAsVersion.set(false);
|
||||
lblModpackLocation.setText(selectedFile.getAbsolutePath());
|
||||
@@ -167,7 +125,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
} else {
|
||||
spinnerPane.hideSpinner();
|
||||
hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
@@ -188,26 +146,19 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
|
||||
settings.remove(MODPACK_FILE);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onInstall() {
|
||||
protected void onInstall() {
|
||||
if (!txtModpackName.validate()) return;
|
||||
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.onFinish();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onDescribe() {
|
||||
protected void onDescribe() {
|
||||
if (manifest != null) {
|
||||
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("modpack.task.install");
|
||||
}
|
||||
|
||||
public static final String MODPACK_FILE = "MODPACK_FILE";
|
||||
public static final String MODPACK_NAME = "MODPACK_NAME";
|
||||
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
protected final WizardController controller;
|
||||
|
||||
protected final Label lblName;
|
||||
protected final Label lblVersion;
|
||||
protected final Label lblAuthor;
|
||||
protected final Label lblModpackLocation;
|
||||
protected final JFXTextField txtModpackName;
|
||||
protected final JFXButton btnInstall;
|
||||
|
||||
protected ModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
this.getStyleClass().add("large-spinner-pane");
|
||||
|
||||
VBox borderPane = new VBox();
|
||||
borderPane.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(borderPane, 500);
|
||||
|
||||
ComponentList componentList = new ComponentList();
|
||||
{
|
||||
BorderPane locationPane = new BorderPane();
|
||||
{
|
||||
locationPane.setLeft(new Label(i18n("modpack.task.install.will")));
|
||||
|
||||
lblModpackLocation = new Label();
|
||||
BorderPane.setAlignment(lblModpackLocation, Pos.CENTER_RIGHT);
|
||||
locationPane.setCenter(lblModpackLocation);
|
||||
}
|
||||
|
||||
BorderPane archiveNamePane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("archive.name"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
archiveNamePane.setLeft(label);
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setCenter(txtModpackName);
|
||||
}
|
||||
|
||||
BorderPane modpackNamePane = new BorderPane();
|
||||
{
|
||||
modpackNamePane.setLeft(new Label(i18n("modpack.name")));
|
||||
|
||||
lblName = new Label();
|
||||
BorderPane.setAlignment(lblName, Pos.CENTER_RIGHT);
|
||||
modpackNamePane.setCenter(lblName);
|
||||
}
|
||||
|
||||
BorderPane versionPane = new BorderPane();
|
||||
{
|
||||
versionPane.setLeft(new Label(i18n("archive.version")));
|
||||
|
||||
lblVersion = new Label();
|
||||
BorderPane.setAlignment(lblVersion, Pos.CENTER_RIGHT);
|
||||
versionPane.setCenter(lblVersion);
|
||||
}
|
||||
|
||||
BorderPane authorPane = new BorderPane();
|
||||
{
|
||||
authorPane.setLeft(new Label(i18n("archive.author")));
|
||||
|
||||
lblAuthor = new Label();
|
||||
BorderPane.setAlignment(lblAuthor, Pos.CENTER_RIGHT);
|
||||
authorPane.setCenter(lblAuthor);
|
||||
}
|
||||
|
||||
BorderPane descriptionPane = new BorderPane();
|
||||
{
|
||||
JFXButton btnDescription = new JFXButton(i18n("modpack.description"));
|
||||
btnDescription.getStyleClass().add("jfx-button-border");
|
||||
btnDescription.setOnAction(e -> onDescribe());
|
||||
descriptionPane.setLeft(btnDescription);
|
||||
|
||||
btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
btnInstall.setOnAction(e -> onInstall());
|
||||
btnInstall.disableProperty().bind(createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty()));
|
||||
descriptionPane.setRight(btnInstall);
|
||||
}
|
||||
|
||||
componentList.getContent().setAll(
|
||||
locationPane, archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
|
||||
}
|
||||
|
||||
borderPane.getChildren().setAll(componentList);
|
||||
this.setContent(borderPane);
|
||||
}
|
||||
|
||||
protected abstract void onInstall();
|
||||
|
||||
protected abstract void onDescribe();
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("modpack.task.install");
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,12 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
@@ -30,6 +34,8 @@ import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
@@ -48,18 +54,23 @@ import static org.jackhuang.hmcl.ui.download.RemoteModpackPage.MODPACK_SERVER_MA
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackSelectionPage extends StackPane implements WizardPage {
|
||||
public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
private final WizardController controller;
|
||||
|
||||
@FXML private JFXButton btnLocal;
|
||||
@FXML private JFXButton btnRemote;
|
||||
|
||||
public ModpackSelectionPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/modpack-source.fxml");
|
||||
|
||||
JFXDepthManager.setDepth(btnLocal, 1);
|
||||
JFXDepthManager.setDepth(btnRemote, 1);
|
||||
Label title = new Label(i18n("install.modpack"));
|
||||
title.setPadding(new Insets(8));
|
||||
|
||||
this.getStyleClass().add("jfx-list-view");
|
||||
this.setMaxSize(400, 150);
|
||||
this.setSpacing(8);
|
||||
this.getChildren().setAll(
|
||||
title,
|
||||
createButton("local", this::onChooseLocalFile),
|
||||
createButton("remote", this::onChooseRemoteFile)
|
||||
);
|
||||
|
||||
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
|
||||
if (filePath.isPresent()) {
|
||||
@@ -74,7 +85,30 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
private JFXButton createButton(String type, Runnable action) {
|
||||
JFXButton button = new JFXButton();
|
||||
|
||||
button.getStyleClass().add("card");
|
||||
button.setStyle("-fx-cursor: HAND;");
|
||||
button.prefWidthProperty().bind(this.widthProperty());
|
||||
button.setOnAction(e -> action.run());
|
||||
|
||||
BorderPane graphic = new BorderPane();
|
||||
graphic.setMouseTransparent(true);
|
||||
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
|
||||
|
||||
SVGPath arrow = new SVGPath();
|
||||
arrow.setContent(SVG.ARROW_RIGHT);
|
||||
BorderPane.setAlignment(arrow, Pos.CENTER);
|
||||
graphic.setRight(arrow);
|
||||
|
||||
button.setGraphic(graphic);
|
||||
|
||||
JFXDepthManager.setDepth(button, 1);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void onChooseLocalFile() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("modpack.choose"));
|
||||
@@ -89,7 +123,6 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
|
||||
controller.onNext();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onChooseRemoteFile() {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
|
||||
try {
|
||||
|
||||
@@ -17,14 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -32,59 +25,21 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.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 java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class RemoteModpackPage extends StackPane implements WizardPage {
|
||||
private final WizardController controller;
|
||||
|
||||
public final class RemoteModpackPage extends ModpackPage {
|
||||
private final ServerModpackManifest manifest;
|
||||
|
||||
@FXML
|
||||
private Region borderPane;
|
||||
|
||||
@FXML
|
||||
private Label lblName;
|
||||
|
||||
@FXML
|
||||
private Label lblVersion;
|
||||
|
||||
@FXML
|
||||
private Label lblAuthor;
|
||||
|
||||
@FXML
|
||||
private Label lblModpackLocation;
|
||||
|
||||
@FXML
|
||||
private JFXTextField txtModpackName;
|
||||
|
||||
@FXML
|
||||
private JFXButton btnInstall;
|
||||
|
||||
@FXML
|
||||
private SpinnerPane spinnerPane;
|
||||
|
||||
public RemoteModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/modpack.fxml");
|
||||
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
|
||||
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
txtModpackName.setDisable(true);
|
||||
}
|
||||
super(controller);
|
||||
|
||||
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
|
||||
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
|
||||
@@ -102,16 +57,18 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
|
||||
lblVersion.setText(manifest.getVersion());
|
||||
lblAuthor.setText(manifest.getAuthor());
|
||||
|
||||
if (!name.isPresent()) {
|
||||
Profile profile = (Profile) controller.getSettings().get("PROFILE");
|
||||
Optional<String> name = tryCast(controller.getSettings().get(MODPACK_NAME), String.class);
|
||||
if (name.isPresent()) {
|
||||
txtModpackName.setText(name.get());
|
||||
txtModpackName.setDisable(true);
|
||||
} else {
|
||||
// trim: https://github.com/huanghongxun/HMCL/issues/962
|
||||
txtModpackName.setText(manifest.getName().trim());
|
||||
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));
|
||||
btnInstall.disableProperty().bind(
|
||||
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
|
||||
.not());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,23 +77,16 @@ public class RemoteModpackPage extends StackPane implements WizardPage {
|
||||
settings.remove(MODPACK_SERVER_MANIFEST);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onInstall() {
|
||||
protected void onInstall() {
|
||||
if (!txtModpackName.validate()) return;
|
||||
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
|
||||
controller.onFinish();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onDescribe() {
|
||||
protected void onDescribe() {
|
||||
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("modpack.task.install");
|
||||
}
|
||||
|
||||
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
|
||||
public static final String MODPACK_NAME = "MODPACK_NAME";
|
||||
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
|
||||
|
||||
@@ -23,14 +23,11 @@ import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
@@ -40,21 +37,25 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.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.Holder;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -71,30 +72,18 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
|
||||
@FXML
|
||||
private JFXListView<RemoteVersion> list;
|
||||
@FXML
|
||||
private JFXSpinner spinner;
|
||||
@FXML
|
||||
private StackPane failedPane;
|
||||
@FXML
|
||||
private StackPane emptyPane;
|
||||
@FXML
|
||||
private TransitionPane root;
|
||||
@FXML
|
||||
private JFXCheckBox chkRelease;
|
||||
@FXML
|
||||
private JFXCheckBox chkSnapshot;
|
||||
@FXML
|
||||
private JFXCheckBox chkOld;
|
||||
@FXML
|
||||
private JFXButton btnRefresh;
|
||||
@FXML
|
||||
private HBox checkPane;
|
||||
@FXML
|
||||
private ComponentList centrePane;
|
||||
@FXML
|
||||
private StackPane center;
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
private final JFXSpinner spinner;
|
||||
private final StackPane failedPane;
|
||||
private final StackPane emptyPane;
|
||||
private final TransitionPane root;
|
||||
private final JFXCheckBox chkRelease;
|
||||
private final JFXCheckBox chkSnapshot;
|
||||
private final JFXCheckBox chkOld;
|
||||
private final JFXButton btnRefresh;
|
||||
private final HBox checkPane;
|
||||
private final ComponentList centrePane;
|
||||
private final StackPane center;
|
||||
|
||||
private final VersionList<?> versionList;
|
||||
private CompletableFuture<?> executor;
|
||||
@@ -105,7 +94,75 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
this.libraryId = libraryId;
|
||||
this.navigation = navigation;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
|
||||
HintPane hintPane = new HintPane();
|
||||
hintPane.setText(i18n("sponsor.bmclapi"));
|
||||
hintPane.getStyleClass().add("sponsor-pane");
|
||||
hintPane.setOnMouseClicked(e -> onSponsor());
|
||||
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
|
||||
this.setTop(hintPane);
|
||||
|
||||
root = new TransitionPane();
|
||||
{
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
center = new StackPane();
|
||||
center.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
checkPane = new HBox();
|
||||
checkPane.setSpacing(10);
|
||||
{
|
||||
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
|
||||
chkRelease.setSelected(true);
|
||||
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
|
||||
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkOld = new JFXCheckBox(i18n("version.game.old"));
|
||||
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
|
||||
|
||||
HBox pane = new HBox();
|
||||
HBox.setHgrow(pane, Priority.ALWAYS);
|
||||
|
||||
btnRefresh = new JFXButton(i18n("button.refresh"));
|
||||
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRefresh.setOnAction(e -> onRefresh());
|
||||
|
||||
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld, pane, btnRefresh);
|
||||
}
|
||||
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
centrePane.getContent().setAll(checkPane, list);
|
||||
}
|
||||
|
||||
center.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
label.setOnMouseClicked(e -> onRefresh());
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
label.setOnMouseClicked(e -> onBack());
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
}
|
||||
this.setCenter(root);
|
||||
|
||||
versionList = downloadProvider.getVersionListById(libraryId);
|
||||
if (versionList.hasType()) {
|
||||
@@ -122,86 +179,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
|
||||
|
||||
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
|
||||
IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
StackPane pane = new StackPane();
|
||||
|
||||
{
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
} else {
|
||||
content.setSubtitle("");
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
|
||||
break;
|
||||
}
|
||||
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof ForgeRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof FabricRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof FabricAPIRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
EnumMap<VersionIconType, Image> icons = new EnumMap<>(VersionIconType.class);
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, icons));
|
||||
|
||||
list.setOnMouseClicked(e -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
@@ -278,16 +258,95 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
executor.cancel(true);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onRefresh() {
|
||||
refresh();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onBack() { navigation.onPrev(true); }
|
||||
|
||||
@FXML
|
||||
private void onSponsor() {
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
private final EnumMap<VersionIconType, Image> icons;
|
||||
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, EnumMap<VersionIconType, Image> icons) {
|
||||
this.lastCell = lastCell;
|
||||
this.icons = icons;
|
||||
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
private Image getIcon(VersionIconType type) {
|
||||
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
} else {
|
||||
content.setSubtitle(null);
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(getIcon(VersionIconType.GRASS));
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(getIcon(VersionIconType.COMMAND));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(getIcon(VersionIconType.CRAFT_TABLE));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.COMMAND;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = null;
|
||||
|
||||
content.setImage(iconType != null ? getIcon(iconType) : null);
|
||||
if (content.getSubtitle() == null)
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
if (background_jpg.isFile())
|
||||
zip.putFile(background_jpg, "background.jpg");
|
||||
|
||||
File background_gif = new File("background.gif").getAbsoluteFile();
|
||||
if (background_gif.isFile())
|
||||
zip.putFile(background_gif, "background.gif");
|
||||
|
||||
zip.putFile(launcherJar.get(), launcherJar.get().getFileName().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -47,12 +49,10 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class ModpackFileSelectionPage extends StackPane implements WizardPage {
|
||||
public final class ModpackFileSelectionPage extends BorderPane implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final String version;
|
||||
private final ModAdviser adviser;
|
||||
@FXML
|
||||
private JFXTreeView<String> treeView;
|
||||
private final CheckBoxTreeItem<String> rootNode;
|
||||
|
||||
public ModpackFileSelectionPage(WizardController controller, Profile profile, String version, ModAdviser adviser) {
|
||||
@@ -60,10 +60,24 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
|
||||
this.version = version;
|
||||
this.adviser = adviser;
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/modpack/selection.fxml");
|
||||
JFXTreeView<String> treeView = new JFXTreeView<>();
|
||||
rootNode = getTreeItem(profile.getRepository().getRunDirectory(version), "minecraft");
|
||||
treeView.setRoot(rootNode);
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
this.setCenter(treeView);
|
||||
|
||||
HBox nextPane = new HBox();
|
||||
nextPane.setPadding(new Insets(16, 16, 16, 0));
|
||||
nextPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
{
|
||||
JFXButton btnNext = FXUtils.newRaisedButton(i18n("wizard.next"));
|
||||
btnNext.setPrefSize(100, 40);
|
||||
btnNext.setOnAction(e -> onNext());
|
||||
|
||||
nextPane.getChildren().setAll(btnNext);
|
||||
}
|
||||
|
||||
this.setBottom(nextPane);
|
||||
}
|
||||
|
||||
private CheckBoxTreeItem<String> getTreeItem(File file, String basePath) {
|
||||
@@ -145,7 +159,6 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
|
||||
controller.getSettings().remove(MODPACK_FILE_SELECTION);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onNext() {
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
getFilesNeeded(rootNode, "minecraft", list);
|
||||
|
||||