Compare commits
247
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7035767828 | ||
|
|
13a2777c87 | ||
|
|
a4f9438d9e | ||
|
|
4445021d66 | ||
|
|
f6a3b76a0b | ||
|
|
42bb602ff5 | ||
|
|
4547448718 | ||
|
|
10bda7e6f4 | ||
|
|
f239140055 | ||
|
|
6f4b557f63 | ||
|
|
5118ce4e62 | ||
|
|
a705b7cf95 | ||
|
|
0d5d1ae4e6 | ||
|
|
b57547415b | ||
|
|
b60d889bf7 | ||
|
|
2f463c1eda | ||
|
|
d1e006ed29 | ||
|
|
ba58905c9c | ||
|
|
de221b9dae | ||
|
|
0913c5e710 | ||
|
|
41fd38d8d1 | ||
|
|
7ec880852f | ||
|
|
9213b7fb2e | ||
|
|
80f1367d9a | ||
|
|
f521a041a9 | ||
|
|
be9efbe6ee | ||
|
|
8f3c716ca7 | ||
|
|
3cdddfa86d | ||
|
|
b1d8edc72a | ||
|
|
bbc546e2a0 | ||
|
|
f621776cdd | ||
|
|
7ba9d9eadd | ||
|
|
ac4c3a7501 | ||
|
|
f95ebe2224 | ||
|
|
7ee2c51b81 | ||
|
|
192c90816b | ||
|
|
1a772e8c65 | ||
|
|
95bed4712c | ||
|
|
2c6ceb3cb0 | ||
|
|
3118c87c65 | ||
|
|
f945a85441 | ||
|
|
72ffbb4176 | ||
|
|
77dc5dbe06 | ||
|
|
0a1d6be7b7 | ||
|
|
bb3d03fc72 | ||
|
|
a0ddaf6766 | ||
|
|
0bd2adc3c6 | ||
|
|
77c3ef6ced | ||
|
|
a66ddb9a20 | ||
|
|
eef365b37e | ||
|
|
5cab603b6e | ||
|
|
6c5d07adad | ||
|
|
1935a7021d | ||
|
|
31c46c963d | ||
|
|
4bc6ffa0f0 | ||
|
|
b62659de62 | ||
|
|
9a27c27b9a | ||
|
|
74c647cc97 | ||
|
|
e2df8e25b1 | ||
|
|
9969dc60c5 | ||
|
|
6230b436ad | ||
|
|
3873459aaa | ||
|
|
4ee3857427 | ||
|
|
2617dd3630 | ||
|
|
e0425102c8 | ||
|
|
a470475c21 | ||
|
|
bcd73e6d15 | ||
|
|
7d6f21dc3a | ||
|
|
3eddfa23b7 | ||
|
|
d5fa232704 | ||
|
|
c84afb3155 | ||
|
|
401fef5774 | ||
|
|
9298fe48e1 | ||
|
|
7f51e0395c | ||
|
|
4f410fb8ad | ||
|
|
9bcf95287a | ||
|
|
5703377626 | ||
|
|
3adb3a67e9 | ||
|
|
7918b333e6 | ||
|
|
329b0e8f3a | ||
|
|
3b17e0a897 | ||
|
|
e10f1dac20 | ||
|
|
86c171545e | ||
|
|
c3a3881cfc | ||
|
|
7f04cdd6b1 | ||
|
|
27361c65fe | ||
|
|
ffa7afcbab | ||
|
|
9aa1367823 | ||
|
|
acc96b95c8 | ||
|
|
46502875c5 | ||
|
|
43b769923b | ||
|
|
fe3fc0413c | ||
|
|
fd13088726 | ||
|
|
195db2ee6d | ||
|
|
96f68e1017 | ||
|
|
24d3bbf4fe | ||
|
|
d60a923841 | ||
|
|
f9dd7a1e64 | ||
|
|
cbb83d4f15 | ||
|
|
bab863287d | ||
|
|
e4c479754b | ||
|
|
8ebf58e8fa | ||
|
|
064b39d753 | ||
|
|
c669026312 | ||
|
|
d9b95b28b2 | ||
|
|
d492884927 | ||
|
|
cf4fde5592 | ||
|
|
fd959c41c3 | ||
|
|
e34037c055 | ||
|
|
417c804f48 | ||
|
|
ea6eced136 | ||
|
|
432465ab46 | ||
|
|
03c95cd726 | ||
|
|
730febca8c | ||
|
|
bc41499d1c | ||
|
|
60fae5cc88 | ||
|
|
c11837baa4 | ||
|
|
aaf1218536 | ||
|
|
88637b33bc | ||
|
|
462eea3539 | ||
|
|
d624086594 | ||
|
|
fa8b3a952b | ||
|
|
c70a6c3372 | ||
|
|
0616c37de7 | ||
|
|
b72056f7fe | ||
|
|
3ebe8dce44 | ||
|
|
a9c28573b8 | ||
|
|
9bead7e34e | ||
|
|
d0b36ffa15 | ||
|
|
ea081333e7 | ||
|
|
2b99da430f | ||
|
|
f1ea316bad | ||
|
|
9565c3bdfe | ||
|
|
710fd0f075 | ||
|
|
7d7dbb9eb5 | ||
|
|
88a7243b32 | ||
|
|
a0e9a20782 | ||
|
|
8889cca878 | ||
|
|
bc911b95a0 | ||
|
|
a46991bd95 | ||
|
|
395823c2b5 | ||
|
|
9769cf3996 | ||
|
|
ee652f6f31 | ||
|
|
d317844b28 | ||
|
|
d9d32eab1d | ||
|
|
24bb630afa | ||
|
|
2359ad9ed7 | ||
|
|
17abfc1a55 | ||
|
|
caa98c8956 | ||
|
|
6124b6711f | ||
|
|
3b3cc54cb5 | ||
|
|
45a40ee675 | ||
|
|
4a75f1d462 | ||
|
|
e0805fc25f | ||
|
|
f223d2bc41 | ||
|
|
5ab75b6a84 | ||
|
|
362969db95 | ||
|
|
fb37e3b0df | ||
|
|
ad312a61fe | ||
|
|
58a4700a46 | ||
|
|
68c8a30bd7 | ||
|
|
bb37e3fafa | ||
|
|
942f7b703e | ||
|
|
267da46050 | ||
|
|
7c40dca3ea | ||
|
|
48e3f79965 | ||
|
|
c5f56eb5c4 | ||
|
|
0900ad3e5a | ||
|
|
d347b0c6cb | ||
|
|
2f3bf61d94 | ||
|
|
5479607597 | ||
|
|
4291ae5f16 | ||
|
|
1ac677365b | ||
|
|
b079e50916 | ||
|
|
f986df9654 | ||
|
|
c576c48f9d | ||
|
|
5bb035c884 | ||
|
|
973b7a9716 | ||
|
|
6e05b5ee58 | ||
|
|
76ed9353bd | ||
|
|
9c0f823705 | ||
|
|
b69602b735 | ||
|
|
7cdf4504d1 | ||
|
|
d95efc4cdc | ||
|
|
48ca2d6ee6 | ||
|
|
1da98d6d67 | ||
|
|
06a89dd6a9 | ||
|
|
4eee79da17 | ||
|
|
9788338b65 | ||
|
|
fb8da249b6 | ||
|
|
2d8b1df465 | ||
|
|
1ab7ab0750 | ||
|
|
f283254ca0 | ||
|
|
2ea96fdaa7 | ||
|
|
b32d3c354a | ||
|
|
cd57b163a3 | ||
|
|
cc5484f9ed | ||
|
|
bfa9c1a697 | ||
|
|
9a37d412a3 | ||
|
|
dc5ebcf64d | ||
|
|
038f56b048 | ||
|
|
528f79e008 | ||
|
|
75d0ec34f5 | ||
|
|
d70712a18f | ||
|
|
05c5b53c52 | ||
|
|
e98c7dfef1 | ||
|
|
8ba0e16dd9 | ||
|
|
77397f6722 | ||
|
|
c9ba789b7c | ||
|
|
91135b961e | ||
|
|
435790aa03 | ||
|
|
c252d7027e | ||
|
|
9d0c8da310 | ||
|
|
84b3312ebb | ||
|
|
f351d1d133 | ||
|
|
5ca1120223 | ||
|
|
6aa4019221 | ||
|
|
73fb8bda10 | ||
|
|
cc16f84992 | ||
|
|
b4945a150e | ||
|
|
8bcbe74d82 | ||
|
|
69c2429e92 | ||
|
|
88e83dc44a | ||
|
|
87c2126e5a | ||
|
|
abf1ec3eb8 | ||
|
|
139fb41149 | ||
|
|
6c986921d6 | ||
|
|
b8e6d6fc96 | ||
|
|
75e4aee8aa | ||
|
|
abda9f50e1 | ||
|
|
c33ef5170b | ||
|
|
a3cb871928 | ||
|
|
62e965c6fe | ||
|
|
99a031cea4 | ||
|
|
f97b9b9382 | ||
|
|
a8929560df | ||
|
|
22c8c59b5d | ||
|
|
78b043c19c | ||
|
|
fd51fb7d26 | ||
|
|
43c59dad9c | ||
|
|
81b0547610 | ||
|
|
997a16486b | ||
|
|
36ebdab698 | ||
|
|
a6471bca09 | ||
|
|
cb6320ffae | ||
|
|
7cf520cb48 | ||
|
|
7dc922d4bd |
No files matched your search
@@ -20,16 +20,6 @@ body:
|
||||
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
|
||||
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord server](https://discord.gg/jVvC7HfM6U) for help.
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台 | Platform
|
||||
description: |
|
||||
请输入你遇到问题的平台。
|
||||
Please enter the platform on which you encountered the bug.
|
||||
placeholder: e.g. Windows 11
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-report
|
||||
attributes:
|
||||
@@ -56,4 +46,4 @@ body:
|
||||
If your launcher does not crash, please DO NOT EXIT your launcher, click "Export Launcher Logs" in the "Settings → General → Debug" of the launcher, and drag the exported log to the following input field.
|
||||
**ATTENTION: The crash report or log file is the key to resolving the bug. Please upload them!**
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
@@ -9,12 +9,12 @@ body:
|
||||
请确认 Issues 列表无重复的项目。
|
||||
Please make sure that no duplicate issues have already been submitted.
|
||||
- type: textarea
|
||||
id: description
|
||||
id: summary
|
||||
attributes:
|
||||
label: 描述 | Description
|
||||
label: 概述 | Summary
|
||||
description: |
|
||||
请详细描述你想加入的新功能。
|
||||
Please describe the new feature in detail.
|
||||
请介绍你想加入的新功能。
|
||||
Please describe the new feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -26,3 +26,10 @@ body:
|
||||
Please describe why you want to add the feature or enhancement to HMCL.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 详情 | Description
|
||||
description: |
|
||||
在这里可以补充描述该功能的具体实现方式或建议。(可选)
|
||||
Describe implementation details or suggestions here. (Optional)
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Build HMCLauncher
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'HMCLauncher/**'
|
||||
- '.github/workflows/build-launcher.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'HMCLauncher/**'
|
||||
- '.github/workflows/build-launcher.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
with:
|
||||
msbuild-architecture: x86
|
||||
- name: Build HMCLauncher
|
||||
run: msbuild /p:Configuration=Release /t:Rebuild /verbosity:detailed .\HMCLauncher\
|
||||
- name: Copy HMCLauncher to assets
|
||||
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\src\main\resources\assets\HMCLauncher.exe
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: .\gradlew makeExecutables --no-daemon
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=$("${{ github.sha }}".SubString(0, 7))" >> $env:GITHUB_ENV
|
||||
- name: Copy HMCLauncher to libs
|
||||
run: Copy-Item .\HMCLauncher\Release\HMCLauncher.exe -Destination .\HMCL\build\libs\HMCLauncher.exe
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: HMCLauncher-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs/*.exe
|
||||
@@ -1,11 +1,13 @@
|
||||
name: Check Translations
|
||||
name: Check Codes
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
|
||||
jobs:
|
||||
@@ -13,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations --no-daemon
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Check Style
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain --no-daemon
|
||||
- name: Check style test
|
||||
run: ./gradlew checkstyleTest --no-daemon
|
||||
@@ -5,7 +5,6 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'HMCLauncher/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -14,11 +13,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 8
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 8
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
||||
|
||||
+3
-1
@@ -13,11 +13,13 @@ hs_err_pid*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLBoot/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/buildSrc/build/
|
||||
@@ -42,7 +44,6 @@ minecraft-exported-crash-info*
|
||||
.nb-gradle
|
||||
|
||||
*.exe
|
||||
!/HMCL/src/main/resources/assets/HMCLauncher.exe
|
||||
|
||||
# macos
|
||||
.DS_Store
|
||||
@@ -53,3 +54,4 @@ minecraft-exported-crash-info*
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
/.hmcl/
|
||||
+81
-41
@@ -9,7 +9,7 @@ import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
plugins {
|
||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
||||
@@ -33,12 +33,24 @@ val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
val embedResources by configurations.registering
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
implementation("libs:JFoenix")
|
||||
implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
|
||||
if (launcherExe == null) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
embedResources(libs.authlib.injector)
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
@@ -82,23 +94,20 @@ fun attachSignature(jar: File) {
|
||||
}
|
||||
}
|
||||
|
||||
val java11 = sourceSets.create("java11") {
|
||||
java {
|
||||
srcDir("src/main/java11")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
|
||||
if (JavaVersion.current() < JavaVersion.VERSION_11) {
|
||||
javaCompiler.set(javaToolchains.compilerFor {
|
||||
languageVersion.set(JavaLanguageVersion.of(11))
|
||||
})
|
||||
}
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
}
|
||||
|
||||
tasks.checkstyleMain {
|
||||
// Third-party code is not checked
|
||||
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
enabled = false
|
||||
dependsOn(tasks["shadowJar"])
|
||||
@@ -106,7 +115,7 @@ tasks.jar {
|
||||
|
||||
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
|
||||
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
|
||||
tasks.shadowJar {
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
exclude("**/package-info.class")
|
||||
@@ -115,9 +124,20 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
|
||||
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
||||
|
||||
listOf(
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
||||
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
||||
).forEach { exclude("com/sun/jna/$it/**") }
|
||||
|
||||
minimize {
|
||||
exclude(dependency("com.google.code.gson:.*:.*"))
|
||||
exclude(dependency("net.java.dev.jna:jna:.*"))
|
||||
exclude(dependency("libs:JFoenix:.*"))
|
||||
exclude(project(":HMCLBoot"))
|
||||
}
|
||||
|
||||
into("assets/") {
|
||||
from(embedResources)
|
||||
}
|
||||
|
||||
manifest {
|
||||
@@ -129,8 +149,8 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Authlib-Injector-Version" to libs.authlib.injector.get().version!!,
|
||||
"Build-Channel" to versionType,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
@@ -154,35 +174,43 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
}
|
||||
}
|
||||
|
||||
if (launcherExe != null) {
|
||||
into("assets") {
|
||||
from(file(launcherExe))
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
attachSignature(jarPath)
|
||||
createChecksum(jarPath)
|
||||
}
|
||||
}
|
||||
|
||||
fun createExecutable(suffix: String, header: String) {
|
||||
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + suffix)
|
||||
val makeExecutables by tasks.registering {
|
||||
val extensions = listOf("exe", "sh")
|
||||
|
||||
output.outputStream().use {
|
||||
it.write(File(project.projectDir, header).readBytes())
|
||||
it.write(jarPath.readBytes())
|
||||
}
|
||||
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
into("META-INF/versions/11") {
|
||||
from(sourceSets["java11"].output)
|
||||
}
|
||||
dependsOn(tasks["java11Classes"])
|
||||
}
|
||||
|
||||
val makeExecutables = tasks.create("makeExecutables") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
inputs.file(jarPath)
|
||||
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
|
||||
|
||||
doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
|
||||
val jarContent = jarPath.readBytes()
|
||||
|
||||
ZipFile(jarPath).use { zipFile ->
|
||||
for (extension in extensions) {
|
||||
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
|
||||
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
|
||||
?: throw GradleException("HMCLauncher.$extension not found")
|
||||
|
||||
output.outputStream().use { outputStream ->
|
||||
zipFile.getInputStream(entry).use { it.copyTo(outputStream) }
|
||||
outputStream.write(jarContent)
|
||||
}
|
||||
|
||||
createChecksum(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,9 +218,9 @@ tasks.build {
|
||||
dependsOn(makeExecutables)
|
||||
}
|
||||
|
||||
fun parseToolOptions(options: String?): List<String> {
|
||||
fun parseToolOptions(options: String?): MutableList<String> {
|
||||
if (options == null)
|
||||
return listOf()
|
||||
return mutableListOf()
|
||||
|
||||
val builder = StringBuilder()
|
||||
val result = mutableListOf<String>()
|
||||
@@ -240,7 +268,7 @@ fun parseToolOptions(options: String?): List<String> {
|
||||
return result
|
||||
}
|
||||
|
||||
tasks.create<JavaExec>("run") {
|
||||
tasks.register<JavaExec>("run") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
group = "application"
|
||||
@@ -249,10 +277,22 @@ tasks.create<JavaExec>("run") {
|
||||
workingDir = rootProject.rootDir
|
||||
|
||||
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
|
||||
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
|
||||
vmOptions += "-Dhmcl.offline.auth.restricted=false"
|
||||
|
||||
jvmArgs(vmOptions)
|
||||
|
||||
val hmclJavaHome = System.getenv("HMCL_JAVA_HOME")
|
||||
if (hmclJavaHome != null) {
|
||||
this.executable(
|
||||
file(hmclJavaHome).resolve("bin")
|
||||
.resolve(if (System.getProperty("os.name").lowercase().startsWith("windows")) "java.exe" else "java")
|
||||
)
|
||||
}
|
||||
|
||||
doFirst {
|
||||
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
|
||||
logger.quiet("HMCL_JAVA_OPTS: {}", vmOptions)
|
||||
logger.quiet("HMCL_JAVA_HOME: {}", hmclJavaHome ?: System.getProperty("java.home"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class EntryPoint {
|
||||
|
||||
private EntryPoint() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
initIcon();
|
||||
|
||||
checkJavaFX();
|
||||
verifyJavaFX();
|
||||
addEnableNativeAccess();
|
||||
enableUnsafeMemoryAccess();
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
public static void exit(int exitCode) {
|
||||
FileSaver.shutdown();
|
||||
LOG.shutdown();
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void createHMCLDirectories() {
|
||||
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
|
||||
try {
|
||||
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Logger has not been started yet, so print directly to System.err
|
||||
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
e.printStackTrace(System.err);
|
||||
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
|
||||
}
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
|
||||
try {
|
||||
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDir = System.getProperty("user.dir", "");
|
||||
if (currentDir.contains("!")) {
|
||||
LOG.error("The current working path contains an exclamation mark: " + currentDir);
|
||||
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||
+ "The path is " + currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.error("Unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.error("User cancels downloading JavaFX", e);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if JavaFX exists but is incomplete
|
||||
*/
|
||||
private static void verifyJavaFX() {
|
||||
try {
|
||||
Class.forName("javafx.beans.binding.Binding"); // javafx.base
|
||||
Class.forName("javafx.stage.Stage"); // javafx.graphics
|
||||
Class.forName("javafx.scene.control.Skin"); // javafx.controls
|
||||
} catch (Exception e) {
|
||||
LOG.warning("JavaFX is incomplete or not found", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addEnableNativeAccess() {
|
||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||
try {
|
||||
// javafx.graphics
|
||||
Module module = Class.forName("javafx.stage.Stage").getModule();
|
||||
if (module.isNamed()) {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
|
||||
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
|
||||
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
|
||||
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
LOG.error("Failed to add enable native access for JavaFX", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void enableUnsafeMemoryAccess() {
|
||||
// https://openjdk.org/jeps/498
|
||||
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("sun.misc.Unsafe");
|
||||
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
|
||||
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
|
||||
.invokeExact();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to enable unsafe memory access", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
private static void showErrorAndExit(String message) {
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
@@ -27,6 +28,8 @@ import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -38,11 +41,14 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.nio.file.Files;
|
||||
@@ -50,9 +56,12 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -73,20 +82,24 @@ public final class Launcher extends Application {
|
||||
LOG.warning("Failed to get prism pipeline", e);
|
||||
}
|
||||
|
||||
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
|
||||
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
|
||||
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (SambaException e) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load config", e);
|
||||
checkConfigInTempDir();
|
||||
checkConfigOwner();
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
// https://lapcatsoftware.com/articles/app-translocation.html
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& 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)
|
||||
@@ -100,8 +113,8 @@ public final class Launcher extends Application {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
|
||||
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
@@ -148,7 +161,7 @@ public final class Launcher extends Application {
|
||||
|| configPath.startsWith("/var/cache/")
|
||||
|| configPath.startsWith("/dev/shm/")
|
||||
|| configPath.contains("/Trash/");
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
return configPath.startsWith("/var/folders/")
|
||||
|| configPath.startsWith("/private/var/folders/")
|
||||
|| configPath.startsWith("/tmp/")
|
||||
@@ -164,7 +177,7 @@ public final class Launcher extends Application {
|
||||
private static void checkConfigInTempDir() {
|
||||
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
|
||||
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
|
||||
Main.exit(0);
|
||||
EntryPoint.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +199,10 @@ public final class Launcher extends Application {
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
files.add(ConfigHolder.configLocation().toString());
|
||||
if (Files.exists(Metadata.HMCL_DIRECTORY))
|
||||
files.add(Metadata.HMCL_DIRECTORY.toString());
|
||||
if (Files.exists(Metadata.HMCL_GLOBAL_DIRECTORY))
|
||||
files.add(Metadata.HMCL_GLOBAL_DIRECTORY.toString());
|
||||
if (Files.exists(Metadata.HMCL_CURRENT_DIRECTORY))
|
||||
files.add(Metadata.HMCL_CURRENT_DIRECTORY.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
@@ -202,12 +217,13 @@ public final class Launcher extends Application {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
Main.exit(1);
|
||||
EntryPoint.exit(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
Controllers.onApplicationStop();
|
||||
FileSaver.shutdown();
|
||||
LOG.shutdown();
|
||||
}
|
||||
|
||||
@@ -238,21 +254,30 @@ 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: " + System.getProperty("user.dir"));
|
||||
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
|
||||
LOG.info("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY);
|
||||
LOG.info("HMCL Current Directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
||||
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
|
||||
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
|
||||
.filter(bean -> bean.getName().equals("Metaspace"))
|
||||
.findAny()
|
||||
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
|
||||
.orElse("Unknown"));
|
||||
LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
|
||||
try {
|
||||
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||
if ("Metaspace".equals(bean.getName())) {
|
||||
long bytes = bean.getUsage().getUsed();
|
||||
LOG.info("Metaspace: " + MEGABYTES.formatBytes(bytes));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (NoClassDefFoundError ignored) {
|
||||
}
|
||||
LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
|
||||
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
|
||||
}
|
||||
|
||||
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
|
||||
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
@@ -281,7 +306,7 @@ public final class Launcher extends Application {
|
||||
Controllers.getStage().close();
|
||||
Schedulers.shutdown();
|
||||
Controllers.shutdown();
|
||||
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
|
||||
Lang.executeDelayed(System::gc, TimeUnit.SECONDS, 5, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.ModuleHelper;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Main {
|
||||
|
||||
private Main() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
|
||||
LOG.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
if (JavaRuntime.CURRENT_VERSION < 9)
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
initIcon();
|
||||
|
||||
checkJavaFX();
|
||||
verifyJavaFX();
|
||||
addEnableNativeAccess();
|
||||
enableUnsafeMemoryAccess();
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
public static void exit(int exitCode) {
|
||||
LOG.shutdown();
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
String currentDirectory = new File("").getAbsolutePath();
|
||||
if (currentDirectory.contains("!")) {
|
||||
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
|
||||
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
|
||||
+ "The path is " + currentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
SelfDependencyPatcher.patch();
|
||||
} catch (SelfDependencyPatcher.PatchException e) {
|
||||
LOG.error("unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.missing"));
|
||||
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
|
||||
LOG.error("unable to patch JVM", e);
|
||||
showErrorAndExit(i18n("fatal.javafx.incompatible"));
|
||||
} catch (CancellationException e) {
|
||||
LOG.error("User cancels downloading JavaFX", e);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if JavaFX exists but is incomplete
|
||||
*/
|
||||
private static void verifyJavaFX() {
|
||||
try {
|
||||
Class.forName("javafx.beans.binding.Binding"); // javafx.base
|
||||
Class.forName("javafx.stage.Stage"); // javafx.graphics
|
||||
Class.forName("javafx.scene.control.Skin"); // javafx.controls
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addEnableNativeAccess() {
|
||||
if (JavaRuntime.CURRENT_VERSION > 21) {
|
||||
try {
|
||||
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace(System.err);
|
||||
showErrorAndExit(i18n("fatal.javafx.incomplete"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void enableUnsafeMemoryAccess() {
|
||||
// https://openjdk.org/jeps/498
|
||||
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("sun.misc.Unsafe");
|
||||
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
|
||||
trySetMemoryAccessWarned.setAccessible(true);
|
||||
trySetMemoryAccessWarned.invoke(null);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
static void showErrorAndExit(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("A fatal error has occurred, forcibly exiting.");
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.ERROR, message).showAndWait();
|
||||
exit(1);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showErrorDialog(message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
|
||||
*/
|
||||
static void showWarningAndContinue(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("Potential issues have been detected.");
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.WARNING, message).showAndWait();
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
private static void fixLetsEncrypt() {
|
||||
try {
|
||||
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
|
||||
|
||||
try (InputStream ksStream = Files.newInputStream(ksPath)) {
|
||||
defaultKeyStore.load(ksStream, "changeit".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
|
||||
for (String alias : Collections.list(defaultKeyStore.aliases()))
|
||||
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
|
||||
|
||||
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
instance.init(merged);
|
||||
SSLContext tls = SSLContext.getInstance("TLS");
|
||||
tls.init(null, instance.getTrustManagers(), null);
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
|
||||
LOG.info("Added Lets Encrypt root certificates as additional trust");
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
|
||||
KeyManagementException e) {
|
||||
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,8 @@ import java.nio.file.Paths;
|
||||
* Stores metadata about this application.
|
||||
*/
|
||||
public final class Metadata {
|
||||
private Metadata() {}
|
||||
private Metadata() {
|
||||
}
|
||||
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
@@ -37,18 +38,25 @@ public final class Metadata {
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://docs.hmcl.net/help.html";
|
||||
public static final String HELP_URL = "https://docs.hmcl.net";
|
||||
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
||||
public static final String EULA_URL = "https://docs.hmcl.net/eula/hmcl.html";
|
||||
public static final String ABOUT_URL = PUBLISH_URL + "/about";
|
||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
|
||||
|
||||
public static final String DOCS_URL = "https://docs.hmcl.net";
|
||||
public static final String CONTACT_URL = DOCS_URL + "/help.html";
|
||||
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
|
||||
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
||||
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
|
||||
|
||||
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY;
|
||||
public static final Path HMCL_GLOBAL_DIRECTORY;
|
||||
public static final Path HMCL_CURRENT_DIRECTORY;
|
||||
public static final Path DEPENDENCIES_DIRECTORY;
|
||||
|
||||
static {
|
||||
String hmclHome = System.getProperty("hmcl.home");
|
||||
@@ -56,16 +64,22 @@ public final class Metadata {
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||
if (StringUtils.isNotBlank(xdgData)) {
|
||||
HMCL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath();
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
|
||||
} else {
|
||||
HMCL_DIRECTORY = Paths.get(System.getProperty("user.home", "."), ".local", "share", "hmcl").toAbsolutePath();
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
||||
}
|
||||
} else {
|
||||
HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
} else {
|
||||
HMCL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
String hmclCurrentDir = System.getProperty("hmcl.dir");
|
||||
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
|
||||
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
|
||||
: CURRENT_DIRECTORY.resolve(".hmcl");
|
||||
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
|
||||
}
|
||||
|
||||
public static boolean isStable() {
|
||||
@@ -79,4 +93,5 @@ public final class Metadata {
|
||||
public static boolean isNightly() {
|
||||
return !isStable() && !isDev();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.ProxyManager;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -40,6 +40,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -150,20 +151,28 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir, dstDir);
|
||||
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||
blackList.add(srcId + ".jar");
|
||||
blackList.add(srcId + ".json");
|
||||
if (!copySaves)
|
||||
blackList.add("saves");
|
||||
|
||||
Path fromJson = dstDir.resolve(srcId + ".json");
|
||||
Path fromJar = dstDir.resolve(srcId + ".jar");
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
|
||||
Files.createDirectories(dstDir);
|
||||
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
|
||||
|
||||
Path fromJson = srcDir.resolve(srcId + ".json");
|
||||
Path fromJar = srcDir.resolve(srcId + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId + ".json");
|
||||
Path toJar = dstDir.resolve(dstId + ".jar");
|
||||
|
||||
if (Files.exists(fromJar)) {
|
||||
Files.move(fromJar, toJar);
|
||||
Files.copy(fromJar, toJar);
|
||||
}
|
||||
Files.move(fromJson, toJson);
|
||||
Files.copy(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
@@ -175,12 +184,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
File srcGameDir = getRunDirectory(srcId);
|
||||
File dstGameDir = getRunDirectory(dstId);
|
||||
|
||||
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||
blackList.add(srcId + ".jar");
|
||||
blackList.add(srcId + ".json");
|
||||
if (!copySaves)
|
||||
blackList.add("saves");
|
||||
|
||||
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
|
||||
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
|
||||
}
|
||||
@@ -193,7 +196,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (file.exists())
|
||||
try {
|
||||
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
|
||||
VersionSetting versionSetting = GSON.fromJson(Files.readString(file.toPath()), VersionSetting.class);
|
||||
initLocalVersionSetting(id, versionSetting);
|
||||
} catch (Exception ex) {
|
||||
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
|
||||
@@ -203,6 +206,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
/**
|
||||
* Create new version setting if version id has no version setting.
|
||||
*
|
||||
* @param id the version id.
|
||||
* @return new version setting, null if given version does not exist.
|
||||
*/
|
||||
@@ -225,7 +229,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
* Get the version setting for version id.
|
||||
*
|
||||
* @param id version id
|
||||
*
|
||||
* @return corresponding version setting, null if the version has no its own version setting.
|
||||
*/
|
||||
@Nullable
|
||||
@@ -329,26 +332,22 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean saveVersionSetting(String id) {
|
||||
public void saveVersionSetting(String id) {
|
||||
if (!localVersionSettings.containsKey(id))
|
||||
return false;
|
||||
File file = getLocalVersionSettingFile(id);
|
||||
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
|
||||
return false;
|
||||
|
||||
LOG.info("Saving version setting: " + id);
|
||||
|
||||
return;
|
||||
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
|
||||
try {
|
||||
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
|
||||
return true;
|
||||
Files.createDirectories(file.getParent());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to save version setting of " + id, e);
|
||||
return false;
|
||||
LOG.warning("Failed to create directory: " + file.getParent(), e);
|
||||
}
|
||||
|
||||
FileSaver.save(file, GSON.toJson(localVersionSettings.get(id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make version use self version settings instead of the global one.
|
||||
*
|
||||
* @param id the version id.
|
||||
* @return specialized version setting, null if given version does not exist.
|
||||
*/
|
||||
@@ -383,7 +382,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
|
||||
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
) / 1024 / 1024))
|
||||
.setMinMemory(vs.getMinMemory())
|
||||
@@ -415,8 +414,12 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
|
||||
.setJavaAgents(javaAgents)
|
||||
.setJavaArguments(javaArguments);
|
||||
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
builder.setProxyType(config().getProxyType());
|
||||
builder.setProxyHost(config().getProxyHost());
|
||||
builder.setProxyPort(config().getProxyPort());
|
||||
|
||||
if (config().hasProxyAuth()) {
|
||||
builder.setProxyUser(config().getProxyUser());
|
||||
builder.setProxyPass(config().getProxyPass());
|
||||
@@ -426,7 +429,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
File json = getModpackConfiguration(version);
|
||||
if (json.exists()) {
|
||||
try {
|
||||
String jsonText = FileUtils.readText(json);
|
||||
String jsonText = Files.readString(json.toPath());
|
||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
|
||||
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
|
||||
@@ -509,16 +512,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
|
||||
if (auto) {
|
||||
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
|
||||
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
|
||||
if (available <= 0) {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
final long threshold = 8L * 1024 * 1024 * 1024;
|
||||
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
|
||||
final long suggested = Math.min(available <= threshold
|
||||
? (long) (available * 0.8)
|
||||
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
|
||||
16384L * 1024 * 1024);
|
||||
16L * 1024 * 1024 * 1024);
|
||||
return Math.max(minimum, suggested);
|
||||
} else {
|
||||
return minimum;
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -66,7 +65,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
|
||||
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(Modpack.class));
|
||||
|
||||
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
@@ -64,7 +64,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException {
|
||||
public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
|
||||
|
||||
@@ -53,16 +53,18 @@ import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static javafx.application.Platform.runLater;
|
||||
import static javafx.application.Platform.setImplicitExit;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -124,6 +126,9 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
private void launch0() {
|
||||
// https://github.com/HMCL-dev/HMCL/pull/4121
|
||||
PROCESSES.removeIf(it -> it.get() == null);
|
||||
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
|
||||
@@ -155,8 +160,10 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getMesaLoader(java);
|
||||
if (renderer != Renderer.DEFAULT
|
||||
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& OperatingSystem.WINDOWS_VERSION != null) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
@@ -208,7 +215,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
}).thenAcceptAsync(process -> { // process is LaunchTask's result
|
||||
if (scriptFile == null) {
|
||||
PROCESSES.add(process);
|
||||
PROCESSES.add(new WeakReference<>(process));
|
||||
if (launcherVisibility == LauncherVisibility.CLOSE)
|
||||
Launcher.stopApplication();
|
||||
else
|
||||
@@ -262,27 +269,27 @@ public final class LauncherHelper {
|
||||
if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", url);
|
||||
message += i18n("download.code.404", uri);
|
||||
else
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof DownloadException) {
|
||||
URL url = ((DownloadException) ex).getUrl();
|
||||
URI uri = ((DownloadException) ex).getUri();
|
||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||
message = i18n("install.failed.downloading.timeout", url);
|
||||
message = i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else {
|
||||
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
|
||||
}
|
||||
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||
message = i18n("assets.index.malformed");
|
||||
@@ -293,11 +300,11 @@ public final class LauncherHelper {
|
||||
} else if (ex instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex;
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message = i18n("download.code.404", url);
|
||||
message = i18n("download.code.404", uri);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
message = i18n("download.failed", uri, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
@@ -553,8 +560,9 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
|
||||
long totalMemorySizeMB = (long) MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
|
||||
if (totalMemorySizeMB > 0 && totalMemorySizeMB < setting.getMaxMemory()) {
|
||||
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
|
||||
}
|
||||
|
||||
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
|
||||
@@ -894,13 +902,10 @@ public final class LauncherHelper {
|
||||
|
||||
}
|
||||
|
||||
private static final String ORACLEJDK_DOWNLOAD_LINK = "https://www.java.com/download/";
|
||||
private static final String OPENJDK_DOWNLOAD_LINK = "https://learn.microsoft.com/java/openjdk/download";
|
||||
|
||||
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
public static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
while (!PROCESSES.isEmpty())
|
||||
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
|
||||
Optional.ofNullable(PROCESSES.poll()).map(WeakReference::get).ifPresent(ManagedProcess::stop);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -41,9 +42,9 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
if (!StringUtils.containsChinese(searchFilter)) {
|
||||
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||
}
|
||||
|
||||
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
|
||||
@@ -65,7 +66,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
|
||||
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
|
||||
{
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
SearchResult searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
|
||||
if (chineseIndex > englishIndex) {
|
||||
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
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.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
@@ -93,9 +94,8 @@ public final class LogExporter {
|
||||
if (Files.isRegularFile(file)) {
|
||||
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
|
||||
if (time.toMillis() >= processStartTime) {
|
||||
try {
|
||||
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
|
||||
zipper.putTextFile(crashLog, file.getFileName().toString());
|
||||
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
|
||||
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read log file: " + file, e);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
@@ -83,7 +83,7 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
|
||||
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
|
||||
// Order for trying detecting manifest is necessary here.
|
||||
// Do not change to iterating providers.
|
||||
for (ModpackProvider provider : new ModpackProvider[]{
|
||||
@@ -140,7 +140,7 @@ public final class ModpackHelper {
|
||||
throw new FileNotFoundException(file.getPath());
|
||||
else
|
||||
try {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
|
||||
return JsonUtils.fromJsonFile(file.toPath(), ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -87,7 +86,7 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final Path TEXTURES_DIR = Metadata.HMCL_DIRECTORY.resolve("skins");
|
||||
private static final Path TEXTURES_DIR = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("skins");
|
||||
|
||||
private static Path getTexturePath(Texture texture) {
|
||||
String url = texture.getUrl();
|
||||
@@ -110,7 +109,7 @@ public final class TexturesLoader {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
|
||||
new FileDownloadTask(texture.getUrl(), file).run();
|
||||
LOG.info("Texture downloaded: " + texture.getUrl());
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -83,7 +82,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
try {
|
||||
return JavaManager.getExecutable(javaDir).toRealPath();
|
||||
} catch (IOException ignored) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
|
||||
try {
|
||||
return JavaManager.getMacExecutable(javaDir).toRealPath();
|
||||
} catch (IOException ignored1) {
|
||||
@@ -98,14 +97,8 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<JavaRuntime> getAllJava(Platform platform) {
|
||||
Path root = getPlatformRoot(platform);
|
||||
if (!Files.isDirectory(root))
|
||||
return Collections.emptyList();
|
||||
|
||||
ArrayList<JavaRuntime> list = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
|
||||
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
|
||||
for (Path file : stream) {
|
||||
try {
|
||||
String name = file.getFileName().toString();
|
||||
@@ -115,19 +108,15 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
try {
|
||||
executable = JavaManager.getExecutable(javaDir).toRealPath();
|
||||
} catch (IOException e) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.OSX)
|
||||
if (platform.getOperatingSystem() == OperatingSystem.MACOS)
|
||||
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (Files.isDirectory(javaDir)) {
|
||||
JavaManifest manifest;
|
||||
try (InputStream input = Files.newInputStream(file)) {
|
||||
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
|
||||
}
|
||||
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), true));
|
||||
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@@ -137,6 +126,23 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<JavaRuntime> getAllJava(Platform platform) {
|
||||
Path platformRoot = getPlatformRoot(platform);
|
||||
if (!Files.isDirectory(platformRoot))
|
||||
return Collections.emptyList();
|
||||
|
||||
ArrayList<JavaRuntime> list = new ArrayList<>();
|
||||
|
||||
getAllJava(list, platform, platformRoot, true);
|
||||
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
|
||||
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
|
||||
if (Files.isDirectory(platformRoot))
|
||||
getAllJava(list, platform, platformRoot, false);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -149,7 +155,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
try {
|
||||
executable = JavaManager.getExecutable(javaDir).toRealPath();
|
||||
} catch (IOException e) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.OSX)
|
||||
if (platform.getOperatingSystem() == OperatingSystem.MACOS)
|
||||
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
|
||||
else
|
||||
throw e;
|
||||
@@ -157,7 +163,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
|
||||
JavaInfo info;
|
||||
if (JavaManager.isCompatible(platform))
|
||||
info = JavaInfo.fromExecutable(executable, false);
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
else
|
||||
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
|
||||
|
||||
@@ -180,7 +186,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
});
|
||||
|
||||
JavaManifest manifest = new JavaManifest(info, update, files);
|
||||
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
|
||||
return JavaRuntime.of(executable, info, true);
|
||||
});
|
||||
}
|
||||
@@ -192,7 +198,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
|
||||
|
||||
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
|
||||
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
|
||||
return JavaRuntime.of(executable, result.getInfo(), true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
* @see <a href="https://github.com/Glavo/java-info">Glavo/java-info</a>
|
||||
*/
|
||||
public final class JavaInfoUtils {
|
||||
|
||||
private JavaInfoUtils() {
|
||||
}
|
||||
|
||||
private static Path tryFindReleaseFile(Path executable) {
|
||||
Path parent = executable.getParent();
|
||||
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
|
||||
Path javaHome = parent.getParent();
|
||||
if (javaHome != null && javaHome.getFileName() != null) {
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
String javaHomeName = javaHome.getFileName().toString();
|
||||
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
|
||||
&& Files.isRegularFile(releaseFile)) {
|
||||
return releaseFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
|
||||
assert executable.isAbsolute();
|
||||
|
||||
Path releaseFile;
|
||||
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
|
||||
try {
|
||||
return JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
Path thisPath = JarUtils.thisJarPath();
|
||||
|
||||
if (thisPath == null) {
|
||||
throw new IOException("Failed to find current HMCL location");
|
||||
}
|
||||
|
||||
try {
|
||||
Result result = JsonUtils.GSON.fromJson(SystemUtils.run(
|
||||
executable.toString(),
|
||||
"-classpath",
|
||||
thisPath.toString(),
|
||||
org.glavo.info.Main.class.getName()
|
||||
), Result.class);
|
||||
|
||||
if (result == null) {
|
||||
throw new IOException("Failed to get Java info from " + executable);
|
||||
}
|
||||
|
||||
if (result.javaVersion == null) {
|
||||
throw new IOException("Failed to get Java version from " + executable);
|
||||
}
|
||||
|
||||
Architecture architecture = Architecture.parseArchName(result.osArch);
|
||||
Platform platform = Platform.getPlatform(OperatingSystem.CURRENT_OS,
|
||||
architecture != Architecture.UNKNOWN
|
||||
? architecture
|
||||
: Architecture.SYSTEM_ARCH);
|
||||
|
||||
|
||||
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Result {
|
||||
@SerializedName("os.name")
|
||||
public String osName;
|
||||
@SerializedName("os.arch")
|
||||
public String osArch;
|
||||
@SerializedName("java.version")
|
||||
public String javaVersion;
|
||||
@SerializedName("java.vendor")
|
||||
public String javaVendor;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import kala.compress.archivers.ArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
|
||||
@@ -31,18 +31,15 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -58,7 +55,8 @@ public final class JavaManager {
|
||||
private JavaManager() {
|
||||
}
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
|
||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
|
||||
|
||||
public static String getMojangJavaPlatform(Platform platform) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||
@@ -75,7 +73,7 @@ public final class JavaManager {
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "linux";
|
||||
}
|
||||
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
|
||||
} else if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "mac-os";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
@@ -113,7 +111,7 @@ public final class JavaManager {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
return architecture == Architecture.X86;
|
||||
break;
|
||||
case OSX:
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
return architecture == Architecture.X86_64;
|
||||
break;
|
||||
@@ -162,7 +160,7 @@ public final class JavaManager {
|
||||
return javaRuntime;
|
||||
}
|
||||
|
||||
JavaInfo info = JavaInfo.fromExecutable(executable);
|
||||
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
|
||||
return JavaRuntime.of(executable, info, false);
|
||||
}
|
||||
|
||||
@@ -279,7 +277,7 @@ public final class JavaManager {
|
||||
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
|
||||
|
||||
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
|
||||
|
||||
GameJavaVersion suggestedJavaVersion =
|
||||
@@ -349,18 +347,18 @@ public final class JavaManager {
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
}
|
||||
break;
|
||||
case OSX:
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
|
||||
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
|
||||
@@ -375,7 +373,7 @@ public final class JavaManager {
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
|
||||
break;
|
||||
case OSX:
|
||||
case MACOS:
|
||||
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
|
||||
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
|
||||
@@ -405,24 +403,31 @@ public final class JavaManager {
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
|
||||
// Search in PATH.
|
||||
if (System.getenv("PATH") != null) {
|
||||
String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR);
|
||||
String[] paths = System.getenv("PATH").split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4079
|
||||
// https://github.com/Meloong-Git/PCL/issues/4261
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
|
||||
.contains("\\common files\\oracle\\java\\")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (System.getenv("HMCL_JRES") != null) {
|
||||
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
|
||||
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(path));
|
||||
@@ -483,12 +488,15 @@ public final class JavaManager {
|
||||
try {
|
||||
info = JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
info = JavaInfo.fromExecutable(executable, false);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e2);
|
||||
}
|
||||
LOG.warning("Failed to read release file " + releaseFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +517,7 @@ public final class JavaManager {
|
||||
|
||||
JavaInfo info = null;
|
||||
try {
|
||||
info = JavaInfo.fromExecutable(executable);
|
||||
info = JavaInfoUtils.fromExecutable(executable, true);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
@@ -548,7 +556,7 @@ public final class JavaManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Path macPath = dir.resolve("jre.bundle/Contents/Home");
|
||||
if (Files.exists(macPath)) {
|
||||
tryAddJavaHome(javaRuntimes, macPath);
|
||||
@@ -564,6 +572,10 @@ public final class JavaManager {
|
||||
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
|
||||
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
|
||||
@@ -587,8 +599,8 @@ public final class JavaManager {
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_X86_64), verify);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,67 +658,22 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
// ==== Windows Registry Support ====
|
||||
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
|
||||
for (String java : querySubFolders(location)) {
|
||||
if (!querySubFolders(java).contains(java + "\\MSI"))
|
||||
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg == null)
|
||||
return;
|
||||
|
||||
for (String java : reg.querySubKeys(hkey, location)) {
|
||||
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
|
||||
continue;
|
||||
String home = queryRegisterValue(java, "JavaHome");
|
||||
if (home != null) {
|
||||
Object home = reg.queryValue(hkey, java, "JavaHome");
|
||||
if (home instanceof String) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(home));
|
||||
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path in system registry: " + home);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> querySubFolders(String location) {
|
||||
List<String> res = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
if (line.startsWith(location) && !line.equals(location)) {
|
||||
res.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to query sub folders of " + location, e);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static String queryRegisterValue(String location, String name) {
|
||||
boolean last = false;
|
||||
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
if (StringUtils.isNotBlank(line)) {
|
||||
if (last && line.trim().startsWith(name)) {
|
||||
int begins = line.indexOf(name);
|
||||
if (begins > 0) {
|
||||
String s2 = line.substring(begins + name.length());
|
||||
begins = s2.indexOf("REG_SZ");
|
||||
if (begins > 0) {
|
||||
return s2.substring(begins + "REG_SZ".length()).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (location.equals(line.trim())) {
|
||||
last = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to query register value of " + location, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
@@ -34,9 +35,8 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
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.FileSaver;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
@@ -50,6 +50,7 @@ import java.util.*;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -68,18 +69,6 @@ public final class Accounts {
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to check update for authlib-injector", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
@@ -173,7 +162,7 @@ public final class Accounts {
|
||||
}
|
||||
|
||||
private static void loadGlobalAccountStorages() {
|
||||
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
|
||||
Path globalAccountsFile = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("accounts.json");
|
||||
if (Files.exists(globalAccountsFile)) {
|
||||
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
|
||||
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
|
||||
@@ -182,21 +171,8 @@ public final class Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
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.error("Failed to save global accounts", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalAccountStorages.addListener(onInvalidating(() ->
|
||||
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
|
||||
FileSaver.save(globalAccountsFile, Config.CONFIG_GSON.toJson(globalAccountStorages))));
|
||||
}
|
||||
|
||||
private static Account parseAccount(Map<Object, Object> storage) {
|
||||
@@ -277,6 +253,30 @@ public final class Accounts {
|
||||
selected = accounts.get(0);
|
||||
}
|
||||
|
||||
if (!globalConfig().isEnableOfflineAccount())
|
||||
for (Account account : accounts) {
|
||||
if (account instanceof MicrosoftAccount) {
|
||||
globalConfig().setEnableOfflineAccount(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalConfig().isEnableOfflineAccount())
|
||||
accounts.addListener(new ListChangeListener<Account>() {
|
||||
@Override
|
||||
public void onChanged(Change<? extends Account> change) {
|
||||
while (change.next()) {
|
||||
for (Account account : change.getAddedSubList()) {
|
||||
if (account instanceof MicrosoftAccount) {
|
||||
accounts.removeListener(this);
|
||||
globalConfig().setEnableOfflineAccount(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
selectedAccount.set(selected);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
@@ -326,8 +326,6 @@ public final class Accounts {
|
||||
});
|
||||
}
|
||||
|
||||
triggerAuthlibInjectorUpdateCheck();
|
||||
|
||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||
continue;
|
||||
@@ -335,7 +333,7 @@ public final class Accounts {
|
||||
try {
|
||||
server.fetchMetadataResponse();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
|
||||
LOG.warning("Failed to fetch authlib-injector server metadata: " + server, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -360,24 +358,18 @@ public final class Accounts {
|
||||
// ==== authlib-injector ====
|
||||
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
|
||||
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
|
||||
if (authlibinjectorLocation == null) {
|
||||
return new AuthlibInjectorDownloader(
|
||||
Metadata.HMCL_DIRECTORY.resolve("authlib-injector.jar"),
|
||||
DownloadProviders::getDownloadProvider) {
|
||||
@Override
|
||||
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
|
||||
Optional<AuthlibInjectorArtifactInfo> local = super.getArtifactInfoImmediately();
|
||||
if (local.isPresent()) {
|
||||
return local;
|
||||
}
|
||||
// search authlib-injector.jar in current directory, it's used as a fallback
|
||||
return parseArtifact(Paths.get("authlib-injector.jar"));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (authlibinjectorLocation != null) {
|
||||
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
|
||||
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
|
||||
}
|
||||
|
||||
String authlibInjectorVersion = JarUtils.getManifestAttribute("Authlib-Injector-Version", null);
|
||||
if (authlibInjectorVersion == null)
|
||||
throw new AssertionError("Missing Authlib-Injector-Version");
|
||||
|
||||
String authlibInjectorFileName = "authlib-injector-" + authlibInjectorVersion + ".jar";
|
||||
return new AuthlibInjectorExtractor(Accounts.class.getResource("/assets/" + authlibInjectorFileName),
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve(authlibInjectorFileName));
|
||||
}
|
||||
|
||||
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
|
||||
@@ -463,6 +455,8 @@ public final class Accounts {
|
||||
return i18n("account.methods.microsoft.error.country_unavailable");
|
||||
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
|
||||
return i18n("account.methods.microsoft.error.missing_xbox_account");
|
||||
} else if (errorCode == MicrosoftService.XboxAuthorizationException.BANNED) {
|
||||
return i18n("account.methods.microsoft.error.banned");
|
||||
} else {
|
||||
return i18n("account.methods.microsoft.error.unknown", errorCode);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -73,8 +72,7 @@ public final class AuthlibInjectorServers implements Validation {
|
||||
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
|
||||
AuthlibInjectorServers configInstance;
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
|
||||
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Malformed authlib-injectors.json", e);
|
||||
return;
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
@@ -28,6 +29,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.scene.paint.Paint;
|
||||
import org.hildan.fxgson.creators.ObservableListCreator;
|
||||
import org.hildan.fxgson.creators.ObservableMapCreator;
|
||||
import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
@@ -36,6 +38,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.PaintAdapter;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
@@ -59,7 +62,9 @@ public final class Config implements Observable {
|
||||
.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
|
||||
.registerTypeAdapter(Paint.class, new PaintAdapter())
|
||||
.setPrettyPrinting()
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.create();
|
||||
|
||||
@Nullable
|
||||
@@ -85,6 +90,12 @@ public final class Config implements Observable {
|
||||
@SerializedName("bgurl")
|
||||
private StringProperty backgroundImageUrl = new SimpleStringProperty();
|
||||
|
||||
@SerializedName("bgImageOpacity")
|
||||
private IntegerProperty backgroundImageOpacity = new SimpleIntegerProperty(100);
|
||||
|
||||
@SerializedName("bgpaint")
|
||||
private ObjectProperty<Paint> backgroundPaint = new SimpleObjectProperty<>();
|
||||
|
||||
@SerializedName("commonDirType")
|
||||
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
|
||||
|
||||
@@ -170,7 +181,7 @@ public final class Config implements Observable {
|
||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[]{server});
|
||||
|
||||
@SerializedName("addedLittleSkin")
|
||||
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
|
||||
@@ -272,6 +283,30 @@ public final class Config implements Observable {
|
||||
this.backgroundImageUrl.set(backgroundImageUrl);
|
||||
}
|
||||
|
||||
public Paint getBackgroundPaint() {
|
||||
return backgroundPaint.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Paint> backgroundPaintProperty() {
|
||||
return backgroundPaint;
|
||||
}
|
||||
|
||||
public void setBackgroundPaint(Paint backgroundPaint) {
|
||||
this.backgroundPaint.set(backgroundPaint);
|
||||
}
|
||||
|
||||
public int getBackgroundImageOpacity() {
|
||||
return backgroundImageOpacity.get();
|
||||
}
|
||||
|
||||
public void setBackgroundImageOpacity(int backgroundImageOpacity) {
|
||||
this.backgroundImageOpacity.set(backgroundImageOpacity);
|
||||
}
|
||||
|
||||
public IntegerProperty backgroundImageOpacityProperty() {
|
||||
return backgroundImageOpacity;
|
||||
}
|
||||
|
||||
public EnumCommonDirectory getCommonDirType() {
|
||||
return commonDirType.get();
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
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.FileSaver;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
@@ -39,7 +38,7 @@ public final class ConfigHolder {
|
||||
|
||||
public static final String CONFIG_FILENAME = "hmcl.json";
|
||||
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
|
||||
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("config.json");
|
||||
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("config.json");
|
||||
|
||||
private static Path configLocation;
|
||||
private static Config configInstance;
|
||||
@@ -83,10 +82,10 @@ public final class ConfigHolder {
|
||||
LOG.info("Config location: " + configLocation);
|
||||
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> markConfigDirty());
|
||||
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
|
||||
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> markGlobalConfigDirty());
|
||||
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
|
||||
|
||||
Locale.setDefault(config().getLocalization().getLocale());
|
||||
I18n.setLocale(configInstance.getLocalization());
|
||||
@@ -94,22 +93,15 @@ public final class ConfigHolder {
|
||||
Settings.init();
|
||||
|
||||
if (newlyCreated) {
|
||||
saveConfigSync();
|
||||
|
||||
// hide the config file on windows
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
Files.setAttribute(configLocation, "dos:hidden", true);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to set hidden attribute of " + configLocation, e);
|
||||
}
|
||||
}
|
||||
LOG.info("Creating config file " + configLocation);
|
||||
FileUtils.saveSafely(configLocation, configInstance.toJson());
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
@@ -121,12 +113,14 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
private static Path locateConfig() {
|
||||
Path exePath = Paths.get("").toAbsolutePath();
|
||||
Path defaultConfigFile = Metadata.HMCL_CURRENT_DIRECTORY.resolve(CONFIG_FILENAME);
|
||||
if (Files.isRegularFile(defaultConfigFile))
|
||||
return defaultConfigFile;
|
||||
|
||||
try {
|
||||
Path jarPath = JarUtils.thisJarPath();
|
||||
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
|
||||
jarPath = jarPath.getParent();
|
||||
exePath = jarPath;
|
||||
|
||||
Path config = jarPath.resolve(CONFIG_FILENAME);
|
||||
if (Files.isRegularFile(config))
|
||||
@@ -149,7 +143,7 @@ public final class ConfigHolder {
|
||||
return dotConfig;
|
||||
|
||||
// create new
|
||||
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
|
||||
return defaultConfigFile;
|
||||
}
|
||||
|
||||
private static Config loadConfig() throws IOException {
|
||||
@@ -164,7 +158,7 @@ public final class ConfigHolder {
|
||||
LOG.warning("Failed to get owner");
|
||||
}
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
String content = Files.readString(configLocation);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
@@ -177,40 +171,16 @@ public final class ConfigHolder {
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Creating an empty config");
|
||||
newlyCreated = true;
|
||||
return new Config();
|
||||
}
|
||||
|
||||
private static final InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
try {
|
||||
writeToConfig(content);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save config", e);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToConfig(String content) throws IOException {
|
||||
LOG.info("Saving config");
|
||||
synchronized (configLocation) {
|
||||
FileUtils.saveSafely(configLocation, content);
|
||||
}
|
||||
}
|
||||
|
||||
private static void markConfigDirty() {
|
||||
configWriter.accept(configInstance.toJson());
|
||||
}
|
||||
|
||||
private static void saveConfigSync() throws IOException {
|
||||
writeToConfig(configInstance.toJson());
|
||||
}
|
||||
|
||||
// Global Config
|
||||
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
String content = Files.readString(GLOBAL_CONFIG_PATH);
|
||||
GlobalConfig deserialized = GlobalConfig.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
@@ -226,22 +196,4 @@ public final class ConfigHolder {
|
||||
return new GlobalConfig();
|
||||
}
|
||||
|
||||
private static final InvocationDispatcher<String> globalConfigWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
|
||||
try {
|
||||
writeToGlobalConfig(content);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save config", e);
|
||||
}
|
||||
});
|
||||
|
||||
private static void writeToGlobalConfig(String content) throws IOException {
|
||||
LOG.info("Saving global config");
|
||||
synchronized (GLOBAL_CONFIG_PATH) {
|
||||
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
|
||||
}
|
||||
}
|
||||
|
||||
private static void markGlobalConfigDirty() {
|
||||
globalConfigWriter.accept(globalConfigInstance.toJson());
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
@@ -137,26 +137,26 @@ public final class DownloadProviders {
|
||||
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
return i18n("install.failed.downloading.timeout", url);
|
||||
return i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), url);
|
||||
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
return i18n("download.code.404", uri);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else if (exception.getCause() instanceof SSLHandshakeException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
|
||||
@@ -22,5 +22,6 @@ public enum EnumBackgroundImage {
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
TRANSLUCENT, // Deprecated
|
||||
PAINT
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class FontManager {
|
||||
|
||||
public static final String[] FONT_EXTENSIONS = {
|
||||
"ttf", "otf", "woff"
|
||||
};
|
||||
|
||||
public static final double DEFAULT_FONT_SIZE = 12.0f;
|
||||
|
||||
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
|
||||
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
font = tryLoadDefaultFont(Metadata.CURRENT_DIRECTORY);
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
font = tryLoadDefaultFont(Metadata.HMCL_GLOBAL_DIRECTORY);
|
||||
if (font != null)
|
||||
return font;
|
||||
|
||||
Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar != null && thisJar.getParent() != null) {
|
||||
font = tryLoadDefaultFont(thisJar.getParent());
|
||||
if (font != null)
|
||||
return font;
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
|
||||
&& Locale.getDefault() != Locale.ROOT
|
||||
&& !"en".equals(Locale.getDefault().getLanguage()))
|
||||
return findByFcMatch();
|
||||
else
|
||||
return null;
|
||||
});
|
||||
|
||||
private static final ObjectProperty<FontReference> fontProperty;
|
||||
|
||||
static {
|
||||
String fontFamily = config().getLauncherFontFamily();
|
||||
if (fontFamily == null)
|
||||
fontFamily = System.getProperty("hmcl.font.override");
|
||||
if (fontFamily == null)
|
||||
fontFamily = System.getenv("HMCL_FONT");
|
||||
|
||||
FontReference fontReference;
|
||||
if (fontFamily == null) {
|
||||
Font defaultFont = DEFAULT_FONT.get();
|
||||
fontReference = defaultFont != null ? new FontReference(defaultFont) : null;
|
||||
} else
|
||||
fontReference = new FontReference(fontFamily);
|
||||
|
||||
fontProperty = new SimpleObjectProperty<>(fontReference);
|
||||
|
||||
LOG.info("Font: " + (fontReference != null ? fontReference.getFamily() : "System"));
|
||||
fontProperty.addListener((obs, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
config().setLauncherFontFamily(newValue.getFamily());
|
||||
else
|
||||
config().setLauncherFontFamily(null);
|
||||
});
|
||||
}
|
||||
|
||||
private static Font tryLoadDefaultFont(Path dir) {
|
||||
for (String extension : FONT_EXTENSIONS) {
|
||||
Path path = dir.resolve("font." + extension);
|
||||
if (Files.isRegularFile(path)) {
|
||||
try {
|
||||
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (font != null) {
|
||||
return font;
|
||||
}
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
|
||||
LOG.warning("Failed to load font " + path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Font findByFcMatch() {
|
||||
Path fcMatch = SystemUtils.which("fc-match");
|
||||
if (fcMatch == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
String result = SystemUtils.run(fcMatch.toString(),
|
||||
":lang=" + Locale.getDefault().toLanguageTag(),
|
||||
"--format", "%{family}\\n%{file}").trim();
|
||||
|
||||
String[] results = result.split("\\n");
|
||||
if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) {
|
||||
LOG.warning("Unexpected output from fc-match: " + result);
|
||||
return null;
|
||||
}
|
||||
|
||||
String family = results[0].trim();
|
||||
String path = results[1];
|
||||
|
||||
Path file = Paths.get(path).toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(file)) {
|
||||
LOG.warning("Font file does not exist: " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||
if (fonts == null) {
|
||||
LOG.warning("Failed to load font from " + path);
|
||||
return null;
|
||||
} else if (fonts.length == 0) {
|
||||
LOG.warning("No fonts loaded from " + path);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(family)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
if (family.indexOf(',') >= 0) {
|
||||
for (String candidateFamily : family.split(",")) {
|
||||
for (Font font : fonts) {
|
||||
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
|
||||
return fonts[0];
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get default font with fc-match", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectProperty<FontReference> fontProperty() {
|
||||
return fontProperty;
|
||||
}
|
||||
|
||||
public static FontReference getFont() {
|
||||
return fontProperty.get();
|
||||
}
|
||||
|
||||
public static void setFont(FontReference font) {
|
||||
fontProperty.set(font);
|
||||
}
|
||||
|
||||
public static void setFontFamily(String fontFamily) {
|
||||
setFont(fontFamily != null ? new FontReference(fontFamily) : null);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/4072
|
||||
public static final class FontReference {
|
||||
private final @NotNull String family;
|
||||
private final @Nullable String style;
|
||||
|
||||
public FontReference(@NotNull String family) {
|
||||
this.family = Objects.requireNonNull(family);
|
||||
this.style = null;
|
||||
}
|
||||
|
||||
public FontReference(@NotNull Font font) {
|
||||
this.family = font.getFamily();
|
||||
this.style = font.getStyle();
|
||||
}
|
||||
|
||||
public @NotNull String getFamily() {
|
||||
return family;
|
||||
}
|
||||
|
||||
public @Nullable String getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof FontReference))
|
||||
return false;
|
||||
FontReference that = (FontReference) o;
|
||||
return Objects.equals(family, that.family) && Objects.equals(style, that.style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(family, style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("FontReference[family='%s', style='%s']", family, style);
|
||||
}
|
||||
}
|
||||
|
||||
private FontManager() {
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,7 @@ import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
@@ -53,6 +52,10 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty();
|
||||
|
||||
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
|
||||
|
||||
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
|
||||
|
||||
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
|
||||
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
|
||||
@@ -115,6 +118,30 @@ public final class GlobalConfig implements Observable {
|
||||
this.logRetention.set(logRetention);
|
||||
}
|
||||
|
||||
public boolean isEnableOfflineAccount() {
|
||||
return enableOfflineAccount.get();
|
||||
}
|
||||
|
||||
public BooleanProperty enableOfflineAccountProperty() {
|
||||
return enableOfflineAccount;
|
||||
}
|
||||
|
||||
public void setEnableOfflineAccount(boolean value) {
|
||||
enableOfflineAccount.set(value);
|
||||
}
|
||||
|
||||
public StringProperty fontAntiAliasingProperty() {
|
||||
return fontAntiAliasing;
|
||||
}
|
||||
|
||||
public String getFontAntiAliasing() {
|
||||
return fontAntiAliasing.get();
|
||||
}
|
||||
|
||||
public void setFontAntiAliasing(String value) {
|
||||
this.fontAntiAliasing.set(value);
|
||||
}
|
||||
|
||||
public ObservableSet<String> getUserJava() {
|
||||
return userJava;
|
||||
}
|
||||
@@ -129,7 +156,9 @@ public final class GlobalConfig implements Observable {
|
||||
"platformPromptVersion",
|
||||
"logRetention",
|
||||
"userJava",
|
||||
"disabledJava"
|
||||
"disabledJava",
|
||||
"enableOfflineAccount",
|
||||
"fontAntiAliasing"
|
||||
));
|
||||
|
||||
@Override
|
||||
@@ -142,6 +171,10 @@ public final class GlobalConfig implements Observable {
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
|
||||
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
|
||||
if (src.enableOfflineAccount.get())
|
||||
jsonObject.addProperty("enableOfflineAccount", true);
|
||||
|
||||
if (!src.getUserJava().isEmpty())
|
||||
jsonObject.add("userJava", context.serialize(src.getUserJava()));
|
||||
|
||||
@@ -165,6 +198,8 @@ public final class GlobalConfig implements Observable {
|
||||
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.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
|
||||
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
|
||||
|
||||
JsonElement userJava = obj.get("userJava");
|
||||
if (userJava != null && userJava.isJsonArray()) {
|
||||
|
||||
@@ -17,100 +17,142 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.Authenticator;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.PasswordAuthentication;
|
||||
import java.net.Proxy;
|
||||
import java.net.Proxy.Type;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ProxyManager {
|
||||
private ProxyManager() {
|
||||
|
||||
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
|
||||
private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
|
||||
|
||||
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
|
||||
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
|
||||
|
||||
private static ProxySelector getProxySelector() {
|
||||
if (config().hasProxy()) {
|
||||
Proxy.Type proxyType = config().getProxyType();
|
||||
String host = config().getProxyHost();
|
||||
int port = config().getProxyPort();
|
||||
|
||||
if (proxyType == Proxy.Type.DIRECT || StringUtils.isBlank(host)) {
|
||||
return NO_PROXY;
|
||||
} else if (port < 0 || port > 0xFFFF) {
|
||||
LOG.warning("Illegal proxy port: " + port);
|
||||
return NO_PROXY;
|
||||
} else {
|
||||
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
|
||||
}
|
||||
} else {
|
||||
return ProxyManager.SYSTEM_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectBinding<Proxy> proxyProperty;
|
||||
private static SimpleAuthenticator getAuthenticator() {
|
||||
if (config().hasProxy() && config().hasProxyAuth()) {
|
||||
String username = config().getProxyUser();
|
||||
String password = config().getProxyPass();
|
||||
|
||||
public static Proxy getProxy() {
|
||||
return proxyProperty.get();
|
||||
}
|
||||
|
||||
public static ObservableObjectValue<Proxy> proxyProperty() {
|
||||
return proxyProperty;
|
||||
if (username != null || password != null)
|
||||
return new SimpleAuthenticator(username, password.toCharArray());
|
||||
else
|
||||
return null;
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
static void init() {
|
||||
proxyProperty = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
String host = config().getProxyHost();
|
||||
int port = config().getProxyPort();
|
||||
if (!config().hasProxy() || StringUtils.isBlank(host) || config().getProxyType() == Proxy.Type.DIRECT) {
|
||||
return Proxy.NO_PROXY;
|
||||
} else {
|
||||
if (port < 0 || port > 0xFFFF) {
|
||||
LOG.warning("Illegal proxy port: " + port);
|
||||
return Proxy.NO_PROXY;
|
||||
}
|
||||
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
|
||||
}
|
||||
},
|
||||
config().proxyTypeProperty(),
|
||||
config().proxyHostProperty(),
|
||||
config().proxyPortProperty(),
|
||||
config().hasProxyProperty());
|
||||
|
||||
proxyProperty.addListener(any -> updateSystemProxy());
|
||||
updateSystemProxy();
|
||||
ProxySelector.setDefault(new ProxySelector() {
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
return defaultProxySelector.select(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
defaultProxySelector.connectFailed(uri, sa, ioe);
|
||||
}
|
||||
});
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
if (config().hasProxyAuth()) {
|
||||
String username = config().getProxyUser();
|
||||
String password = config().getProxyPass();
|
||||
if (username != null && password != null) {
|
||||
return new PasswordAuthentication(username, password.toCharArray());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
|
||||
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
|
||||
}
|
||||
});
|
||||
|
||||
defaultProxySelector = getProxySelector();
|
||||
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
|
||||
config().proxyTypeProperty().addListener(updateProxySelector);
|
||||
config().proxyHostProperty().addListener(updateProxySelector);
|
||||
config().proxyPortProperty().addListener(updateProxySelector);
|
||||
config().hasProxyProperty().addListener(updateProxySelector);
|
||||
|
||||
defaultAuthenticator = getAuthenticator();
|
||||
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
|
||||
config().hasProxyProperty().addListener(updateAuthenticator);
|
||||
config().hasProxyAuthProperty().addListener(updateAuthenticator);
|
||||
config().proxyUserProperty().addListener(updateAuthenticator);
|
||||
config().proxyPassProperty().addListener(updateAuthenticator);
|
||||
}
|
||||
|
||||
private static void updateSystemProxy() {
|
||||
Proxy proxy = proxyProperty.get();
|
||||
if (proxy.type() == Proxy.Type.DIRECT) {
|
||||
System.clearProperty("http.proxyHost");
|
||||
System.clearProperty("http.proxyPort");
|
||||
System.clearProperty("https.proxyHost");
|
||||
System.clearProperty("https.proxyPort");
|
||||
System.clearProperty("socksProxyHost");
|
||||
System.clearProperty("socksProxyPort");
|
||||
} else {
|
||||
InetSocketAddress address = (InetSocketAddress) proxy.address();
|
||||
String host = address.getHostString();
|
||||
String port = String.valueOf(address.getPort());
|
||||
if (proxy.type() == Type.HTTP) {
|
||||
System.clearProperty("socksProxyHost");
|
||||
System.clearProperty("socksProxyPort");
|
||||
System.setProperty("http.proxyHost", host);
|
||||
System.setProperty("http.proxyPort", port);
|
||||
System.setProperty("https.proxyHost", host);
|
||||
System.setProperty("https.proxyPort", port);
|
||||
} else if (proxy.type() == Type.SOCKS) {
|
||||
System.clearProperty("http.proxyHost");
|
||||
System.clearProperty("http.proxyPort");
|
||||
System.clearProperty("https.proxyHost");
|
||||
System.clearProperty("https.proxyPort");
|
||||
System.setProperty("socksProxyHost", host);
|
||||
System.setProperty("socksProxyPort", port);
|
||||
private static final class SimpleProxySelector extends ProxySelector {
|
||||
private final List<Proxy> proxies;
|
||||
|
||||
SimpleProxySelector(Proxy proxy) {
|
||||
this(Collections.singletonList(proxy));
|
||||
}
|
||||
|
||||
SimpleProxySelector(List<Proxy> proxies) {
|
||||
this.proxies = proxies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Proxy> select(URI uri) {
|
||||
if (uri == null)
|
||||
throw new IllegalArgumentException("URI can't be null.");
|
||||
|
||||
return proxies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||
if (uri == null || sa == null || ioe == null) {
|
||||
throw new IllegalArgumentException("Arguments can't be null.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleProxySelector" + proxies;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SimpleAuthenticator extends Authenticator {
|
||||
private final String username;
|
||||
private final char[] password;
|
||||
|
||||
private SimpleAuthenticator(String username, char[] password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PasswordAuthentication getPasswordAuthentication() {
|
||||
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
|
||||
}
|
||||
}
|
||||
|
||||
private ProxyManager() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class StyleSheets {
|
||||
private static final int FONT_STYLE_SHEET_INDEX = 0;
|
||||
private static final int THEME_STYLE_SHEET_INDEX = 1;
|
||||
|
||||
private static final ObservableList<String> stylesheets;
|
||||
|
||||
static {
|
||||
String[] array = new String[]{
|
||||
getFontStyleSheet(),
|
||||
getThemeStyleSheet(),
|
||||
"/assets/css/root.css"
|
||||
};
|
||||
stylesheets = FXCollections.observableList(Arrays.asList(array));
|
||||
|
||||
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
|
||||
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
|
||||
}
|
||||
|
||||
private static String toStyleSheetUri(String styleSheet, String fallback) {
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
Files.writeString(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
final String defaultCss = "/assets/css/font.css";
|
||||
final FontManager.FontReference font = FontManager.getFont();
|
||||
|
||||
if (font == null || "System".equals(font.getFamily()))
|
||||
return defaultCss;
|
||||
|
||||
String fontFamily = font.getFamily();
|
||||
String style = font.getStyle();
|
||||
String weight = null;
|
||||
String posture = null;
|
||||
|
||||
if (style != null) {
|
||||
style = style.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (style.contains("thin"))
|
||||
weight = "100";
|
||||
else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light"))
|
||||
weight = "200";
|
||||
else if (style.contains("medium"))
|
||||
weight = "500";
|
||||
else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold"))
|
||||
weight = "600";
|
||||
else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold"))
|
||||
weight = "800";
|
||||
else if (style.contains("black") || style.contains("heavy"))
|
||||
weight = "900";
|
||||
else if (style.contains("light"))
|
||||
weight = "lighter";
|
||||
else if (style.contains("bold"))
|
||||
weight = "bold";
|
||||
|
||||
posture = style.contains("italic") || style.contains("oblique") ? "italic" : null;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(".root {");
|
||||
builder.append("-fx-font-family:\"").append(fontFamily).append("\";");
|
||||
|
||||
if (weight != null)
|
||||
builder.append("-fx-font-weight:").append(weight).append(";");
|
||||
|
||||
if (posture != null)
|
||||
builder.append("-fx-font-style:").append(posture).append(";");
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString(), fontFamily);
|
||||
}
|
||||
|
||||
private static String rgba(Color color, double opacity) {
|
||||
return String.format("rgba(%d, %d, %d, %.1f)",
|
||||
(int) Math.ceil(color.getRed() * 256),
|
||||
(int) Math.ceil(color.getGreen() * 256),
|
||||
(int) Math.ceil(color.getBlue() * 256),
|
||||
opacity);
|
||||
}
|
||||
|
||||
private static String getThemeStyleSheet() {
|
||||
final String blueCss = "/assets/css/blue.css";
|
||||
|
||||
Theme theme = config().getTheme();
|
||||
if (theme == null || theme.getPaint().equals(Theme.BLUE.getPaint()))
|
||||
return blueCss;
|
||||
|
||||
return toStyleSheetUri(".root {" +
|
||||
"-fx-base-color:" + theme.getColor() + ';' +
|
||||
"-fx-base-darker-color: derive(-fx-base-color, -10%);" +
|
||||
"-fx-base-check-color: derive(-fx-base-color, 30%);" +
|
||||
"-fx-rippler-color:" + rgba(theme.getPaint(), 0.3) + ';' +
|
||||
"-fx-base-rippler-color: derive(" + rgba(theme.getPaint(), 0.3) + ", 100%);" +
|
||||
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
|
||||
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
|
||||
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
|
||||
'}', blueCss);
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
Bindings.bindContent(scene.getStylesheets(), stylesheets);
|
||||
}
|
||||
|
||||
private StyleSheets() {
|
||||
}
|
||||
}
|
||||
@@ -23,31 +23,16 @@ import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@JsonAdapter(Theme.TypeAdapter.class)
|
||||
public class Theme {
|
||||
public final class Theme {
|
||||
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
|
||||
public static final Color BLACK = Color.web("#292929");
|
||||
public static final Color[] SUGGESTED_COLORS = new Color[]{
|
||||
@@ -59,42 +44,6 @@ public class Theme {
|
||||
Color.web("#B71C1C") // red
|
||||
};
|
||||
|
||||
private static final Lazy<Font> FONT = new Lazy<>(() -> {
|
||||
String[] fileNames = {"font.ttf", "font.otf", "font.woff"};
|
||||
|
||||
for (String fileName : fileNames) {
|
||||
Path path = Paths.get(fileName).toAbsolutePath();
|
||||
if (Files.isRegularFile(path)) {
|
||||
try {
|
||||
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), 0);
|
||||
if (font != null) {
|
||||
return font;
|
||||
}
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
|
||||
LOG.warning("Failed to load font " + path);
|
||||
}
|
||||
}
|
||||
|
||||
for (String fileName : fileNames) {
|
||||
Path path = Metadata.HMCL_DIRECTORY.resolve(fileName);
|
||||
if (Files.isRegularFile(path)) {
|
||||
try {
|
||||
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), 0);
|
||||
if (font != null) {
|
||||
return font;
|
||||
}
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
|
||||
LOG.warning("Failed to load font " + path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
public static Theme getTheme() {
|
||||
Theme theme = config().getTheme();
|
||||
return theme == null ? BLUE : theme;
|
||||
@@ -118,6 +67,10 @@ public class Theme {
|
||||
return color;
|
||||
}
|
||||
|
||||
public Color getPaint() {
|
||||
return paint;
|
||||
}
|
||||
|
||||
public boolean isCustom() {
|
||||
return name.startsWith("#");
|
||||
}
|
||||
@@ -130,119 +83,6 @@ public class Theme {
|
||||
return isLight() ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
private static String rgba(Color color, double opacity) {
|
||||
return String.format("rgba(%d, %d, %d, %.1f)",
|
||||
(int) Math.ceil(color.getRed() * 256),
|
||||
(int) Math.ceil(color.getGreen() * 256),
|
||||
(int) Math.ceil(color.getBlue() * 256),
|
||||
opacity);
|
||||
}
|
||||
|
||||
private static Pair<String, String> parseFontStyle(String style) {
|
||||
if (style == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
style = style.toLowerCase(Locale.ROOT);
|
||||
|
||||
String weight;
|
||||
String posture;
|
||||
|
||||
if (style.contains("thin")) {
|
||||
weight = "100";
|
||||
} else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light")) {
|
||||
weight = "200";
|
||||
} else if (style.contains("medium")) {
|
||||
weight = "500";
|
||||
} else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold")) {
|
||||
weight = "600";
|
||||
} else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold")) {
|
||||
weight = "800";
|
||||
} else if (style.contains("black") || style.contains("heavy")) {
|
||||
weight = "900";
|
||||
} else if (style.contains("light")) {
|
||||
weight = "lighter";
|
||||
} else if (style.contains("bold")) {
|
||||
weight = "bold";
|
||||
} else {
|
||||
weight = null;
|
||||
}
|
||||
|
||||
if (style.contains("italic") || style.contains("oblique")) {
|
||||
posture = "italic";
|
||||
} else {
|
||||
posture = null;
|
||||
}
|
||||
|
||||
return Pair.pair(weight, posture);
|
||||
}
|
||||
|
||||
public String[] getStylesheets(String overrideFontFamily) {
|
||||
String css = "/assets/css/blue.css";
|
||||
|
||||
String fontFamily = overrideFontFamily == null
|
||||
? System.getProperty("hmcl.font.override", System.getenv("HMCL_FONT"))
|
||||
: overrideFontFamily;
|
||||
|
||||
Pair<String, String> fontStyle = null;
|
||||
if (fontFamily == null) {
|
||||
Font font = FONT.get();
|
||||
if (font != null) {
|
||||
fontFamily = font.getFamily();
|
||||
fontStyle = parseFontStyle(font.getStyle());
|
||||
}
|
||||
}
|
||||
|
||||
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
|
||||
Color textFill = getForegroundColor();
|
||||
|
||||
StringBuilder themeBuilder = new StringBuilder(512);
|
||||
themeBuilder.append(".root {")
|
||||
.append("-fx-base-color:").append(color).append(';')
|
||||
.append("-fx-base-darker-color: derive(-fx-base-color, -10%);")
|
||||
.append("-fx-base-check-color: derive(-fx-base-color, 30%);")
|
||||
.append("-fx-rippler-color:").append(rgba(paint, 0.3)).append(';')
|
||||
.append("-fx-base-rippler-color: derive(").append(rgba(paint, 0.3)).append(", 100%);")
|
||||
.append("-fx-base-disabled-text-fill:").append(rgba(textFill, 0.7)).append(";")
|
||||
.append("-fx-base-text-fill:").append(getColorDisplayName(getForegroundColor())).append(";")
|
||||
.append("-theme-thumb:").append(rgba(paint, 0.7)).append(";");
|
||||
|
||||
if (fontFamily == null)
|
||||
// https://github.com/HMCL-dev/HMCL/pull/3423
|
||||
themeBuilder.append("-fx-font-family: -fx-base-font-family;");
|
||||
else
|
||||
themeBuilder.append("-fx-font-family:\"").append(fontFamily).append("\";");
|
||||
|
||||
if (fontStyle != null) {
|
||||
if (fontStyle.getKey() != null)
|
||||
themeBuilder.append("-fx-font-weight:").append(fontStyle.getKey()).append(";");
|
||||
|
||||
if (fontStyle.getValue() != null)
|
||||
themeBuilder.append("-fx-font-style:").append(fontStyle.getValue()).append(";");
|
||||
}
|
||||
|
||||
themeBuilder.append('}');
|
||||
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
css = "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(themeBuilder.toString().getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
File temp = File.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
FileUtils.writeText(temp, themeBuilder.toString(), Charset.defaultCharset());
|
||||
temp.deleteOnExit();
|
||||
css = temp.toURI().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create theme stylesheet. Fallback to blue theme.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{css, "/assets/css/root.css"};
|
||||
}
|
||||
|
||||
public static Theme custom(String color) {
|
||||
if (!color.startsWith("#"))
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
@@ -29,6 +29,7 @@ public enum VersionIconType {
|
||||
COMMAND("/assets/img/command.png"),
|
||||
OPTIFINE("/assets/img/optifine.png"),
|
||||
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
||||
FABRIC("/assets/img/fabric.png"),
|
||||
FORGE("/assets/img/forge.png"),
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
|
||||
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -39,6 +39,7 @@ import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
@@ -47,7 +48,16 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@JsonAdapter(VersionSetting.Serializer.class)
|
||||
public final class VersionSetting implements Cloneable, Observable {
|
||||
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private static final int SUGGESTED_MEMORY;
|
||||
|
||||
static {
|
||||
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
|
||||
SUGGESTED_MEMORY = totalMemoryMB >= 32768
|
||||
? 8192
|
||||
: Integer.max((int) (Math.round(totalMemoryMB / 4.0 / 128.0) * 128), 256);
|
||||
}
|
||||
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
public VersionSetting() {
|
||||
PropertyUtils.attachListener(this, helper);
|
||||
@@ -219,7 +229,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
permSizeProperty.set(permSize);
|
||||
}
|
||||
|
||||
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
|
||||
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", SUGGESTED_MEMORY);
|
||||
|
||||
public IntegerProperty maxMemoryProperty() {
|
||||
return maxMemoryProperty;
|
||||
@@ -734,7 +744,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
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("maxMemory", src.getMaxMemory() <= 0 ? SUGGESTED_MEMORY : src.getMaxMemory());
|
||||
obj.addProperty("minMemory", src.getMinMemory());
|
||||
obj.addProperty("autoMemory", src.isAutoMemory());
|
||||
obj.addProperty("permSize", src.getPermSize());
|
||||
@@ -801,8 +811,8 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), OperatingSystem.SUGGESTED_MEMORY);
|
||||
if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY;
|
||||
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), SUGGESTED_MEMORY);
|
||||
if (maxMemoryN <= 0) maxMemoryN = SUGGESTED_MEMORY;
|
||||
|
||||
VersionSetting vs = new VersionSetting();
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class Controllers {
|
||||
public static final String JAVA_VERSION_TIP = "javaVersion";
|
||||
|
||||
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
|
||||
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
|
||||
public static final Screen SCREEN = Screen.getPrimary();
|
||||
@@ -173,6 +175,18 @@ public final class Controllers {
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
if (System.getProperty("prism.lcdtext") == null) {
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
LOG.info("Enable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "true");
|
||||
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|
||||
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
|
||||
LOG.info("Disable sub-pixel antialiasing");
|
||||
System.getProperties().put("prism.lcdtext", "false");
|
||||
}
|
||||
}
|
||||
|
||||
Controllers.stage = stage;
|
||||
|
||||
stageSizeChangeListener = o -> {
|
||||
@@ -202,7 +216,9 @@ public final class Controllers {
|
||||
|
||||
if (targetProperty != null
|
||||
&& Controllers.stage != null
|
||||
&& !Controllers.stage.isIconified()) {
|
||||
&& !Controllers.stage.isIconified()
|
||||
&& !Controllers.stage.isFullScreen()
|
||||
&& !Controllers.stage.isMaximized()) {
|
||||
targetProperty.set(sourceProperty.get());
|
||||
}
|
||||
};
|
||||
@@ -265,7 +281,7 @@ public final class Controllers {
|
||||
stage.setMinHeight(MIN_HEIGHT);
|
||||
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
|
||||
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
|
||||
scene.getStylesheets().setAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
StyleSheets.init(scene);
|
||||
|
||||
FXUtils.setIcon(stage);
|
||||
stage.setTitle(Metadata.FULL_TITLE);
|
||||
@@ -293,8 +309,8 @@ public final class Controllers {
|
||||
if (!Architecture.SYSTEM_ARCH.isX86() && globalConfig().getPlatformPromptVersion() < 1) {
|
||||
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.osx_arm64"), null, MessageType.INFO, continueAction);
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
|
||||
@@ -307,7 +323,6 @@ public final class Controllers {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (globalConfig().getAgreementVersion() < 1) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.WeakListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.*;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
import javafx.event.EventType;
|
||||
@@ -41,11 +41,9 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
@@ -54,11 +52,12 @@ import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.glavo.png.PNGType;
|
||||
import org.glavo.png.PNGWriter;
|
||||
import org.glavo.png.javafx.PNGJavaFXUtils;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.image.ImageLoader;
|
||||
import org.jackhuang.hmcl.ui.image.ImageUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
@@ -66,24 +65,23 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.*;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -106,6 +104,11 @@ public final class FXUtils {
|
||||
|
||||
public static final int JAVAFX_MAJOR_VERSION;
|
||||
|
||||
/// @see Platform.Preferences
|
||||
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
|
||||
public static final @Nullable ObservableBooleanValue DARK_MODE;
|
||||
public static final @Nullable Boolean REDUCED_MOTION;
|
||||
|
||||
static {
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
int majorVersion = -1;
|
||||
@@ -116,27 +119,51 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
JAVAFX_MAJOR_VERSION = majorVersion;
|
||||
|
||||
ObservableMap<String, Object> preferences = null;
|
||||
ObservableBooleanValue darkMode = null;
|
||||
Boolean reducedMotion = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 22) {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
Class<?> preferencesClass = Class.forName("javafx.application.Platform$Preferences");
|
||||
@SuppressWarnings("unchecked")
|
||||
var preferences0 = (ObservableMap<String, Object>) lookup.findStatic(Platform.class, "getPreferences", MethodType.methodType(preferencesClass))
|
||||
.invoke();
|
||||
preferences = preferences0;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var colorSchemeProperty =
|
||||
(ReadOnlyObjectProperty<? extends Enum<?>>)
|
||||
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
|
||||
.invoke(preferences);
|
||||
|
||||
darkMode = Bindings.createBooleanBinding(() ->
|
||||
"DARK".equals(colorSchemeProperty.get().name()), colorSchemeProperty);
|
||||
|
||||
if (JAVAFX_MAJOR_VERSION >= 24) {
|
||||
reducedMotion = (boolean)
|
||||
lookup.findVirtual(preferencesClass, "isReducedMotion", MethodType.methodType(boolean.class))
|
||||
.invoke(preferences);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get preferences", e);
|
||||
}
|
||||
}
|
||||
PREFERENCES = preferences;
|
||||
DARK_MODE = darkMode;
|
||||
REDUCED_MOTION = reducedMotion;
|
||||
}
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
|
||||
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp"
|
||||
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
|
||||
);
|
||||
|
||||
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
|
||||
|
||||
public static void shutdown() {
|
||||
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
|
||||
try {
|
||||
Files.deleteIfExists(entry.getValue());
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
|
||||
}
|
||||
remoteImageCache.remove(entry.getKey());
|
||||
}
|
||||
|
||||
builtinImageCache.clear();
|
||||
}
|
||||
|
||||
@@ -329,8 +356,15 @@ public final class FXUtils {
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
|
||||
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
tooltip.setShowDelay(showDelay);
|
||||
tooltip.setShowDuration(showDuration);
|
||||
tooltip.setHideDelay(hideDelay);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
@@ -338,7 +372,7 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
@@ -380,7 +414,7 @@ public final class FXUtils {
|
||||
String openCommand;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && new File("/usr/bin/xdg-open").exists())
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
@@ -411,31 +445,15 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
private static String which(String command) {
|
||||
String path = System.getenv("PATH");
|
||||
if (path == null)
|
||||
return null;
|
||||
|
||||
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
|
||||
try {
|
||||
Path program = Paths.get(item, command);
|
||||
if (Files.isExecutable(program))
|
||||
return program.toRealPath().toString();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
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)
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
openCommands = new String[]{"/usr/bin/open", "-R", path};
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
|
||||
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && SystemUtils.which("dbus-send") != null)
|
||||
openCommands = new String[]{
|
||||
"dbus-send",
|
||||
"--print-reply",
|
||||
@@ -501,10 +519,10 @@ public final class FXUtils {
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
String path = which(browser);
|
||||
Path path = SystemUtils.which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), link});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
@@ -515,7 +533,7 @@ public final class FXUtils {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
@@ -717,6 +735,240 @@ public final class FXUtils {
|
||||
property.removeListener(binding);
|
||||
}
|
||||
|
||||
private static final class PaintBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<ColorPicker> colorPickerRef;
|
||||
private final WeakReference<Property<Paint>> propertyRef;
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private PaintBidirectionalBinding(ColorPicker colorPicker, Property<Paint> property) {
|
||||
this.colorPickerRef = new WeakReference<>(colorPicker);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.hashCode = System.identityHashCode(colorPicker) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable sourceProperty) {
|
||||
if (!updating) {
|
||||
final ColorPicker colorPicker = colorPickerRef.get();
|
||||
final Property<Paint> property = propertyRef.get();
|
||||
|
||||
if (colorPicker == null || property == null) {
|
||||
if (colorPicker != null) {
|
||||
colorPicker.valueProperty().removeListener(this);
|
||||
}
|
||||
|
||||
if (property != null) {
|
||||
property.removeListener(this);
|
||||
}
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
if (property == sourceProperty) {
|
||||
Paint newValue = property.getValue();
|
||||
if (newValue instanceof Color)
|
||||
colorPicker.setValue((Color) newValue);
|
||||
else
|
||||
colorPicker.setValue(null);
|
||||
} else {
|
||||
Paint newValue = colorPicker.getValue();
|
||||
property.setValue(newValue);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return colorPickerRef.get() == null || propertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof FXUtils.PaintBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (FXUtils.PaintBidirectionalBinding) o;
|
||||
|
||||
final ColorPicker colorPicker = this.colorPickerRef.get();
|
||||
final Property<Paint> property = this.propertyRef.get();
|
||||
|
||||
final ColorPicker thatColorPicker = that.colorPickerRef.get();
|
||||
final Property<?> thatProperty = that.propertyRef.get();
|
||||
|
||||
if (colorPicker == null || property == null || thatColorPicker == null || thatProperty == null)
|
||||
return false;
|
||||
|
||||
return colorPicker == thatColorPicker && property == thatProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindPaint(ColorPicker colorPicker, Property<Paint> property) {
|
||||
PaintBidirectionalBinding binding = new PaintBidirectionalBinding(colorPicker, property);
|
||||
|
||||
colorPicker.valueProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
|
||||
if (property.getValue() instanceof Color)
|
||||
colorPicker.setValue((Color) property.getValue());
|
||||
else
|
||||
colorPicker.setValue(null);
|
||||
|
||||
colorPicker.valueProperty().addListener(binding);
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<JFXComboBox<String>> comboBoxRef;
|
||||
private final WeakReference<IntegerProperty> widthPropertyRef;
|
||||
private final WeakReference<IntegerProperty> heightPropertyRef;
|
||||
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
|
||||
IntegerProperty widthProperty,
|
||||
IntegerProperty heightProperty) {
|
||||
this.comboBoxRef = new WeakReference<>(comboBox);
|
||||
this.widthPropertyRef = new WeakReference<>(widthProperty);
|
||||
this.heightPropertyRef = new WeakReference<>(heightProperty);
|
||||
this.hashCode = System.identityHashCode(comboBox)
|
||||
^ System.identityHashCode(widthProperty)
|
||||
^ System.identityHashCode(heightProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (!updating) {
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null) {
|
||||
if (comboBox != null) {
|
||||
comboBox.focusedProperty().removeListener(this);
|
||||
comboBox.sceneProperty().removeListener(this);
|
||||
}
|
||||
if (widthProperty != null)
|
||||
widthProperty.removeListener(this);
|
||||
if (heightProperty != null)
|
||||
heightProperty.removeListener(this);
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
int width = widthProperty.get();
|
||||
int height = heightProperty.get();
|
||||
|
||||
if (observable instanceof ReadOnlyProperty<?>
|
||||
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
|
||||
String value = comboBox.valueProperty().get();
|
||||
if (value == null)
|
||||
value = "";
|
||||
int idx = value.indexOf('x');
|
||||
if (idx < 0)
|
||||
idx = value.indexOf('*');
|
||||
|
||||
if (idx < 0) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
String widthStr = value.substring(0, idx).trim();
|
||||
String heightStr = value.substring(idx + 1).trim();
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
try {
|
||||
newWidth = Integer.parseInt(widthStr);
|
||||
newHeight = Integer.parseInt(heightStr);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
widthProperty.set(newWidth);
|
||||
heightProperty.set(newHeight);
|
||||
} else {
|
||||
comboBox.setValue(width + "x" + height);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return this.comboBoxRef.get() == null
|
||||
|| this.widthPropertyRef.get() == null
|
||||
|| this.heightPropertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof WindowsSizeBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (WindowsSizeBidirectionalBinding) obj;
|
||||
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
var thatComboBox = that.comboBoxRef.get();
|
||||
var thatWidthProperty = that.widthPropertyRef.get();
|
||||
var thatHeightProperty = that.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null
|
||||
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return comboBox == thatComboBox
|
||||
&& widthProperty == thatWidthProperty
|
||||
&& heightProperty == thatHeightProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().addListener(binding);
|
||||
comboBox.sceneProperty().addListener(binding);
|
||||
widthProperty.addListener(binding);
|
||||
heightProperty.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().removeListener(binding);
|
||||
comboBox.sceneProperty().removeListener(binding);
|
||||
widthProperty.removeListener(binding);
|
||||
heightProperty.removeListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
int itemCount = children.length;
|
||||
int childSelectedCount = 0;
|
||||
@@ -776,7 +1028,7 @@ public final class FXUtils {
|
||||
String icon;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
icon = "/assets/img/icon.png";
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
icon = "/assets/img/icon-mac.png";
|
||||
} else {
|
||||
icon = "/assets/img/icon@4x.png";
|
||||
@@ -784,47 +1036,53 @@ public final class FXUtils {
|
||||
stage.getIcons().add(newBuiltinImage(icon));
|
||||
}
|
||||
|
||||
private static Image loadWebPImage(InputStream input) throws IOException {
|
||||
WebPImageReaderSpi spi = new WebPImageReaderSpi();
|
||||
ImageReader reader = spi.createReaderInstance(null);
|
||||
|
||||
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
|
||||
reader.setInput(imageInput, true, true);
|
||||
return SwingFXUtils.toFXImage(reader.read(0, reader.getDefaultReadParam()), null);
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static Image loadImage(Path path) throws Exception {
|
||||
try (InputStream input = Files.newInputStream(path)) {
|
||||
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
|
||||
return loadWebPImage(input);
|
||||
else {
|
||||
Image image = new Image(input);
|
||||
if (image.isError())
|
||||
throw image.getException();
|
||||
return image;
|
||||
return loadImage(path, 0, 0, false, false);
|
||||
}
|
||||
|
||||
public static Image loadImage(Path path,
|
||||
int requestedWidth, int requestedHeight,
|
||||
boolean preserveRatio, boolean smooth) throws Exception {
|
||||
try (var input = new BufferedInputStream(Files.newInputStream(path))) {
|
||||
String ext = FileUtils.getExtension(path).toLowerCase(Locale.ROOT);
|
||||
ImageLoader loader = ImageUtils.EXT_TO_LOADER.get(ext);
|
||||
if (loader == null && !ImageUtils.DEFAULT_EXTS.contains(ext)) {
|
||||
input.mark(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
byte[] headerBuffer = input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
input.reset();
|
||||
loader = ImageUtils.guessLoader(headerBuffer);
|
||||
}
|
||||
if (loader == null)
|
||||
loader = ImageUtils.DEFAULT;
|
||||
return loader.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
}
|
||||
}
|
||||
|
||||
public static Image loadImage(URL url) throws Exception {
|
||||
URLConnection connection = NetworkUtils.createConnection(url);
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
}
|
||||
public static Image loadImage(String url) throws Exception {
|
||||
URI uri = NetworkUtils.toURI(url);
|
||||
|
||||
try (InputStream input = connection.getInputStream()) {
|
||||
String path = url.getPath();
|
||||
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
|
||||
return loadWebPImage(input);
|
||||
else {
|
||||
Image image = new Image(input);
|
||||
if (image.isError())
|
||||
throw image.getException();
|
||||
return image;
|
||||
URLConnection connection = NetworkUtils.createConnection(uri);
|
||||
if (connection instanceof HttpURLConnection)
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
|
||||
try (BufferedInputStream input = new BufferedInputStream(connection.getInputStream())) {
|
||||
String contentType = Objects.requireNonNull(connection.getContentType(), "");
|
||||
Matcher matcher = ImageUtils.CONTENT_TYPE_PATTERN.matcher(contentType);
|
||||
if (matcher.find())
|
||||
contentType = matcher.group("type");
|
||||
|
||||
ImageLoader loader = ImageUtils.CONTENT_TYPE_TO_LOADER.get(contentType);
|
||||
if (loader == null && !ImageUtils.DEFAULT_CONTENT_TYPES.contains(contentType)) {
|
||||
input.mark(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
byte[] headerBuffer = input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
|
||||
input.reset();
|
||||
loader = ImageUtils.guessLoader(headerBuffer);
|
||||
}
|
||||
|
||||
if (loader == null)
|
||||
loader = ImageUtils.DEFAULT;
|
||||
|
||||
return loader.load(input, 0, 0, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,78 +1126,22 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url) {
|
||||
return newRemoteImage(url, 0, 0, false, false, false);
|
||||
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(url)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from the internet. It will cache the data of images for the further usage.
|
||||
* The cached data will be deleted when HMCL is closed or hidden.
|
||||
*
|
||||
* @param url the url of image. The image resource should be a file on the internet.
|
||||
* @param requestedWidth the image's bounding box width
|
||||
* @param requestedHeight the image's bounding box height
|
||||
* @param preserveRatio indicates whether to preserve the aspect ratio of
|
||||
* the original image when scaling to fit the image within the
|
||||
* specified bounding box
|
||||
* @param smooth indicates whether to use a better quality filtering
|
||||
* algorithm or a faster one when scaling this image to fit within
|
||||
* the specified bounding box
|
||||
* @return the image resource within the jar.
|
||||
*/
|
||||
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
|
||||
Path currentPath = remoteImageCache.get(url);
|
||||
if (currentPath != null) {
|
||||
if (Files.isReadable(currentPath)) {
|
||||
try (InputStream inputStream = Files.newInputStream(currentPath)) {
|
||||
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("An exception encountered while reading data from cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// The file is unavailable or unreadable.
|
||||
remoteImageCache.remove(url);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(currentPath);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("An exception encountered while deleting broken cached image file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
|
||||
image.progressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
|
||||
Task.runAsync(() -> {
|
||||
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
|
||||
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
|
||||
OutputStream outputStream = Files.newOutputStream(newPath);
|
||||
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
|
||||
) {
|
||||
writer.write(PNGJavaFXUtils.asArgbImage(image));
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.delete(newPath);
|
||||
} catch (IOException e2) {
|
||||
e2.addSuppressed(e);
|
||||
throw e2;
|
||||
}
|
||||
throw e;
|
||||
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
var image = new SimpleObjectProperty<Image>();
|
||||
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
image.set(result);
|
||||
} else {
|
||||
LOG.warning("An exception encountered while loading remote image: " + url, exception);
|
||||
}
|
||||
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
|
||||
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
})
|
||||
.start();
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,20 +37,14 @@ 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.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -64,7 +58,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -98,7 +92,7 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
|
||||
total_memory = OperatingSystem.TOTAL_MEMORY + " MB";
|
||||
total_memory = MEGABYTES.formatBytes(SystemInfo.getTotalMemorySize());
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
@@ -109,7 +103,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(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
StyleSheets.init(getScene());
|
||||
setTitle(i18n("game.crash.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
@@ -133,7 +127,7 @@ public class GameCrashWindow extends Stage {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
|
||||
return pair(CrashReportAnalyzer.anaylze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
@@ -142,13 +136,13 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
String log;
|
||||
try {
|
||||
log = FileUtils.readText(latestLog);
|
||||
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read logs/latest.log", e);
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
|
||||
return pair(CrashReportAnalyzer.anaylze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
|
||||
return pair(CrashReportAnalyzer.analyze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
|
||||
})).whenComplete(Schedulers.javafx(), (taskResult, exception) -> {
|
||||
loading.set(false);
|
||||
|
||||
@@ -271,7 +265,7 @@ public class GameCrashWindow extends Stage {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.handleAsync((result, exception) -> {
|
||||
@@ -422,7 +416,7 @@ public class GameCrashWindow extends Stage {
|
||||
logButton.setOnAction(e -> showLogWindow());
|
||||
|
||||
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
|
||||
helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
|
||||
helpButton.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
|
||||
@@ -159,28 +160,19 @@ public final class HTMLRenderer {
|
||||
|
||||
private void appendImage(Node node) {
|
||||
String src = node.absUrl("src");
|
||||
URI imageUri = null;
|
||||
try {
|
||||
if (!src.isEmpty())
|
||||
imageUri = URI.create(src);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
String alt = node.attr("alt");
|
||||
|
||||
if (imageUri != null) {
|
||||
URI uri = URI.create(src);
|
||||
|
||||
if (StringUtils.isNotBlank(src)) {
|
||||
String widthAttr = node.attr("width");
|
||||
String heightAttr = node.attr("height");
|
||||
|
||||
double width = 0;
|
||||
double height = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
|
||||
try {
|
||||
width = Double.parseDouble(widthAttr);
|
||||
height = Double.parseDouble(heightAttr);
|
||||
width = (int) Double.parseDouble(widthAttr);
|
||||
height = (int) Double.parseDouble(heightAttr);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
@@ -190,10 +182,12 @@ public final class HTMLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
|
||||
if (image.isError()) {
|
||||
LOG.warning("Failed to load image: " + uri, image.getException());
|
||||
} else {
|
||||
try {
|
||||
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
|
||||
.run();
|
||||
if (image == null)
|
||||
throw new AssertionError("Image loading task returned null");
|
||||
|
||||
ImageView imageView = new ImageView(image);
|
||||
if (hyperlink != null) {
|
||||
URI target = resolveLink(hyperlink);
|
||||
@@ -204,6 +198,8 @@ public final class HTMLRenderer {
|
||||
}
|
||||
children.add(imageView);
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load image: " + src, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,13 @@ public class InstallerItem extends Control {
|
||||
|
||||
for (InstallerItem item : all) {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
item.resolvedStateProperty.bind(item.versionProperty);
|
||||
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
|
||||
InstalledState itemVersion = item.versionProperty.get();
|
||||
if (itemVersion != null) {
|
||||
return itemVersion;
|
||||
}
|
||||
return InstallableState.INSTANCE;
|
||||
}, item.versionProperty));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +368,9 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
return i18n("install.installer.version", s.version);
|
||||
} else if (state instanceof InstallableState) {
|
||||
return i18n("install.installer.not_installed");
|
||||
return control.style == Style.CARD
|
||||
? i18n("install.installer.do_not_install")
|
||||
: i18n("install.installer.not_installed");
|
||||
} else if (state instanceof IncompatibleState) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
|
||||
} else {
|
||||
@@ -383,7 +391,10 @@ public class InstallerItem extends Control {
|
||||
if (control.id.equals(MINECRAFT.getPatchId())) {
|
||||
removeButton.setVisible(false);
|
||||
} else {
|
||||
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
|
||||
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
State state = control.resolvedStateProperty.get();
|
||||
return state instanceof InstalledState && !((InstalledState) state).external;
|
||||
}, control.resolvedStateProperty));
|
||||
}
|
||||
removeButton.managedProperty().bind(removeButton.visibleProperty());
|
||||
removeButton.setOnAction(e -> {
|
||||
|
||||
@@ -38,9 +38,8 @@ import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
@@ -92,7 +91,7 @@ public final class LogWindow extends Stage {
|
||||
this.logs = logs;
|
||||
this.impl = new LogWindowImpl();
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||
StyleSheets.init(getScene());
|
||||
setTitle(i18n("logwindow.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
@@ -186,7 +185,7 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private void onTerminateGame() {
|
||||
LauncherHelper.stopManagedProcesses();
|
||||
LogWindow.this.gameProcess.stop();
|
||||
}
|
||||
|
||||
private void onClear() {
|
||||
@@ -224,7 +223,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
try {
|
||||
if (gameProcess.isRunning()) {
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
|
||||
FXUtils.showFileInExplorer(dumpFile);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@@ -307,8 +306,8 @@ public final class LogWindow extends Stage {
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
});
|
||||
|
||||
listView.setStyle("-fx-font-family: " + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
|
||||
+ "; -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>() {
|
||||
{
|
||||
|
||||
@@ -50,6 +50,7 @@ public enum SVG {
|
||||
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
|
||||
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
|
||||
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
|
||||
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
|
||||
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
|
||||
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
|
||||
@@ -57,12 +58,14 @@ public enum SVG {
|
||||
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
|
||||
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
|
||||
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
|
||||
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
|
||||
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
|
||||
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
|
||||
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
|
||||
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
|
||||
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
|
||||
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
|
||||
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
|
||||
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
|
||||
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
|
||||
@@ -72,9 +75,11 @@ public enum SVG {
|
||||
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
|
||||
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
|
||||
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
|
||||
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
|
||||
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
|
||||
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
|
||||
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
|
||||
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
|
||||
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
|
||||
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
|
||||
MINIMIZE("M6 21V19H18V21H6Z"),
|
||||
@@ -91,6 +96,7 @@ public enum SVG {
|
||||
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
|
||||
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
|
||||
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
|
||||
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
|
||||
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
|
||||
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
|
||||
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
|
||||
@@ -102,6 +108,8 @@ public enum SVG {
|
||||
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
|
||||
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
|
||||
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
|
||||
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
|
||||
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
|
||||
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
|
||||
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
|
||||
;
|
||||
@@ -125,11 +133,10 @@ public enum SVG {
|
||||
return pane;
|
||||
}
|
||||
|
||||
Group svg = new Group(path);
|
||||
double scale = size / 24;
|
||||
svg.setScaleX(scale);
|
||||
svg.setScaleY(scale);
|
||||
return svg;
|
||||
path.setScaleX(scale);
|
||||
path.setScaleY(scale);
|
||||
return new Group(path);
|
||||
}
|
||||
|
||||
public Node createIcon(ObservableValue<? extends Paint> fill, double size) {
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public class TooltipInstaller {
|
||||
public static final TooltipInstaller INSTALLER;
|
||||
|
||||
static {
|
||||
TooltipInstaller installer = null;
|
||||
|
||||
try {
|
||||
installer = new NewInstaller();
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
installer = new OldInstaller();
|
||||
} catch (Throwable e2) {
|
||||
e2.addSuppressed(e);
|
||||
LOG.warning("Failed to initialize TooltipInstaller", e2);
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLER = installer != null ? installer : new TooltipInstaller();
|
||||
}
|
||||
|
||||
TooltipInstaller() {
|
||||
}
|
||||
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
// For Java 8
|
||||
private static final class OldInstaller extends TooltipInstaller {
|
||||
private static final Constructor<?> createTooltipBehavior;
|
||||
private static final Method installTooltipBehavior;
|
||||
|
||||
static {
|
||||
try {
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
createTooltipBehavior.setAccessible(true);
|
||||
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installTooltipBehavior.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
|
||||
installTooltipBehavior.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For Java 9+
|
||||
private static final class NewInstaller extends TooltipInstaller {
|
||||
private static final MethodHandle setTooltipShowDelay;
|
||||
private static final MethodHandle setTooltipShowDuration;
|
||||
private static final MethodHandle setTooltipHideDelay;
|
||||
|
||||
static {
|
||||
try {
|
||||
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
MethodType methodType = MethodType.methodType(void.class, Duration.class);
|
||||
|
||||
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
|
||||
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
|
||||
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
|
||||
} catch (Exception e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
try {
|
||||
setTooltipShowDelay.invokeExact(tooltip, showDelay);
|
||||
setTooltipShowDuration.invokeExact(tooltip, showDuration);
|
||||
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set tooltip show delay", e);
|
||||
}
|
||||
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
Document document = Jsoup.parse(new URL(url), 30 * 1000);
|
||||
Node node = document.selectFirst("#nowchange");
|
||||
if (node == null)
|
||||
if (node == null || !"h1".equals(node.nodeName()))
|
||||
throw new IOException("Cannot find #nowchange in document");
|
||||
|
||||
HTMLRenderer renderer = new HTMLRenderer(uri -> {
|
||||
@@ -60,6 +60,9 @@ public final class UpgradeDialog extends JFXDialogLayout {
|
||||
});
|
||||
|
||||
do {
|
||||
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
|
||||
break;
|
||||
}
|
||||
renderer.appendNode(node);
|
||||
node = node.nextSibling();
|
||||
} while (node != null);
|
||||
|
||||
@@ -33,6 +33,7 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -128,6 +129,10 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
|
||||
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
btnRefresh.setDisable(true);
|
||||
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
|
||||
}
|
||||
btnRefresh.setOnAction(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
skinnable.refreshAsync()
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -40,18 +41,72 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
|
||||
public class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
|
||||
|
||||
private static boolean isExemptedRegion() {
|
||||
String zoneId = ZoneId.systemDefault().getId();
|
||||
if (Arrays.asList(
|
||||
"Asia/Shanghai",
|
||||
// Although Asia/Beijing is not a legal name, Deepin uses it
|
||||
"Asia/Beijing",
|
||||
"Asia/Chongqing",
|
||||
"Asia/Chungking",
|
||||
"Asia/Harbin"
|
||||
).contains(zoneId))
|
||||
return true;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && NativeUtils.USE_JNA) {
|
||||
Kernel32 kernel32 = Kernel32.INSTANCE;
|
||||
|
||||
// https://learn.microsoft.com/windows/win32/intl/table-of-geographical-locations
|
||||
if (kernel32 != null && kernel32.GetUserGeoID(WinConstants.GEOCLASS_NATION) == 45) // China
|
||||
return true;
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && "GMT+08:00".equals(zoneId))
|
||||
// Some Linux distributions may use invalid time zone ids (e.g., Asia/Beijing)
|
||||
// Java may not be able to resolve this name and use GMT+08:00 instead.
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static {
|
||||
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
|
||||
|
||||
if ("false".equals(property)
|
||||
|| "auto".equals(property) && isExemptedRegion()
|
||||
|| globalConfig().isEnableOfflineAccount())
|
||||
RESTRICTED.set(false);
|
||||
else
|
||||
globalConfig().enableOfflineAccountProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) {
|
||||
if (newValue) {
|
||||
globalConfig().enableOfflineAccountProperty().removeListener(this);
|
||||
RESTRICTED.set(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final ObservableList<AccountListItem> items;
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("account.manage")));
|
||||
private final ListProperty<Account> accounts = new SimpleListProperty<>(this, "accounts", FXCollections.observableArrayList());
|
||||
@@ -88,6 +143,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
private static class AccountListPageSkin extends DecoratorAnimatedPageSkin<AccountListPage> {
|
||||
|
||||
private final ObservableList<AdvancedListItem> authServerItems;
|
||||
private ChangeListener<Boolean> holder;
|
||||
|
||||
public AccountListPageSkin(AccountListPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -96,24 +152,21 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
VBox boxMethods = new VBox();
|
||||
{
|
||||
boxMethods.getStyleClass().add("advanced-list-box-content");
|
||||
boxMethods.getChildren().add(new ClassTitle(i18n("account.create").toUpperCase(Locale.ROOT)));
|
||||
FXUtils.setLimitWidth(boxMethods, 200);
|
||||
|
||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
boxMethods.getChildren().add(offlineItem);
|
||||
|
||||
AdvancedListItem microsoftItem = new AdvancedListItem();
|
||||
microsoftItem.getStyleClass().add("navigation-drawer-item");
|
||||
microsoftItem.setActionButtonVisible(false);
|
||||
microsoftItem.setTitle(i18n("account.methods.microsoft"));
|
||||
microsoftItem.setLeftGraphic(wrap(SVG.MICROSOFT));
|
||||
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
|
||||
boxMethods.getChildren().add(microsoftItem);
|
||||
|
||||
AdvancedListItem offlineItem = new AdvancedListItem();
|
||||
offlineItem.getStyleClass().add("navigation-drawer-item");
|
||||
offlineItem.setActionButtonVisible(false);
|
||||
offlineItem.setTitle(i18n("account.methods.offline"));
|
||||
offlineItem.setLeftGraphic(wrap(SVG.PERSON));
|
||||
offlineItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_OFFLINE)));
|
||||
|
||||
VBox boxAuthServers = new VBox();
|
||||
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
|
||||
@@ -137,7 +190,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
item.titleProperty().bind(title);
|
||||
String host = "";
|
||||
try {
|
||||
host = URI.create(server.getUrl()).getHost();
|
||||
host = NetworkUtils.toURI(server.getUrl()).getHost();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
|
||||
}
|
||||
@@ -149,7 +202,29 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
return item;
|
||||
});
|
||||
Bindings.bindContent(boxAuthServers.getChildren(), authServerItems);
|
||||
boxMethods.getChildren().add(boxAuthServers);
|
||||
|
||||
ClassTitle title = new ClassTitle(i18n("account.create").toUpperCase(Locale.ROOT));
|
||||
if (RESTRICTED.get()) {
|
||||
VBox wrapper = new VBox(offlineItem, boxAuthServers);
|
||||
wrapper.setPadding(Insets.EMPTY);
|
||||
FXUtils.installFastTooltip(wrapper, i18n("account.login.restricted"));
|
||||
|
||||
offlineItem.setDisable(true);
|
||||
boxAuthServers.setDisable(true);
|
||||
|
||||
boxMethods.getChildren().setAll(title, microsoftItem, wrapper);
|
||||
|
||||
holder = FXUtils.onWeakChange(RESTRICTED, value -> {
|
||||
if (!value) {
|
||||
holder = null;
|
||||
offlineItem.setDisable(false);
|
||||
boxAuthServers.setDisable(false);
|
||||
boxMethods.getChildren().setAll(title, microsoftItem, offlineItem, boxAuthServers);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
boxMethods.getChildren().setAll(title, microsoftItem, offlineItem, boxAuthServers);
|
||||
}
|
||||
}
|
||||
|
||||
AdvancedListItem addAuthServerItem = new AdvancedListItem();
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
@@ -196,7 +195,8 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
lblServerName.setText(serverBeingAdded.getName());
|
||||
lblServerUrl.setText(serverBeingAdded.getUrl());
|
||||
|
||||
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
|
||||
//noinspection HttpUrlsUsage
|
||||
lblServerWarning.setVisible(serverBeingAdded.getUrl().startsWith("http://"));
|
||||
|
||||
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -35,6 +38,9 @@ import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
|
||||
@@ -106,12 +112,17 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
|
||||
public CreateAccountPane(AccountFactory<?> factory) {
|
||||
if (factory == null) {
|
||||
showMethodSwitcher = true;
|
||||
String preferred = config().getPreferredLoginType();
|
||||
try {
|
||||
factory = Accounts.getAccountFactory(preferred);
|
||||
} catch (IllegalArgumentException e) {
|
||||
factory = Accounts.FACTORY_OFFLINE;
|
||||
if (AccountListPage.RESTRICTED.get()) {
|
||||
showMethodSwitcher = false;
|
||||
factory = Accounts.FACTORY_MICROSOFT;
|
||||
} else {
|
||||
showMethodSwitcher = true;
|
||||
String preferred = config().getPreferredLoginType();
|
||||
try {
|
||||
factory = Accounts.getAccountFactory(preferred);
|
||||
} catch (IllegalArgumentException e) {
|
||||
factory = Accounts.FACTORY_OFFLINE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showMethodSwitcher = false;
|
||||
@@ -256,17 +267,34 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}).executor(true);
|
||||
};
|
||||
|
||||
if (factory instanceof OfflineAccountFactory && username != null && !USERNAME_CHECKER_PATTERN.matcher(username).matches()) {
|
||||
Controllers.confirm(
|
||||
if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) {
|
||||
JFXButton btnYes = new JFXButton(i18n("button.ok"));
|
||||
btnYes.getStyleClass().add("dialog-error");
|
||||
btnYes.setOnAction(e -> doCreate.run());
|
||||
btnYes.setDisable(true);
|
||||
|
||||
int countdown = 10;
|
||||
KeyFrame[] keyFrames = new KeyFrame[countdown + 1];
|
||||
for (int i = 0; i < countdown; i++) {
|
||||
keyFrames[i] = new KeyFrame(Duration.seconds(i),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok.countdown", countdown - i)));
|
||||
}
|
||||
keyFrames[countdown] = new KeyFrame(Duration.seconds(countdown),
|
||||
new KeyValue(btnYes.textProperty(), i18n("button.ok")),
|
||||
new KeyValue(btnYes.disableProperty(), false));
|
||||
|
||||
Timeline timeline = new Timeline(keyFrames);
|
||||
Controllers.confirmAction(
|
||||
i18n("account.methods.offline.name.invalid"), i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
doCreate,
|
||||
btnYes,
|
||||
() -> {
|
||||
lblErrorMessage.setText(i18n("account.methods.offline.name.invalid.tip"));
|
||||
timeline.stop();
|
||||
body.setDisable(false);
|
||||
spinner.hideSpinner();
|
||||
}
|
||||
);
|
||||
timeline.play();
|
||||
} else {
|
||||
doCreate.run();
|
||||
}
|
||||
@@ -320,7 +348,9 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
forgotpasswordLink.setExternalLink("https://account.live.com/ResetPassword.aspx");
|
||||
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
|
||||
createProfileLink.setExternalLink("https://www.minecraft.net/msaprofile/mygames/editprofile");
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
|
||||
JFXHyperlink bannedQueryLink = new JFXHyperlink(i18n("account.methods.ban_query"));
|
||||
bannedQueryLink.setExternalLink("https://enforcement.xbox.com/enforcement/showenforcementhistory");
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink, bannedQueryLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
@@ -337,7 +367,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
|
||||
JFXHyperlink officialWebsite = new JFXHyperlink(i18n("account.methods.microsoft.snapshot.website"));
|
||||
officialWebsite.setExternalLink("https://hmcl.huangyuhui.net");
|
||||
officialWebsite.setExternalLink(Metadata.PUBLISH_URL);
|
||||
|
||||
vbox.getChildren().setAll(hintPane, officialWebsite);
|
||||
btnAccept.setDisable(true);
|
||||
|
||||
@@ -154,7 +154,7 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
result.getCape() != null ? result.getCape().getImage() : null);
|
||||
}
|
||||
}).start();
|
||||
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
|
||||
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), modelCombobox.valueProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
|
||||
|
||||
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
|
||||
GridPane gridPane = new GridPane();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.animation;
|
||||
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
/**
|
||||
@@ -36,7 +37,7 @@ public final class AnimationUtils {
|
||||
public static void init() {
|
||||
}
|
||||
|
||||
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
|
||||
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled() && FXUtils.REDUCED_MOTION != Boolean.TRUE;
|
||||
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
|
||||
|
||||
public static boolean isAnimationEnabled() {
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.jfoenix.controls.JFXListCell;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
public class FontComboBox extends JFXComboBox<String> {
|
||||
public final class FontComboBox extends JFXComboBox<String> {
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ public final class MessageDialogPane extends HBox {
|
||||
content.getChildren().setAll(textFlow);
|
||||
else {
|
||||
ScrollPane scrollPane = new ScrollPane(textFlow);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
scrollPane.setPrefHeight(350);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
@@ -160,6 +161,13 @@ public final class MessageDialogPane extends HBox {
|
||||
this.dialog = new MessageDialogPane(text, title, type);
|
||||
}
|
||||
|
||||
public Builder addHyperLink(String text, String externalLink) {
|
||||
JFXHyperlink link = new JFXHyperlink(text);
|
||||
link.setExternalLink(externalLink);
|
||||
dialog.actions.getChildren().add(link);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addAction(Node actionNode) {
|
||||
dialog.addButton(actionNode);
|
||||
actionNode.getStyleClass().add("dialog-accept");
|
||||
|
||||
@@ -24,11 +24,13 @@ import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -297,4 +299,35 @@ public final class MultiFileItem<T> extends VBox {
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PaintOption<T> extends Option<T> {
|
||||
private final ColorPicker colorPicker = new ColorPicker();
|
||||
|
||||
public PaintOption(String title, T data) {
|
||||
super(title, data);
|
||||
}
|
||||
|
||||
public PaintOption<T> bindBidirectional(Property<Paint> property) {
|
||||
FXUtils.bindPaint(colorPicker, property);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node createItem(ToggleGroup group) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(3));
|
||||
FXUtils.setLimitHeight(pane, 30);
|
||||
|
||||
left.setText(title);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setToggleGroup(group);
|
||||
left.setUserData(data);
|
||||
pane.setLeft(left);
|
||||
|
||||
colorPicker.disableProperty().bind(left.selectedProperty().not());
|
||||
BorderPane.setAlignment(colorPicker, Pos.CENTER_RIGHT);
|
||||
pane.setRight(colorPicker);
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ public class PromptDialogPane extends DialogPane {
|
||||
public PromptDialogPane(Builder builder) {
|
||||
this.builder = builder;
|
||||
setTitle(builder.title);
|
||||
setPrefWidth(560);
|
||||
|
||||
GridPane body = new GridPane();
|
||||
body.setVgap(8);
|
||||
@@ -72,13 +73,13 @@ public class PromptDialogPane extends DialogPane {
|
||||
if (StringUtils.isNotBlank(question.question.get())) {
|
||||
body.addRow(rowIndex++, new Label(question.question.get()), textField);
|
||||
} else {
|
||||
GridPane.setColumnSpan(textField, 2);
|
||||
GridPane.setColumnSpan(textField, 1);
|
||||
body.addRow(rowIndex++, textField);
|
||||
}
|
||||
GridPane.setMargin(textField, new Insets(0, 0, 20, 0));
|
||||
} else if (question instanceof Builder.BooleanQuestion) {
|
||||
HBox hBox = new HBox();
|
||||
GridPane.setColumnSpan(hBox, 2);
|
||||
GridPane.setColumnSpan(hBox, 1);
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
hBox.getChildren().setAll(checkBox);
|
||||
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
|
||||
@@ -95,12 +96,12 @@ public class PromptDialogPane extends DialogPane {
|
||||
if (StringUtils.isNotBlank(question.question.get())) {
|
||||
body.addRow(rowIndex++, new Label(question.question.get()), comboBox);
|
||||
} else {
|
||||
GridPane.setColumnSpan(comboBox, 2);
|
||||
GridPane.setColumnSpan(comboBox, 1);
|
||||
body.addRow(rowIndex++, comboBox);
|
||||
}
|
||||
} else if (question instanceof Builder.HintQuestion) {
|
||||
HintPane pane = new HintPane();
|
||||
GridPane.setColumnSpan(pane, 2);
|
||||
GridPane.setColumnSpan(pane, 1);
|
||||
pane.textProperty().bind(question.question);
|
||||
body.addRow(rowIndex++, pane);
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2022 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.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ServerAddressValidator extends ValidatorBase {
|
||||
private final boolean nullable;
|
||||
|
||||
public ServerAddressValidator() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public ServerAddressValidator(@NamedArg("nullable") boolean nullable) {
|
||||
this(i18n("input.url"), nullable);
|
||||
}
|
||||
|
||||
public ServerAddressValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
|
||||
super(message);
|
||||
this.nullable = nullable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void eval() {
|
||||
if (srcControl.get() instanceof TextInputControl) {
|
||||
evalTextInputField();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("[-a-zA-Z0-9@:%._+~#=]{1,256}(:\\d+)?");
|
||||
|
||||
private void evalTextInputField() {
|
||||
TextInputControl textField = ((TextInputControl) srcControl.get());
|
||||
|
||||
if (StringUtils.isBlank(textField.getText()))
|
||||
hasErrors.set(!nullable);
|
||||
else
|
||||
hasErrors.set(!PATTERN.matcher(textField.getText()).matches());
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,7 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -43,7 +41,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class TaskExecutorDialogPane extends BorderPane {
|
||||
private TaskExecutor executor;
|
||||
private TaskCancellationAction onCancel;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
private final Label lblTitle;
|
||||
private final Label lblProgress;
|
||||
@@ -98,19 +96,17 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
double speed = speedEvent.getSpeed();
|
||||
if (speed > 1024) {
|
||||
speed /= 1024;
|
||||
unit = "KB/s";
|
||||
unit = "KiB/s";
|
||||
}
|
||||
if (speed > 1024) {
|
||||
speed /= 1024;
|
||||
unit = "MB/s";
|
||||
unit = "MiB/s";
|
||||
}
|
||||
double finalSpeed = speed;
|
||||
String finalUnit = unit;
|
||||
Platform.runLater(() -> {
|
||||
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
||||
});
|
||||
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
|
||||
};
|
||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
}
|
||||
|
||||
@@ -147,15 +147,15 @@ public final class TaskListPane extends StackPane {
|
||||
} else if (task instanceof ModpackUpdateTask) {
|
||||
task.setName(i18n("modpack.update"));
|
||||
} else if (task instanceof CurseInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.curse")));
|
||||
} else if (task instanceof MultiMCModpackInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.multimc")));
|
||||
} else if (task instanceof ModrinthInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.modrinth")));
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.modrinth")));
|
||||
} else if (task instanceof ServerModpackLocalInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.server")));
|
||||
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
@@ -163,7 +163,7 @@ public final class TaskListPane extends StackPane {
|
||||
} else if (task instanceof MojangJavaDownloadTask) {
|
||||
task.setName(i18n("download.java"));
|
||||
} else if (task instanceof JavaInstallTask) {
|
||||
task.setName(i18n("java.install"));
|
||||
task.setName(i18n("java.installing"));
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
|
||||
@@ -21,10 +21,7 @@ import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -58,9 +55,9 @@ public class URLValidator extends ValidatorBase {
|
||||
hasErrors.set(!nullable);
|
||||
else {
|
||||
try {
|
||||
new URL(textField.getText()).toURI();
|
||||
NetworkUtils.toURI(textField.getText());
|
||||
hasErrors.set(false);
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
hasErrors.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,18 +31,21 @@ import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
|
||||
@@ -54,26 +57,22 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DecoratorController {
|
||||
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
|
||||
@@ -123,18 +122,13 @@ public class DecoratorController {
|
||||
|
||||
// Setup background
|
||||
decorator.setContentBackground(getBackground());
|
||||
changeBackgroundListener = o -> {
|
||||
final int currentCount = ++this.changeBackgroundCount;
|
||||
CompletableFuture.supplyAsync(this::getBackground, Schedulers.io())
|
||||
.thenAcceptAsync(background -> {
|
||||
if (this.changeBackgroundCount == currentCount)
|
||||
decorator.setContentBackground(background);
|
||||
}, Schedulers.javafx());
|
||||
};
|
||||
changeBackgroundListener = o -> updateBackground();
|
||||
WeakInvalidationListener weakListener = new WeakInvalidationListener(changeBackgroundListener);
|
||||
config().backgroundImageTypeProperty().addListener(weakListener);
|
||||
config().backgroundImageProperty().addListener(weakListener);
|
||||
config().backgroundImageUrlProperty().addListener(weakListener);
|
||||
config().backgroundPaintProperty().addListener(weakListener);
|
||||
config().backgroundImageOpacityProperty().addListener(weakListener);
|
||||
|
||||
// pass key events to current dialog / current page
|
||||
decorator.addEventFilter(KeyEvent.ANY, e -> {
|
||||
@@ -170,6 +164,14 @@ public class DecoratorController {
|
||||
// press ESC to go back
|
||||
onEscPressed(navigator, this::back);
|
||||
|
||||
// press F11 to toggle full screen
|
||||
navigator.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.F11) {
|
||||
stage.setFullScreen(!stage.isFullScreen());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// For JavaFX 12+
|
||||
MouseButton button = MouseButton.valueOf("BACK");
|
||||
@@ -195,6 +197,20 @@ public class DecoratorController {
|
||||
@SuppressWarnings("FieldCanBeLocal") // Strong reference
|
||||
private final InvalidationListener changeBackgroundListener;
|
||||
|
||||
private void updateBackground() {
|
||||
final int currentCount = ++this.changeBackgroundCount;
|
||||
Task.supplyAsync(Schedulers.io(), this::getBackground)
|
||||
.setName("Update background")
|
||||
.whenComplete(Schedulers.javafx(), (background, exception) -> {
|
||||
if (exception == null) {
|
||||
if (this.changeBackgroundCount == currentCount)
|
||||
decorator.setContentBackground(background);
|
||||
} else {
|
||||
LOG.warning("Failed to update background", exception);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private Background getBackground() {
|
||||
EnumBackgroundImage imageType = config().getBackgroundImageType();
|
||||
|
||||
@@ -203,13 +219,17 @@ public class DecoratorController {
|
||||
case CUSTOM:
|
||||
String backgroundImage = config().getBackgroundImage();
|
||||
if (backgroundImage != null)
|
||||
image = tryLoadImage(Paths.get(backgroundImage));
|
||||
try {
|
||||
image = tryLoadImage(Paths.get(backgroundImage));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Couldn't load background image", e);
|
||||
}
|
||||
break;
|
||||
case NETWORK:
|
||||
String backgroundImageUrl = config().getBackgroundImageUrl();
|
||||
if (backgroundImageUrl != null) {
|
||||
try {
|
||||
image = FXUtils.loadImage(new URL(backgroundImageUrl));
|
||||
image = FXUtils.loadImage(backgroundImageUrl);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Couldn't load background image", e);
|
||||
}
|
||||
@@ -218,25 +238,82 @@ public class DecoratorController {
|
||||
case CLASSIC:
|
||||
image = newBuiltinImage("/assets/img/background-classic.jpg");
|
||||
break;
|
||||
case TRANSLUCENT:
|
||||
case TRANSLUCENT: // Deprecated
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
case PAINT:
|
||||
Paint paint = config().getBackgroundPaint();
|
||||
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
|
||||
if (paint instanceof Color || paint == null) {
|
||||
Color color = (Color) paint;
|
||||
if (color == null)
|
||||
color = Color.WHITE; // Default to white if no color is set
|
||||
if (opacity < 1.)
|
||||
color = new Color(color.getRed(), color.getGreen(), color.getBlue(), opacity);
|
||||
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else {
|
||||
// TODO: Support opacity for non-color paints
|
||||
return new Background(new BackgroundFill(paint, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
}
|
||||
if (image == null) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
}
|
||||
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
|
||||
return createBackgroundWithOpacity(image, config().getBackgroundImageOpacity());
|
||||
}
|
||||
|
||||
private Background createBackgroundWithOpacity(Image image, int opacity) {
|
||||
if (opacity <= 0) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
} else if (opacity >= 100) {
|
||||
return new Background(new BackgroundImage(
|
||||
image,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
} else {
|
||||
WritableImage tempImage = new WritableImage((int) image.getWidth(), (int) image.getHeight());
|
||||
PixelReader pixelReader = image.getPixelReader();
|
||||
PixelWriter pixelWriter = tempImage.getPixelWriter();
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
Color color = pixelReader.getColor(x, y);
|
||||
Color newColor = new Color(color.getRed(), color.getGreen(), color.getBlue(), color.getOpacity() * opacity / 100);
|
||||
pixelWriter.setColor(x, y, newColor);
|
||||
}
|
||||
}
|
||||
|
||||
return new Background(new BackgroundImage(
|
||||
tempImage,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
new BackgroundSize(800, 480, false, false, true, true)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load background image from bg/, background.png, background.jpg, background.gif
|
||||
*/
|
||||
private Image loadDefaultBackgroundImage() {
|
||||
Image image = randomImageIn(Paths.get("bg"));
|
||||
Image image = randomImageIn(Metadata.HMCL_CURRENT_DIRECTORY.resolve("background"));
|
||||
if (image != null)
|
||||
return image;
|
||||
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
image = tryLoadImage(Paths.get("background." + extension));
|
||||
image = tryLoadImage(Metadata.HMCL_CURRENT_DIRECTORY.resolve("background." + extension));
|
||||
if (image != null)
|
||||
return image;
|
||||
}
|
||||
|
||||
image = randomImageIn(Metadata.CURRENT_DIRECTORY.resolve("bg"));
|
||||
if (image != null)
|
||||
return image;
|
||||
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
image = tryLoadImage(Metadata.CURRENT_DIRECTORY.resolve("background." + extension));
|
||||
if (image != null)
|
||||
return image;
|
||||
}
|
||||
@@ -296,8 +373,9 @@ public class DecoratorController {
|
||||
if (navigator.getCurrentPage() instanceof DecoratorPage) {
|
||||
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
|
||||
|
||||
// FIXME: Get WorldPage working first, and revisit this later
|
||||
page.closePage();
|
||||
if (page.isPageCloseable()) {
|
||||
page.closePage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
package org.jackhuang.hmcl.ui.decorator;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -29,11 +32,14 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.effect.BlurType;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -48,6 +54,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
|
||||
private final InvalidationListener onWindowsStatusChange;
|
||||
|
||||
private double mouseInitX, mouseInitY, stageInitX, stageInitY, stageInitWidth, stageInitHeight;
|
||||
|
||||
/**
|
||||
@@ -78,9 +86,25 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
skinnable.getSnackbar().registerSnackbarContainer(parent);
|
||||
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, this::onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, this::onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
|
||||
EventHandler<MouseEvent> onMouseReleased = this::onMouseReleased;
|
||||
EventHandler<MouseEvent> onMouseDragged = this::onMouseDragged;
|
||||
EventHandler<MouseEvent> onMouseMoved = this::onMouseMoved;
|
||||
onWindowsStatusChange = observable -> {
|
||||
if (primaryStage.isIconified() || primaryStage.isFullScreen() || primaryStage.isMaximized()) {
|
||||
root.removeEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.removeEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
} else {
|
||||
root.addEventFilter(MouseEvent.MOUSE_RELEASED, onMouseReleased);
|
||||
root.addEventFilter(MouseEvent.MOUSE_DRAGGED, onMouseDragged);
|
||||
root.addEventFilter(MouseEvent.MOUSE_MOVED, onMouseMoved);
|
||||
}
|
||||
};
|
||||
WeakInvalidationListener weakOnWindowsStatusChange = new WeakInvalidationListener(onWindowsStatusChange);
|
||||
primaryStage.iconifiedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.maximizedProperty().addListener(weakOnWindowsStatusChange);
|
||||
primaryStage.fullScreenProperty().addListener(weakOnWindowsStatusChange);
|
||||
onWindowsStatusChange.invalidated(null);
|
||||
|
||||
shadowContainer.getChildren().setAll(parent);
|
||||
root.getChildren().setAll(shadowContainer);
|
||||
@@ -153,6 +177,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
BorderPane titleBar = new BorderPane();
|
||||
titleContainer.getChildren().add(titleBar);
|
||||
titleBar.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
primaryStage.setMaximized(!primaryStage.isMaximized());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
@@ -190,7 +220,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
btnHelp.setFocusTraversable(false);
|
||||
btnHelp.setGraphic(SVG.HELP.createIcon(Theme.foregroundFillBinding(), -1));
|
||||
btnHelp.getStyleClass().add("jfx-decorator-button");
|
||||
btnHelp.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html"));
|
||||
btnHelp.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
|
||||
JFXButton btnMin = new JFXButton();
|
||||
btnMin.setFocusTraversable(false);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public abstract class AbstractInstallersPage extends Control implements WizardPage {
|
||||
protected final WizardController controller;
|
||||
|
||||
protected InstallerItem.InstallerItemGroup group;
|
||||
protected JFXTextField txtName = new JFXTextField();
|
||||
|
||||
protected BooleanProperty installable = new SimpleBooleanProperty();
|
||||
|
||||
public AbstractInstallersPage(WizardController controller, String gameVersion, DownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
|
||||
library.setOnInstall(() -> {
|
||||
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
|
||||
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
|
||||
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
|
||||
});
|
||||
library.setOnRemove(() -> {
|
||||
controller.getSettings().remove(libraryId);
|
||||
reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected InstallerItem.Style getInstallerItemStyle() {
|
||||
return InstallerItem.Style.CARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract String getTitle();
|
||||
|
||||
protected abstract void reload();
|
||||
|
||||
@Override
|
||||
public void onNavigate(Map<String, Object> settings) {
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract void cleanup(Map<String, Object> settings);
|
||||
|
||||
protected abstract void onInstall();
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new InstallersPageSkin(this);
|
||||
}
|
||||
|
||||
protected static class InstallersPageSkin extends SkinBase<AbstractInstallersPage> {
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
protected InstallersPageSkin(AbstractInstallersPage control) {
|
||||
super(control);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPadding(new Insets(16));
|
||||
|
||||
{
|
||||
HBox versionNamePane = new HBox(8);
|
||||
versionNamePane.getStyleClass().add("card-non-transparent");
|
||||
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
control.txtName.setMaxWidth(300);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
|
||||
root.setTop(versionNamePane);
|
||||
}
|
||||
|
||||
{
|
||||
InstallerItem[] libraries = control.group.getLibraries();
|
||||
|
||||
FlowPane libraryPane = new FlowPane(libraries);
|
||||
libraryPane.setVgap(16);
|
||||
libraryPane.setHgap(16);
|
||||
|
||||
if (libraries.length <= 8) {
|
||||
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(libraryPane);
|
||||
} else {
|
||||
ScrollPane scrollPane = new ScrollPane(libraryPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(scrollPane);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
installButton.disableProperty().bind(control.installable.not());
|
||||
installButton.setPrefWidth(100);
|
||||
installButton.setPrefHeight(40);
|
||||
installButton.setOnAction(e -> control.onInstall());
|
||||
BorderPane.setAlignment(installButton, Pos.CENTER_RIGHT);
|
||||
root.setBottom(installButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,26 +36,21 @@ import java.util.Optional;
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
class AdditionalInstallersPage extends InstallersPage {
|
||||
class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
protected final BooleanProperty compatible = new SimpleBooleanProperty();
|
||||
protected final GameRepository repository;
|
||||
protected final String gameVersion;
|
||||
protected final Version version;
|
||||
|
||||
public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) {
|
||||
super(controller, repository, gameVersion, downloadProvider);
|
||||
super(controller, gameVersion, downloadProvider);
|
||||
this.gameVersion = gameVersion;
|
||||
this.version = version;
|
||||
this.repository = repository;
|
||||
|
||||
txtName.getValidators().clear();
|
||||
txtName.setText(version.getId());
|
||||
txtName.setEditable(false);
|
||||
|
||||
installable.bind(Bindings.createBooleanBinding(
|
||||
() -> compatible.get() && txtName.validate(),
|
||||
txtName.textProperty(), compatible));
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals("game")) continue;
|
||||
@@ -64,6 +59,8 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
reload();
|
||||
});
|
||||
}
|
||||
|
||||
installable.bind(Bindings.createBooleanBinding(() -> compatible.get() && txtName.validate(), txtName.textProperty(), compatible));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
@@ -43,19 +44,21 @@ 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;
|
||||
import org.jackhuang.hmcl.ui.versions.*;
|
||||
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.HMCLLocalizedDownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -69,6 +72,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private final TabHeader.Tab<DownloadListPage> modTab = new TabHeader.Tab<>("modTab");
|
||||
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
|
||||
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
|
||||
private final TabHeader.Tab<DownloadListPage> shaderTab = new TabHeader.Tab<>("shaderTab");
|
||||
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
|
||||
@@ -76,10 +80,16 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private WeakListenerHolder listenerHolder;
|
||||
|
||||
public DownloadPage() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DownloadPage(String uploadVersion) {
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack(Versions::downloadModpackImpl, false);
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((profile, __, file) -> {
|
||||
Versions.downloadModpackImpl(profile, uploadVersion, file);
|
||||
}, false);
|
||||
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
|
||||
@@ -89,8 +99,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(ModrinthRemoteModRepository.SHADER_PACKS, (profile, version, file) -> download(profile, version, file, "shaderpacks"), true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
|
||||
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
|
||||
Profiles.registerVersionsListener(this::loadVersions);
|
||||
|
||||
@@ -106,6 +117,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
.startCategory(i18n("download.content").toUpperCase(Locale.ROOT))
|
||||
.addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION)
|
||||
.addNavigationDrawerTab(tab, resourcePackTab, i18n("resourcepack"), SVG.TEXTURE)
|
||||
.addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY)
|
||||
.addNavigationDrawerTab(tab, worldTab, i18n("world"), SVG.PUBLIC);
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -130,7 +142,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
};
|
||||
}
|
||||
|
||||
private static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
|
||||
@@ -143,7 +155,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
|
||||
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
|
||||
task.setName(file.getName());
|
||||
return task;
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
@@ -177,6 +189,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
if (resourcePackTab.isInitialized()) {
|
||||
resourcePackTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (shaderTab.isInitialized()) {
|
||||
shaderTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (worldTab.isInitialized()) {
|
||||
worldTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
@@ -294,7 +309,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("title", i18n("install.new_game"));
|
||||
settings.put("title", i18n("install.new_game.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
|
||||
|
||||
@@ -17,47 +17,28 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
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;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class InstallersPage extends Control implements WizardPage {
|
||||
protected final WizardController controller;
|
||||
|
||||
protected InstallerItem.InstallerItemGroup group;
|
||||
protected JFXTextField txtName = new JFXTextField();
|
||||
protected BooleanProperty installable = new SimpleBooleanProperty();
|
||||
public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
private boolean isNameModifiedByUser = false;
|
||||
|
||||
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
|
||||
super(controller, gameVersion, downloadProvider);
|
||||
|
||||
txtName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
@@ -66,27 +47,6 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
|
||||
|
||||
txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true);
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
|
||||
library.setOnInstall(() -> {
|
||||
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
|
||||
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
|
||||
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
|
||||
});
|
||||
library.setOnRemove(() -> {
|
||||
controller.getSettings().remove(libraryId);
|
||||
reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected InstallerItem.Style getInstallerItemStyle() {
|
||||
return InstallerItem.Style.CARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,19 +73,27 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNavigate(Map<String, Object> settings) {
|
||||
reload();
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
private static boolean checkName(String name) {
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char c = name.charAt(i);
|
||||
if (!(c >= '0' && c <= '9')
|
||||
&& !(c >= 'a' && c <= 'z')
|
||||
&& !(c >= 'A' && c <= 'Z')
|
||||
&& c != '-' && c != '_' && c != '.'
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void onInstall() {
|
||||
String name = txtName.getText();
|
||||
|
||||
// Check for non-ASCII characters.
|
||||
if (!StandardCharsets.US_ASCII.newEncoder().canEncode(name)) {
|
||||
if (!checkName(name)) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("install.name.invalid"),
|
||||
i18n("message.warning"),
|
||||
@@ -143,11 +111,6 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new InstallersPageSkin(this);
|
||||
}
|
||||
|
||||
private void setTxtNameWithLoaders() {
|
||||
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().getVersion());
|
||||
|
||||
@@ -190,62 +153,4 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
txtName.setText(nameBuilder.toString());
|
||||
isNameModifiedByUser = false;
|
||||
}
|
||||
|
||||
protected static class InstallersPageSkin extends SkinBase<InstallersPage> {
|
||||
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
protected InstallersPageSkin(InstallersPage control) {
|
||||
super(control);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPadding(new Insets(16));
|
||||
|
||||
{
|
||||
HBox versionNamePane = new HBox(8);
|
||||
versionNamePane.getStyleClass().add("card-non-transparent");
|
||||
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
control.txtName.setMaxWidth(300);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
|
||||
root.setTop(versionNamePane);
|
||||
}
|
||||
|
||||
{
|
||||
InstallerItem[] libraries = control.group.getLibraries();
|
||||
|
||||
FlowPane libraryPane = new FlowPane(libraries);
|
||||
libraryPane.setVgap(16);
|
||||
libraryPane.setHgap(16);
|
||||
|
||||
if (libraries.length <= 8) {
|
||||
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(libraryPane);
|
||||
} else {
|
||||
ScrollPane scrollPane = new ScrollPane(libraryPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(scrollPane);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
installButton.disableProperty().bind(control.installable.not());
|
||||
installButton.setPrefWidth(100);
|
||||
installButton.setPrefHeight(40);
|
||||
installButton.setOnAction(e -> control.onInstall());
|
||||
BorderPane.setAlignment(installButton, Pos.CENTER_RIGHT);
|
||||
root.setBottom(installButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -153,7 +152,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
String name = txtModpackName.getText();
|
||||
|
||||
// Check for non-ASCII characters.
|
||||
if (!StandardCharsets.US_ASCII.newEncoder().canEncode(name)) {
|
||||
if (!StringUtils.isASCII(name)) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("install.name.invalid"),
|
||||
i18n("message.warning"),
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -102,7 +102,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
} catch (MismatchedModpackTypeException e) {
|
||||
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
|
||||
Controllers.dialog(i18n("modpack.mismatched_type", e.getRequired(), e.getFound()), i18n("message.error"), MessageType.ERROR);
|
||||
} catch (IOException e) {
|
||||
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageType.ERROR);
|
||||
}
|
||||
@@ -120,7 +120,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("title", i18n("install.modpack"));
|
||||
settings.put("title", i18n("install.modpack.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", new FailureCallback() {
|
||||
@Override
|
||||
|
||||
@@ -29,9 +29,7 @@ import javafx.scene.shape.SVGPath;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -43,13 +41,13 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_FILE;
|
||||
import static org.jackhuang.hmcl.ui.download.LocalModpackPage.MODPACK_NAME;
|
||||
import static org.jackhuang.hmcl.ui.download.RemoteModpackPage.MODPACK_SERVER_MANIFEST;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -125,10 +123,9 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
private void onChooseRemoteFile() {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
if (urlString.endsWith("server-manifest.json")) {
|
||||
if (url.endsWith("server-manifest.json")) {
|
||||
// if urlString ends with .json, we assume that the url is server-manifest.json
|
||||
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||
@@ -149,7 +146,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
resolve.run();
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(url, modpack.toFile(), null)
|
||||
new FileDownloadTask(url, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
resolve.run();
|
||||
@@ -170,7 +167,8 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
}
|
||||
|
||||
public void onChooseRepository() {
|
||||
DownloadPage downloadPage = new DownloadPage();
|
||||
String modPackName = (String) controller.getSettings().get(MODPACK_NAME);
|
||||
DownloadPage downloadPage = new DownloadPage(modPackName);
|
||||
downloadPage.showModpackDownloads();
|
||||
Controllers.navigate(downloadPage);
|
||||
}
|
||||
|
||||
+10
-10
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -71,7 +71,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("title", i18n("install.change_version"));
|
||||
settings.put("title", i18n("install.change_version.process"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> alertFailureMessage(exception, next));
|
||||
|
||||
@@ -133,28 +133,28 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
|
||||
int responseCode = rce.getResponseCode();
|
||||
URL url = rce.getUrl();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
message += i18n("download.code.404", url);
|
||||
message += i18n("download.code.404", uri);
|
||||
else
|
||||
message += i18n("download.failed", url, responseCode);
|
||||
message += i18n("download.failed", uri, responseCode);
|
||||
} else {
|
||||
message += StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else if (exception instanceof UnsupportedInstallationException) {
|
||||
switch (((UnsupportedInstallationException) exception).getReason()) {
|
||||
|
||||
@@ -66,7 +66,7 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
settings.put("title", i18n("install.new_game"));
|
||||
settings.put("title", i18n("install.new_game.installation"));
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_callback", (FailureCallback) (settings1, exception, next) -> UpdateInstallerWizardProvider.alertFailureMessage(exception, next));
|
||||
|
||||
|
||||
@@ -17,24 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
@@ -49,294 +42,73 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionsPage extends BorderPane implements WizardPage, Refreshable {
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final String libraryId;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
private final JFXSpinner spinner;
|
||||
private final StackPane failedPane;
|
||||
private final StackPane emptyPane;
|
||||
private final TransitionPane root;
|
||||
private final JFXCheckBox chkRelease;
|
||||
private final JFXCheckBox chkSnapshot;
|
||||
private final JFXCheckBox chkOld;
|
||||
private final ComponentList centrePane;
|
||||
private final StackPane center;
|
||||
|
||||
private final VersionList<?> versionList;
|
||||
private CompletableFuture<?> executor;
|
||||
private final Runnable callback;
|
||||
|
||||
private final HBox searchBar;
|
||||
private final StringProperty queryString = new SimpleStringProperty();
|
||||
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.navigation = navigation;
|
||||
|
||||
HintPane hintPane = new HintPane();
|
||||
hintPane.setText(i18n("sponsor.bmclapi"));
|
||||
hintPane.getStyleClass().add("sponsor-pane");
|
||||
FXUtils.onClicked(hintPane, this::onSponsor);
|
||||
BorderPane.setMargin(hintPane, new Insets(10, 10, 0, 10));
|
||||
this.setTop(hintPane);
|
||||
|
||||
root = new TransitionPane();
|
||||
BorderPane toolbarPane = new BorderPane();
|
||||
JFXButton btnRefresh;
|
||||
{
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
center = new StackPane();
|
||||
center.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
HBox checkPane = new HBox();
|
||||
checkPane.setSpacing(10);
|
||||
{
|
||||
chkRelease = new JFXCheckBox(i18n("version.game.releases"));
|
||||
chkRelease.setSelected(true);
|
||||
HBox.setMargin(chkRelease, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkSnapshot = new JFXCheckBox(i18n("version.game.snapshots"));
|
||||
HBox.setMargin(chkSnapshot, new Insets(10, 0, 10, 0));
|
||||
|
||||
chkOld = new JFXCheckBox(i18n("version.game.old"));
|
||||
HBox.setMargin(chkOld, new Insets(10, 0, 10, 0));
|
||||
|
||||
checkPane.getChildren().setAll(chkRelease, chkSnapshot, chkOld);
|
||||
}
|
||||
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
TransitionPane rightToolbarPane = new TransitionPane();
|
||||
{
|
||||
HBox refreshPane = new HBox();
|
||||
refreshPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
btnRefresh = new JFXButton(i18n("button.refresh"));
|
||||
btnRefresh.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnRefresh.setOnAction(e -> onRefresh());
|
||||
|
||||
JFXButton btnSearch = new JFXButton(i18n("search"));
|
||||
btnSearch.getStyleClass().add("jfx-tool-bar-button");
|
||||
btnSearch.setGraphic(wrap(SVG.SEARCH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
searchBar = new HBox();
|
||||
{
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 0));
|
||||
|
||||
JFXTextField searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
|
||||
JFXButton closeSearchBar = new JFXButton();
|
||||
closeSearchBar.getStyleClass().add("jfx-tool-bar-button");
|
||||
closeSearchBar.setGraphic(wrap(SVG.CLOSE.createIcon(Theme.blackFill(), -1)));
|
||||
closeSearchBar.setOnAction(e -> {
|
||||
searchField.clear();
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.FADE);
|
||||
});
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
PauseTransition pause = new PauseTransition(Duration.millis(100));
|
||||
pause.setOnFinished(e -> queryString.set(searchField.getText()));
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pause.setRate(1);
|
||||
pause.playFromStart();
|
||||
});
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
btnSearch.setOnAction(e -> {
|
||||
rightToolbarPane.setContent(searchBar, ContainerAnimations.FADE);
|
||||
searchField.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
refreshPane.getChildren().setAll(new HBox(btnSearch, btnRefresh));
|
||||
rightToolbarPane.setContent(refreshPane, ContainerAnimations.NONE);
|
||||
}
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
toolbarPane.setLeft(checkPane);
|
||||
toolbarPane.setRight(rightToolbarPane);
|
||||
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
}
|
||||
|
||||
center.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, this::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, this::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
}
|
||||
this.setCenter(root);
|
||||
|
||||
versionList = downloadProvider.getVersionListById(libraryId);
|
||||
boolean hasType = versionList.hasType();
|
||||
chkRelease.setManaged(hasType);
|
||||
chkRelease.setVisible(hasType);
|
||||
chkSnapshot.setManaged(hasType);
|
||||
chkSnapshot.setVisible(hasType);
|
||||
chkOld.setManaged(hasType);
|
||||
chkOld.setVisible(hasType);
|
||||
|
||||
if (hasType) {
|
||||
centrePane.getContent().setAll(toolbarPane, list);
|
||||
} else {
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
List<RemoteVersion> versions = loadVersions();
|
||||
String query = queryString.get();
|
||||
if (!StringUtils.isBlank(query)) {
|
||||
Predicate<RemoteVersion> predicate;
|
||||
if (query.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(query.substring("regex:".length()));
|
||||
predicate = it -> pattern.matcher(it.getSelfVersion()).find();
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression", e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = query.toLowerCase(Locale.ROOT);
|
||||
predicate = it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString);
|
||||
}
|
||||
|
||||
versions = versions.stream().filter(predicate).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
list.getItems().setAll(versions);
|
||||
};
|
||||
chkRelease.selectedProperty().addListener(listener);
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
queryString.addListener(listener);
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.REFRESH.createIcon(Theme.blackFill(), -1)));
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
navigation.getSettings().put(libraryId, list.getSelectionModel().getSelectedItem());
|
||||
callback.run();
|
||||
});
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
this.callback = callback;
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private List<RemoteVersion> loadVersions() {
|
||||
return versionList.getVersions(gameVersion).stream()
|
||||
.filter(it -> {
|
||||
switch (it.getVersionType()) {
|
||||
case RELEASE:
|
||||
return chkRelease.isSelected();
|
||||
case SNAPSHOT:
|
||||
return chkSnapshot.isSelected();
|
||||
case OLD:
|
||||
return chkOld.isSelected();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.sorted().collect(Collectors.toList());
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new VersionsPageSkin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() {
|
||||
VersionList<?> currentVersionList = versionList;
|
||||
root.setContent(spinner, ContainerAnimations.FADE);
|
||||
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
|
||||
if (exception == null) {
|
||||
List<RemoteVersion> items = loadVersions();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
if (currentVersionList.getVersions(gameVersion).isEmpty()) {
|
||||
root.setContent(emptyPane, ContainerAnimations.FADE);
|
||||
status.set(Status.LOADING);
|
||||
Task<?> task = versionList.refreshAsync(gameVersion)
|
||||
.thenSupplyAsync(() -> versionList.getVersions(gameVersion).stream().sorted().collect(Collectors.toList()))
|
||||
.whenComplete(Schedulers.javafx(), (items, exception) -> {
|
||||
if (exception == null) {
|
||||
versions.setAll(items);
|
||||
status.set(Status.SUCCESS);
|
||||
} else {
|
||||
if (items.isEmpty()) {
|
||||
chkRelease.setSelected(true);
|
||||
chkSnapshot.setSelected(true);
|
||||
chkOld.setSelected(true);
|
||||
} else {
|
||||
list.getItems().setAll(items);
|
||||
}
|
||||
root.setContent(center, ContainerAnimations.FADE);
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
status.set(Status.FAILED);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOG.warning("Failed to fetch versions list", exception);
|
||||
Platform.runLater(() -> {
|
||||
if (versionList != currentVersionList) return;
|
||||
root.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
});
|
||||
task.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -347,8 +119,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
settings.remove(libraryId);
|
||||
if (executor != null)
|
||||
executor.cancel(true);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
@@ -363,10 +133,24 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
|
||||
}
|
||||
|
||||
private enum Status {
|
||||
LOADING,
|
||||
FAILED,
|
||||
SUCCESS,
|
||||
}
|
||||
|
||||
private enum VersionTypeFilter {
|
||||
ALL,
|
||||
RELEASE,
|
||||
SNAPSHOTS,
|
||||
APRIL_FOOLS,
|
||||
OLD
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
private final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
private final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
private final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
|
||||
@@ -405,23 +189,30 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
String wikiSuffix = getWikiUrlSuffix(remoteVersion.getGameVersion());
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(VersionIconType.GRASS.getIcon());
|
||||
content.setExternalLink(i18n("wiki.version.game.release", remoteVersion.getGameVersion()));
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
|
||||
|
||||
content.setExternalLink(i18n("wiki.version.game.snapshot", remoteVersion.getGameVersion()));
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isSpecial()) {
|
||||
content.getTags().setAll(i18n("version.game.april_fools"));
|
||||
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
content.setExternalLink(null);
|
||||
content.setExternalLink(i18n("wiki.version.game", wikiSuffix));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -449,5 +240,285 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
content.setExternalLink(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getWikiUrlSuffix(String gameVersion) {
|
||||
String id = gameVersion.toLowerCase(Locale.ROOT);
|
||||
|
||||
switch (id) {
|
||||
case "0.30-1":
|
||||
case "0.30-2":
|
||||
case "c0.30_01c":
|
||||
return i18n("wiki.version.game.search", "Classic_0.30");
|
||||
case "in-20100206-2103":
|
||||
return i18n("wiki.version.game.search", "Indev_20100206");
|
||||
case "inf-20100630-1":
|
||||
return i18n("wiki.version.game.search", "Infdev_20100630");
|
||||
case "inf-20100630-2":
|
||||
return i18n("wiki.version.game.search", "Alpha_v1.0.0");
|
||||
case "1.19_deep_dark_experimental_snapshot-1":
|
||||
return "1.19-exp1";
|
||||
case "in-20100130":
|
||||
return i18n("wiki.version.game.search", "Indev_0.31_20100130");
|
||||
case "b1.6-tb3":
|
||||
return i18n("wiki.version.game.search", "Beta_1.6_Test_Build_3");
|
||||
case "1.14_combat-212796":
|
||||
return i18n("wiki.version.game.search", "1.14.3_-_Combat_Test");
|
||||
case "1.14_combat-0":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_2");
|
||||
case "1.14_combat-3":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_3");
|
||||
case "1_15_combat-1":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_4");
|
||||
case "1_15_combat-6":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_5");
|
||||
case "1_16_combat-0":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_6");
|
||||
case "1_16_combat-1":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7");
|
||||
case "1_16_combat-2":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7b");
|
||||
case "1_16_combat-3":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_7c");
|
||||
case "1_16_combat-4":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8");
|
||||
case "1_16_combat-5":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8b");
|
||||
case "1_16_combat-6":
|
||||
return i18n("wiki.version.game.search", "Combat_Test_8c");
|
||||
}
|
||||
|
||||
if (id.startsWith("1.0.0-rc2")) return "RC2";
|
||||
if (id.startsWith("2.0")) return i18n("wiki.version.game.search", "2.0");
|
||||
if (id.startsWith("b1.8-pre1")) return "Beta_1.8-pre1";
|
||||
if (id.startsWith("b1.1-")) return i18n("wiki.version.game.search", "Beta_1.1");
|
||||
if (id.startsWith("a1.1.0")) return "Alpha_v1.1.0";
|
||||
if (id.startsWith("a1.0.14")) return "Alpha_v1.0.14";
|
||||
if (id.startsWith("a1.0.13_01")) return "Alpha_v1.0.13_01";
|
||||
if (id.startsWith("in-20100214")) return i18n("wiki.version.game.search", "Indev_20100214");
|
||||
|
||||
if (id.contains("experimental-snapshot")) {
|
||||
return id.replace("_experimental-snapshot-", "-exp");
|
||||
}
|
||||
|
||||
if (id.startsWith("inf-")) return id.replace("inf-", "Infdev_");
|
||||
if (id.startsWith("in-")) return id.replace("in-", "Indev_");
|
||||
if (id.startsWith("rd-")) return "pre-Classic_" + id;
|
||||
if (id.startsWith("b")) return id.replace("b", "Beta_");
|
||||
if (id.startsWith("a")) return id.replace("a", "Alpha_v");
|
||||
if (id.startsWith("c")) return id.replace("c", "Classic_").replace("st", "SURVIVAL_TEST");
|
||||
|
||||
return i18n("wiki.version.game.search", id);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
|
||||
private final TransitionPane transitionPane;
|
||||
private final JFXSpinner spinner;
|
||||
|
||||
private final JFXTextField nameField;
|
||||
private final JFXComboBox<VersionTypeFilter> categoryField = new JFXComboBox<>();
|
||||
|
||||
VersionsPageSkin(VersionsPage control) {
|
||||
super(control);
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
root.setTop(searchPane);
|
||||
searchPane.getStyleClass().addAll("card");
|
||||
BorderPane.setMargin(searchPane, new Insets(10, 10, 0, 10));
|
||||
|
||||
ColumnConstraints nameColumn = new ColumnConstraints();
|
||||
nameColumn.setMinWidth(USE_PREF_SIZE);
|
||||
ColumnConstraints column1 = new ColumnConstraints();
|
||||
column1.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints column2 = new ColumnConstraints();
|
||||
column2.setMaxWidth(150);
|
||||
ColumnConstraints column3 = new ColumnConstraints();
|
||||
|
||||
if (control.versionList.hasType())
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, nameColumn, column2, column3);
|
||||
else
|
||||
searchPane.getColumnConstraints().setAll(nameColumn, column1, column3);
|
||||
|
||||
searchPane.setHgap(16);
|
||||
searchPane.setVgap(10);
|
||||
|
||||
{
|
||||
int rowIndex = 0;
|
||||
|
||||
{
|
||||
nameField = new JFXTextField();
|
||||
nameField.setPromptText(i18n("version.search.prompt"));
|
||||
nameField.textProperty().addListener(o -> updateList());
|
||||
|
||||
if ("game".equals(control.libraryId)) {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS,
|
||||
VersionTypeFilter.APRIL_FOOLS,
|
||||
VersionTypeFilter.OLD
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
|
||||
} else {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
VersionTypeFilter.SNAPSHOTS
|
||||
);
|
||||
categoryField.getSelectionModel().select(VersionTypeFilter.ALL);
|
||||
}
|
||||
categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT))));
|
||||
categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList());
|
||||
|
||||
JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
refreshButton.setOnAction(event -> control.onRefresh());
|
||||
|
||||
if (control.versionList.hasType()) {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
new Label(i18n("version.game.type")), categoryField,
|
||||
refreshButton
|
||||
);
|
||||
} else {
|
||||
searchPane.addRow(rowIndex++,
|
||||
new Label(i18n("version.search")), nameField,
|
||||
refreshButton
|
||||
);
|
||||
}
|
||||
}
|
||||
// {
|
||||
// HBox actionsBox = new HBox(8);
|
||||
// GridPane.setColumnSpan(actionsBox, 4);
|
||||
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
//
|
||||
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
// refreshButton.setOnAction(event -> control.onRefresh());
|
||||
//
|
||||
// actionsBox.getChildren().setAll(refreshButton);
|
||||
//
|
||||
// searchPane.addRow(rowIndex++, actionsBox);
|
||||
// }
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
root.setCenter(spinnerPane);
|
||||
|
||||
transitionPane = new TransitionPane();
|
||||
spinner = new JFXSpinner();
|
||||
|
||||
StackPane centerWrapper = new StackPane();
|
||||
centerWrapper.setStyle("-fx-padding: 10;");
|
||||
{
|
||||
ComponentList centrePane = new ComponentList();
|
||||
centrePane.getStyleClass().add("no-padding");
|
||||
{
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
control.versions.addListener((InvalidationListener) o -> updateList());
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem());
|
||||
control.callback.run();
|
||||
});
|
||||
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(list, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
centrePane.getContent().setAll(list);
|
||||
}
|
||||
|
||||
centerWrapper.getChildren().setAll(centrePane);
|
||||
}
|
||||
|
||||
StackPane failedPane = new StackPane();
|
||||
failedPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.refresh"));
|
||||
FXUtils.onClicked(label, control::onRefresh);
|
||||
|
||||
failedPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
StackPane emptyPane = new StackPane();
|
||||
emptyPane.getStyleClass().add("notice-pane");
|
||||
{
|
||||
Label label = new Label(i18n("download.failed.empty"));
|
||||
FXUtils.onClicked(label, control::onBack);
|
||||
|
||||
emptyPane.getChildren().setAll(label);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(control.status, status -> {
|
||||
if (status == Status.LOADING)
|
||||
transitionPane.setContent(spinner, ContainerAnimations.FADE);
|
||||
else if (status == Status.SUCCESS)
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
else // if (status == Status.FAILED)
|
||||
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
root.setCenter(transitionPane);
|
||||
}
|
||||
|
||||
this.getChildren().setAll(root);
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
|
||||
|
||||
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
switch (filter) {
|
||||
case RELEASE:
|
||||
return versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isSpecial();
|
||||
case OLD:
|
||||
return versionType == RemoteVersion.Type.OLD;
|
||||
case ALL:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
String nameQuery = nameField.getText();
|
||||
if (!StringUtils.isBlank(nameQuery)) {
|
||||
if (nameQuery.startsWith("regex:")) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(nameQuery.substring("regex:".length()));
|
||||
versions = versions.filter(it -> pattern.matcher(it.getSelfVersion()).find());
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression: " + nameQuery, e);
|
||||
}
|
||||
} else {
|
||||
String lowerQueryString = nameQuery.toLowerCase(Locale.ROOT);
|
||||
versions = versions.filter(it -> it.getSelfVersion().toLowerCase(Locale.ROOT).contains(lowerQueryString));
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
list.getItems().setAll(versions.collect(Collectors.toList()));
|
||||
list.scrollTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,16 @@
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.Config;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.FontManager;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -99,6 +101,9 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
exportTask = exportAsServer(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH:
|
||||
exportTask = exportAsModrinth(exportInfo, dest);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
|
||||
}
|
||||
@@ -123,19 +128,31 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
exported.setPreferredLoginType(config().getPreferredLoginType());
|
||||
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
|
||||
|
||||
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
|
||||
zip.putTextFile(exported.toJson(), ".hmcl/hmcl.json");
|
||||
zip.putFile(tempModpack, "modpack.zip");
|
||||
|
||||
File bg = new File("bg").getAbsoluteFile();
|
||||
if (bg.isDirectory())
|
||||
zip.putDirectory(bg.toPath(), "bg");
|
||||
Path bg = Metadata.HMCL_CURRENT_DIRECTORY.resolve("background");
|
||||
if (!Files.isDirectory(bg))
|
||||
bg = Metadata.CURRENT_DIRECTORY.resolve("bg");
|
||||
if (Files.isDirectory(bg))
|
||||
zip.putDirectory(bg, ".hmcl/bg");
|
||||
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
String fileName = "background." + extension;
|
||||
Path background = Metadata.HMCL_CURRENT_DIRECTORY.resolve(fileName);
|
||||
if (!Files.isRegularFile(background))
|
||||
background = Metadata.CURRENT_DIRECTORY.resolve(fileName);
|
||||
if (Files.isRegularFile(background))
|
||||
zip.putFile(background, ".hmcl/" + fileName);
|
||||
}
|
||||
|
||||
File background = new File(fileName).getAbsoluteFile();
|
||||
if (background.isFile())
|
||||
zip.putFile(background, "background.png");
|
||||
for (String extension : FontManager.FONT_EXTENSIONS) {
|
||||
String fileName = "font." + extension;
|
||||
Path font = Metadata.HMCL_CURRENT_DIRECTORY.resolve(fileName);
|
||||
if (!Files.isRegularFile(font))
|
||||
font = Metadata.CURRENT_DIRECTORY.resolve(fileName);
|
||||
if (Files.isRegularFile(font))
|
||||
zip.putFile(font, ".hmcl/" + fileName);
|
||||
}
|
||||
|
||||
zip.putFile(launcherJar, launcherJar.getFileName().toString());
|
||||
@@ -220,6 +237,27 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ModrinthModpackExportTask(
|
||||
profile.getRepository(),
|
||||
version,
|
||||
exportInfo,
|
||||
modpackFile
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependencies() {
|
||||
return Collections.singleton(dependency);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
switch (step) {
|
||||
|
||||
@@ -27,6 +27,8 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
@@ -41,7 +43,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
@@ -51,7 +53,9 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH;
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -77,6 +81,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
|
||||
private final SimpleBooleanProperty noCreateRemoteFiles = new SimpleBooleanProperty();
|
||||
private final SimpleBooleanProperty skipCurseForgeRemoteFiles = new SimpleBooleanProperty();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
|
||||
this.controller = controller;
|
||||
@@ -99,7 +105,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private void onNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
if (controller.getSettings().get(MODPACK_TYPE) == ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH) {
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.mrpack"));
|
||||
fileChooser.setInitialFileName(name.get() + ".mrpack");
|
||||
} else {
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
|
||||
fileChooser.setInitialFileName(name.get() + ".zip");
|
||||
}
|
||||
File file = fileChooser.showSaveDialog(Controllers.getStage());
|
||||
if (file == null) {
|
||||
controller.onEnd();
|
||||
@@ -119,6 +131,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
exportInfo.setLaunchArguments(launchArguments.get());
|
||||
exportInfo.setJavaArguments(javaArguments.get());
|
||||
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
|
||||
exportInfo.setNoCreateRemoteFiles(noCreateRemoteFiles.get());
|
||||
exportInfo.setSkipCurseForgeRemoteFiles(skipCurseForgeRemoteFiles.get());
|
||||
|
||||
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
|
||||
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
|
||||
@@ -173,8 +187,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
|
||||
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER) {
|
||||
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
|
||||
hyperlink.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/modpack/serverpack.html"));
|
||||
hyperlink.setOnAction(e -> FXUtils.openLink(Metadata.DOCS_URL + "/modpack/serverpack.html"));
|
||||
borderPane.setTop(hyperlink);
|
||||
} if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_MODRINTH) {
|
||||
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
pane.setText(i18n("modpack.wizard.step.initialization.modrinth.info"));
|
||||
borderPane.setTop(pane);
|
||||
} else {
|
||||
HintPane pane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
pane.setText(i18n("modpack.wizard.step.initialization.warning"));
|
||||
@@ -285,12 +303,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
AtomicBoolean changedByTextField = new AtomicBoolean(false);
|
||||
FXUtils.onChangeAndOperate(skinnable.minMemory, minMemory -> {
|
||||
changedByTextField.set(true);
|
||||
slider.setValue(minMemory.intValue() * 1.0 / OperatingSystem.TOTAL_MEMORY);
|
||||
slider.setValue(minMemory.intValue() * 1.0 / MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize()));
|
||||
changedByTextField.set(false);
|
||||
});
|
||||
slider.valueProperty().addListener((value, oldVal, newVal) -> {
|
||||
if (changedByTextField.get()) return;
|
||||
skinnable.minMemory.set((int) (value.getValue().doubleValue() * OperatingSystem.TOTAL_MEMORY));
|
||||
skinnable.minMemory.set((int) (value.getValue().doubleValue() * MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize())));
|
||||
});
|
||||
|
||||
JFXTextField txtMinMemory = new JFXTextField();
|
||||
@@ -299,7 +317,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
FXUtils.setLimitWidth(txtMinMemory, 60);
|
||||
validatingFields.add(txtMinMemory);
|
||||
|
||||
lowerBoundPane.getChildren().setAll(label, slider, txtMinMemory, new Label("MB"));
|
||||
lowerBoundPane.getChildren().setAll(label, slider, txtMinMemory, new Label("MiB"));
|
||||
}
|
||||
|
||||
pane.getChildren().setAll(title, lowerBoundPane);
|
||||
@@ -363,6 +381,32 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireNoCreateRemoteFiles()) {
|
||||
BorderPane noCreateRemoteFiles = new BorderPane();
|
||||
noCreateRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.no_create_remote_files")));
|
||||
list.getContent().add(noCreateRemoteFiles);
|
||||
|
||||
JFXToggleButton noCreateRemoteFilesButton = new JFXToggleButton();
|
||||
noCreateRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.noCreateRemoteFiles);
|
||||
noCreateRemoteFilesButton.setSize(8);
|
||||
noCreateRemoteFilesButton.setMinHeight(16);
|
||||
noCreateRemoteFilesButton.setMaxHeight(16);
|
||||
noCreateRemoteFiles.setRight(noCreateRemoteFilesButton);
|
||||
}
|
||||
|
||||
if (skinnable.options.isRequireSkipCurseForgeRemoteFiles()) {
|
||||
BorderPane skipCurseForgeRemoteFiles = new BorderPane();
|
||||
skipCurseForgeRemoteFiles.setLeft(new Label(i18n("modpack.wizard.step.initialization.skip_curseforge_remote_files")));
|
||||
list.getContent().add(skipCurseForgeRemoteFiles);
|
||||
|
||||
JFXToggleButton skipCurseForgeRemoteFilesButton = new JFXToggleButton();
|
||||
skipCurseForgeRemoteFilesButton.selectedProperty().bindBidirectional(skinnable.skipCurseForgeRemoteFiles);
|
||||
skipCurseForgeRemoteFilesButton.setSize(8);
|
||||
skipCurseForgeRemoteFilesButton.setMinHeight(16);
|
||||
skipCurseForgeRemoteFilesButton.setMaxHeight(16);
|
||||
skipCurseForgeRemoteFiles.setRight(skipCurseForgeRemoteFilesButton);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
@@ -43,22 +44,28 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
|
||||
public ModpackTypeSelectionPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
this.setPadding(new Insets(10));
|
||||
|
||||
Label title = new Label(i18n("modpack.export.as"));
|
||||
title.setPadding(new Insets(8));
|
||||
VBox.setMargin(title, new Insets(8, 0, 8, 12));
|
||||
|
||||
this.getStyleClass().add("jfx-list-view");
|
||||
this.setMaxSize(300, 150);
|
||||
this.setMaxSize(400, 150);
|
||||
this.setSpacing(8);
|
||||
|
||||
this.getChildren().setAll(
|
||||
title,
|
||||
createButton(MODPACK_TYPE_MCBBS, McbbsModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MULTIMC, MultiMCModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION)
|
||||
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MODRINTH, ModrinthModpackExportTask.OPTION)
|
||||
);
|
||||
}
|
||||
|
||||
private JFXButton createButton(String type, ModpackExportInfo.Options option) {
|
||||
JFXButton button = new JFXButton();
|
||||
|
||||
button.getStyleClass().add("card");
|
||||
button.setOnAction(e -> {
|
||||
controller.getSettings().put(MODPACK_TYPE, type);
|
||||
controller.getSettings().put(MODPACK_INFO_OPTION, option);
|
||||
@@ -95,4 +102,5 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
public static final String MODPACK_TYPE_SERVER = "server";
|
||||
public static final String MODPACK_TYPE_MODRINTH = "modrinth";
|
||||
}
|
||||
+2
-9
@@ -15,17 +15,10 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
package org.jackhuang.hmcl.ui.image;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class ModuleHelper {
|
||||
|
||||
public static void addEnableNativeAccess(Class<?> clazzInModule) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
private ModuleHelper() {
|
||||
}
|
||||
public interface AnimationImage {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.image;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface ImageLoader {
|
||||
Image load(InputStream input,
|
||||
int requestedWidth, int requestedHeight,
|
||||
boolean preserveRatio, boolean smooth) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.image;
|
||||
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import org.jackhuang.hmcl.ui.image.apng.Png;
|
||||
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
|
||||
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
|
||||
import org.jackhuang.hmcl.util.SwingFXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class ImageUtils {
|
||||
|
||||
// ImageLoaders
|
||||
|
||||
public static final ImageLoader DEFAULT = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
Image image = new Image(input,
|
||||
requestedWidth, requestedHeight,
|
||||
preserveRatio, smooth);
|
||||
if (image.isError())
|
||||
throw image.getException();
|
||||
return image;
|
||||
};
|
||||
|
||||
public static final ImageLoader WEBP = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
WebPImageReaderSpi spi = new WebPImageReaderSpi();
|
||||
ImageReader reader = spi.createReaderInstance(null);
|
||||
BufferedImage bufferedImage;
|
||||
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
|
||||
reader.setInput(imageInput, true, true);
|
||||
bufferedImage = reader.read(0, reader.getDefaultReadParam());
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
};
|
||||
|
||||
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
|
||||
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
|
||||
try {
|
||||
var sequence = Png.readArgb8888BitmapSequence(input);
|
||||
|
||||
final int width = sequence.header.width;
|
||||
final int height = sequence.header.height;
|
||||
|
||||
boolean doScale;
|
||||
if (requestedWidth > 0 && requestedHeight > 0
|
||||
&& (requestedWidth != width || requestedHeight != height)) {
|
||||
doScale = true;
|
||||
|
||||
if (preserveRatio) {
|
||||
double scaleX = (double) requestedWidth / width;
|
||||
double scaleY = (double) requestedHeight / height;
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
|
||||
requestedWidth = (int) (width * scale);
|
||||
requestedHeight = (int) (height * scale);
|
||||
}
|
||||
} else {
|
||||
doScale = false;
|
||||
}
|
||||
|
||||
if (sequence.isAnimated()) {
|
||||
try {
|
||||
return toImage(sequence, doScale, requestedWidth, requestedHeight);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load animated image", e);
|
||||
}
|
||||
}
|
||||
|
||||
Argb8888Bitmap defaultImage = sequence.defaultImage;
|
||||
int targetWidth;
|
||||
int targetHeight;
|
||||
int[] pixels;
|
||||
if (doScale) {
|
||||
targetWidth = requestedWidth;
|
||||
targetHeight = requestedHeight;
|
||||
pixels = scale(defaultImage.array,
|
||||
defaultImage.width, defaultImage.height,
|
||||
targetWidth, targetHeight);
|
||||
} else {
|
||||
targetWidth = defaultImage.width;
|
||||
targetHeight = defaultImage.height;
|
||||
pixels = defaultImage.array;
|
||||
}
|
||||
|
||||
WritableImage image = new WritableImage(targetWidth, targetHeight);
|
||||
image.getPixelWriter().setPixels(0, 0, targetWidth, targetHeight,
|
||||
PixelFormat.getIntArgbInstance(), pixels,
|
||||
0, targetWidth);
|
||||
return image;
|
||||
} catch (PngException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
};
|
||||
|
||||
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
|
||||
"webp", WEBP,
|
||||
"apng", APNG
|
||||
);
|
||||
|
||||
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
|
||||
"image/webp", WEBP,
|
||||
"image/apng", APNG
|
||||
);
|
||||
|
||||
public static final Set<String> DEFAULT_EXTS = Set.of(
|
||||
"jpg", "jpeg", "bmp", "gif"
|
||||
);
|
||||
|
||||
public static final Set<String> DEFAULT_CONTENT_TYPES = Set.of(
|
||||
"image/jpeg", "image/bmp", "image/gif"
|
||||
);
|
||||
|
||||
// ------
|
||||
|
||||
public static final int HEADER_BUFFER_SIZE = 1024;
|
||||
|
||||
private static final byte[] RIFF_HEADER = {'R', 'I', 'F', 'F'};
|
||||
private static final byte[] WEBP_HEADER = {'W', 'E', 'B', 'P'};
|
||||
|
||||
public static boolean isWebP(byte[] headerBuffer) {
|
||||
return headerBuffer.length > 12
|
||||
&& Arrays.equals(headerBuffer, 0, 4, RIFF_HEADER, 0, 4)
|
||||
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
|
||||
}
|
||||
|
||||
private static final byte[] PNG_HEADER = {
|
||||
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
|
||||
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
|
||||
};
|
||||
|
||||
private static final class PngChunkHeader {
|
||||
private static final int IDAT_HEADER = 0x49444154;
|
||||
private static final int acTL_HEADER = 0x6163544c;
|
||||
|
||||
private final int length;
|
||||
private final int chunkType;
|
||||
|
||||
private PngChunkHeader(int length, int chunkType) {
|
||||
this.length = length;
|
||||
this.chunkType = chunkType;
|
||||
}
|
||||
|
||||
private static @Nullable PngChunkHeader readHeader(ByteBuffer headerBuffer) {
|
||||
if (headerBuffer.remaining() < 8)
|
||||
return null;
|
||||
|
||||
int length = headerBuffer.getInt();
|
||||
int chunkType = headerBuffer.getInt();
|
||||
|
||||
return new PngChunkHeader(length, chunkType);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isApng(byte[] headerBuffer) {
|
||||
if (headerBuffer.length <= 20)
|
||||
return false;
|
||||
|
||||
if (!Arrays.equals(
|
||||
headerBuffer, 0, 8,
|
||||
PNG_HEADER, 0, 8))
|
||||
return false;
|
||||
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(headerBuffer, 8, headerBuffer.length - 8);
|
||||
|
||||
PngChunkHeader header;
|
||||
while ((header = PngChunkHeader.readHeader(buffer)) != null) {
|
||||
// https://wiki.mozilla.org/APNG_Specification#Structure
|
||||
// To be recognized as an APNG, an `acTL` chunk must appear in the stream before any `IDAT` chunks.
|
||||
// The `acTL` structure is described below.
|
||||
if (header.chunkType == PngChunkHeader.IDAT_HEADER)
|
||||
break;
|
||||
|
||||
if (header.chunkType == PngChunkHeader.acTL_HEADER)
|
||||
return true;
|
||||
|
||||
final int numBytes = header.length + 4;
|
||||
|
||||
if (buffer.remaining() > numBytes)
|
||||
buffer.position(buffer.position() + numBytes);
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static @Nullable ImageLoader guessLoader(byte[] headerBuffer) {
|
||||
if (isWebP(headerBuffer))
|
||||
return WEBP;
|
||||
if (isApng(headerBuffer))
|
||||
return APNG;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^\\s(?<type>image/[\\w-])");
|
||||
|
||||
// APNG
|
||||
|
||||
private static int[] scale(int[] pixels,
|
||||
int sourceWidth, int sourceHeight,
|
||||
int targetWidth, int targetHeight) {
|
||||
assert pixels.length == sourceWidth * sourceHeight;
|
||||
|
||||
double xScale = ((double) sourceWidth) / targetWidth;
|
||||
double yScale = ((double) sourceHeight) / targetHeight;
|
||||
|
||||
int[] result = new int[targetWidth * targetHeight];
|
||||
|
||||
for (int row = 0; row < targetHeight; row++) {
|
||||
for (int col = 0; col < targetWidth; col++) {
|
||||
int sourceX = (int) (col * xScale);
|
||||
int sourceY = (int) (row * yScale);
|
||||
int color = pixels[sourceY * sourceWidth + sourceX];
|
||||
|
||||
result[row * targetWidth + col] = color;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Image toImage(Argb8888BitmapSequence sequence,
|
||||
boolean doScale,
|
||||
int targetWidth, int targetHeight) throws PngException {
|
||||
final int width = sequence.header.width;
|
||||
final int height = sequence.header.height;
|
||||
|
||||
List<Argb8888BitmapSequence.Frame> frames = sequence.getAnimationFrames();
|
||||
|
||||
var framePixels = new int[frames.size()][];
|
||||
var durations = new int[framePixels.length];
|
||||
|
||||
int[] buffer = new int[Math.multiplyExact(width, height)];
|
||||
for (int frameIndex = 0; frameIndex < frames.size(); frameIndex++) {
|
||||
var frame = frames.get(frameIndex);
|
||||
PngFrameControl control = frame.control;
|
||||
|
||||
if (frameIndex == 0 && (
|
||||
control.xOffset != 0 || control.yOffset != 0
|
||||
|| control.width != width || control.height != height)) {
|
||||
throw new PngIntegrityException("Invalid first frame: " + control);
|
||||
}
|
||||
|
||||
if (control.xOffset < 0 || control.yOffset < 0
|
||||
|| width < 0 || height < 0
|
||||
|| control.xOffset + control.width > width
|
||||
|| control.yOffset + control.height > height
|
||||
|| control.delayNumerator < 0 || control.delayDenominator < 0
|
||||
) {
|
||||
throw new PngIntegrityException("Invalid frame control: " + control);
|
||||
}
|
||||
|
||||
int[] currentFrameBuffer = buffer.clone();
|
||||
if (control.blendOp == 0) {
|
||||
for (int row = 0; row < control.height; row++) {
|
||||
System.arraycopy(frame.bitmap.array,
|
||||
row * control.width,
|
||||
currentFrameBuffer,
|
||||
(control.yOffset + row) * width + control.xOffset,
|
||||
control.width);
|
||||
}
|
||||
} else if (control.blendOp == 1) {
|
||||
// APNG_BLEND_OP_OVER - Alpha blending
|
||||
for (int row = 0; row < control.height; row++) {
|
||||
for (int col = 0; col < control.width; col++) {
|
||||
int srcIndex = row * control.width + col;
|
||||
int dstIndex = (control.yOffset + row) * width + control.xOffset + col;
|
||||
|
||||
int srcPixel = frame.bitmap.array[srcIndex];
|
||||
int dstPixel = currentFrameBuffer[dstIndex];
|
||||
|
||||
int srcAlpha = (srcPixel >>> 24) & 0xFF;
|
||||
if (srcAlpha == 0) {
|
||||
continue;
|
||||
} else if (srcAlpha == 255) {
|
||||
currentFrameBuffer[dstIndex] = srcPixel;
|
||||
} else {
|
||||
int srcR = (srcPixel >>> 16) & 0xFF;
|
||||
int srcG = (srcPixel >>> 8) & 0xFF;
|
||||
int srcB = srcPixel & 0xFF;
|
||||
|
||||
int dstAlpha = (dstPixel >>> 24) & 0xFF;
|
||||
int dstR = (dstPixel >>> 16) & 0xFF;
|
||||
int dstG = (dstPixel >>> 8) & 0xFF;
|
||||
int dstB = dstPixel & 0xFF;
|
||||
|
||||
int invSrcAlpha = 255 - srcAlpha;
|
||||
|
||||
int outAlpha = srcAlpha + (dstAlpha * invSrcAlpha + 127) / 255;
|
||||
int outR, outG, outB;
|
||||
|
||||
if (outAlpha == 0) {
|
||||
outR = outG = outB = 0;
|
||||
} else {
|
||||
outR = (srcR * srcAlpha + dstR * dstAlpha * invSrcAlpha / 255 + outAlpha / 2) / outAlpha;
|
||||
outG = (srcG * srcAlpha + dstG * dstAlpha * invSrcAlpha / 255 + outAlpha / 2) / outAlpha;
|
||||
outB = (srcB * srcAlpha + dstB * dstAlpha * invSrcAlpha / 255 + outAlpha / 2) / outAlpha;
|
||||
}
|
||||
|
||||
outAlpha = Math.min(outAlpha, 255);
|
||||
outR = Math.min(outR, 255);
|
||||
outG = Math.min(outG, 255);
|
||||
outB = Math.min(outB, 255);
|
||||
|
||||
currentFrameBuffer[dstIndex] = (outAlpha << 24) | (outR << 16) | (outG << 8) | outB;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new PngIntegrityException("Unsupported blendOp " + control.blendOp + " at frame " + frameIndex);
|
||||
}
|
||||
|
||||
if (doScale)
|
||||
framePixels[frameIndex] = scale(currentFrameBuffer,
|
||||
width, height,
|
||||
targetWidth, targetHeight);
|
||||
else
|
||||
framePixels[frameIndex] = currentFrameBuffer;
|
||||
|
||||
if (control.delayNumerator == 0) {
|
||||
durations[frameIndex] = 10;
|
||||
} else {
|
||||
int durationsMills = 1000 * control.delayNumerator;
|
||||
if (control.delayDenominator == 0)
|
||||
durationsMills /= 100;
|
||||
else
|
||||
durationsMills /= control.delayDenominator;
|
||||
|
||||
durations[frameIndex] = durationsMills;
|
||||
}
|
||||
|
||||
switch (control.disposeOp) {
|
||||
case 0: // APNG_DISPOST_OP_NONE
|
||||
System.arraycopy(currentFrameBuffer, 0, buffer, 0, currentFrameBuffer.length);
|
||||
break;
|
||||
case 1: // APNG_DISPOSE_OP_BACKGROUND
|
||||
for (int row = 0; row < control.height; row++) {
|
||||
int fromIndex = (control.yOffset + row) * width + control.xOffset;
|
||||
Arrays.fill(buffer, fromIndex, fromIndex + control.width, 0);
|
||||
}
|
||||
break;
|
||||
case 2: // APNG_DISPOSE_OP_PREVIOUS
|
||||
// Do nothing, keep the previous frame.
|
||||
break;
|
||||
default:
|
||||
throw new PngIntegrityException("Unsupported disposeOp " + control.disposeOp + " at frame " + frameIndex);
|
||||
}
|
||||
}
|
||||
|
||||
PngAnimationControl animationControl = sequence.getAnimationControl();
|
||||
int cycleCount;
|
||||
if (animationControl != null) {
|
||||
cycleCount = animationControl.numPlays;
|
||||
if (cycleCount == 0)
|
||||
cycleCount = Timeline.INDEFINITE;
|
||||
} else {
|
||||
cycleCount = Timeline.INDEFINITE;
|
||||
}
|
||||
|
||||
if (doScale)
|
||||
return new AnimationImageImpl(targetWidth, targetHeight, framePixels, durations, cycleCount);
|
||||
else
|
||||
return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
|
||||
}
|
||||
|
||||
private ImageUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.argb8888.*;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.map.PngMap;
|
||||
import org.jackhuang.hmcl.ui.image.apng.map.PngMapReader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.DefaultPngChunkReader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.PngChunkProcessor;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.PngReadHelper;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.PngReader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.util.PngContainer;
|
||||
import org.jackhuang.hmcl.ui.image.apng.util.PngContainerBuilder;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Convenient one liners for loading PNG images.
|
||||
*/
|
||||
public class Png {
|
||||
|
||||
/**
|
||||
* Read the provided stream and produce a PngMap of the data.
|
||||
*
|
||||
* @param is stream to read from
|
||||
* @param sourceName optional name, mainly for debugging.
|
||||
* @return PngMap of the data.
|
||||
* @throws PngException
|
||||
*/
|
||||
public static PngMap readMap(InputStream is, String sourceName) throws PngException {
|
||||
return PngReadHelper.read(is, new PngMapReader(sourceName));
|
||||
}
|
||||
|
||||
public static PngContainer readContainer(InputStream is) throws PngException {
|
||||
return PngReadHelper.read(is, new DefaultPngChunkReader<>(new PngContainerBuilder()));
|
||||
}
|
||||
|
||||
public static <ResultT> ResultT read(InputStream is, PngReader<ResultT> reader) throws PngException {
|
||||
return PngReadHelper.read(is, reader);
|
||||
}
|
||||
|
||||
public static <ResultT> ResultT read(InputStream is, PngChunkProcessor<ResultT> processor) throws PngException {
|
||||
return PngReadHelper.read(is, new DefaultPngChunkReader<>(processor));
|
||||
}
|
||||
|
||||
public static Argb8888Bitmap readArgb8888Bitmap(InputStream is) throws PngException {
|
||||
Argb8888Processor<Argb8888Bitmap> processor = new Argb8888Processor<>(new DefaultImageArgb8888Director());
|
||||
return PngReadHelper.read(is, new DefaultPngChunkReader<>(processor));
|
||||
}
|
||||
|
||||
public static Argb8888BitmapSequence readArgb8888BitmapSequence(InputStream is) throws PngException {
|
||||
Argb8888Processor<Argb8888BitmapSequence> processor = new Argb8888Processor<>(new Argb8888BitmapSequenceDirector());
|
||||
return PngReadHelper.read(is, new DefaultPngChunkReader<>(processor));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
/**
|
||||
* A PNG file has an animation type - most commonly, not animated.
|
||||
* <p>
|
||||
* This is primarily relevant to tracking whether the default image in a PNG
|
||||
* is to be
|
||||
* a) processed as normal (e.g. in a standard or "non-animated" PNG file);
|
||||
* b) processed and used as the first frame in an animated PNG; or
|
||||
* c) not processed and discarded (e.g. in an animated PNG which does not use the first frame).
|
||||
*/
|
||||
public enum PngAnimationType {
|
||||
NOT_ANIMATED,
|
||||
ANIMATED_KEEP_DEFAULT_IMAGE,
|
||||
ANIMATED_DISCARD_DEFAULT_IMAGE;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
/**
|
||||
* One four-letter (32-byte) code identifying the type of a PNG chunk.
|
||||
* <p>
|
||||
* Common chunk codes are defined as constants that can be used in a switch statement.
|
||||
* Users can add their own chunk codes separately.
|
||||
*/
|
||||
public class PngChunkCode {
|
||||
public static final PngChunkCode IHDR = new PngChunkCode(PngConstants.IHDR_VALUE, "IHDR");
|
||||
public static final PngChunkCode PLTE = new PngChunkCode(PngConstants.PLTE_VALUE, "PLTE");
|
||||
public static final PngChunkCode IDAT = new PngChunkCode(PngConstants.IDAT_VALUE, "IDAT");
|
||||
public static final PngChunkCode IEND = new PngChunkCode(PngConstants.IEND_VALUE, "IEND");
|
||||
public static final PngChunkCode gAMA = new PngChunkCode(PngConstants.gAMA_VALUE, "gAMA");
|
||||
public static final PngChunkCode bKGD = new PngChunkCode(PngConstants.bKGD_VALUE, "bKGD");
|
||||
public static final PngChunkCode tRNS = new PngChunkCode(PngConstants.tRNS_VALUE, "tRNS");
|
||||
public static final PngChunkCode acTL = new PngChunkCode(PngConstants.acTL_VALUE, "acTL");
|
||||
public static final PngChunkCode fcTL = new PngChunkCode(PngConstants.fcTL_VALUE, "fcTL");
|
||||
public static final PngChunkCode fdAT = new PngChunkCode(PngConstants.fdAT_VALUE, "fdAT");
|
||||
|
||||
public final int numeric;
|
||||
public final String letters;
|
||||
|
||||
PngChunkCode(int numeric, String letters) {
|
||||
this.numeric = numeric;
|
||||
this.letters = letters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out if the chunk is "critical" as defined by http://www.w3.org/TR/PNG/#5Chunk-naming-conventions
|
||||
*
|
||||
* <pre>
|
||||
* cHNk <-- 32 bit chunk type represented in text form
|
||||
* ||||
|
||||
* |||+- Safe-to-copy bit is 1 (lower case letter; bit 5 is 1)
|
||||
* ||+-- Reserved bit is 0 (upper case letter; bit 5 is 0)
|
||||
* |+--- Private bit is 0 (upper case letter; bit 5 is 0)
|
||||
* +---- Ancillary bit is 1 (lower case letter; bit 5 is 1)
|
||||
* </pre>
|
||||
*
|
||||
* @return true if this chunk is considered critical according to the PNG spec.
|
||||
*/
|
||||
public boolean isCritical() {
|
||||
// Ancillary bit: bit 5 o first byte
|
||||
return (numeric & PngConstants.BIT_CHUNK_IS_ANCILLARY) == 0;
|
||||
}
|
||||
|
||||
public boolean isAncillary() {
|
||||
// Ancillary bit: bit 5 of first byte
|
||||
return (numeric & PngConstants.BIT_CHUNK_IS_ANCILLARY) > 0;
|
||||
}
|
||||
|
||||
public boolean isPrivate() {
|
||||
return (numeric & PngConstants.BIT_CHUNK_IS_PRIVATE) > 0;
|
||||
}
|
||||
|
||||
public boolean isPublic() {
|
||||
return (numeric & PngConstants.BIT_CHUNK_IS_PRIVATE) == 0;
|
||||
}
|
||||
|
||||
public boolean isReserved() {
|
||||
return (numeric & PngConstants.BIT_CHUNK_IS_RESERVED) > 0;
|
||||
}
|
||||
|
||||
public boolean isSafeToCopy() {
|
||||
return (numeric & PngConstants.BIT_CHUNK_IS_SAFE_TO_COPY) > 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PngChunkCode that = (PngChunkCode) o;
|
||||
|
||||
if (numeric != that.numeric) return false;
|
||||
return !(letters != null ? !letters.equals(that.letters) : that.letters != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = numeric;
|
||||
result = 31 * result + (letters != null ? letters.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return letters + "(" + numeric + ")";
|
||||
}
|
||||
|
||||
public static PngChunkCode from(int code) {
|
||||
switch (code) {
|
||||
case PngConstants.IHDR_VALUE:
|
||||
return IHDR;
|
||||
case PngConstants.gAMA_VALUE:
|
||||
return gAMA;
|
||||
case PngConstants.bKGD_VALUE:
|
||||
return bKGD;
|
||||
case PngConstants.tRNS_VALUE:
|
||||
return tRNS;
|
||||
case PngConstants.IDAT_VALUE:
|
||||
return IDAT;
|
||||
case PngConstants.IEND_VALUE:
|
||||
return IEND;
|
||||
case PngConstants.acTL_VALUE:
|
||||
return acTL;
|
||||
case PngConstants.fdAT_VALUE:
|
||||
return fdAT;
|
||||
default:
|
||||
byte[] s = new byte[4];
|
||||
s[0] = (byte) ((code & 0xff000000) >> 24);
|
||||
s[1] = (byte) ((code & 0x00ff0000) >> 16);
|
||||
s[2] = (byte) ((code & 0x0000ff00) >> 8);
|
||||
s[3] = (byte) ((code & 0x000000ff));
|
||||
return new PngChunkCode(code, new String(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
|
||||
/**
|
||||
* PNG images support 5 colour types as defined at http://www.w3.org/TR/PNG/#11IHDR
|
||||
*/
|
||||
public enum PngColourType {
|
||||
PNG_GREYSCALE(0, 1, "1, 2, 4, 8, 16", "Greyscale", "Each pixel is a greyscale sample"),
|
||||
PNG_TRUECOLOUR(2, 3, "8, 16", "Truecolour", "Each pixel is an R,G,B triple"),
|
||||
PNG_INDEXED_COLOUR(3, 1, "1, 2, 4, 8", "Indexed-colour", "Each pixel is a palette index; a PLTE chunk shall appear."),
|
||||
PNG_GREYSCALE_WITH_ALPHA(4, 2, "4, 8, 16", "Greyscale with alpha", "Each pixel is a greyscale sample followed by an alpha sample."),
|
||||
PNG_TRUECOLOUR_WITH_ALPHA(6, 4, "8, 16", "Truecolour with alpha", "Each pixel is an R,G,B triple followed by an alpha sample.");
|
||||
|
||||
public final int code;
|
||||
public final int componentsPerPixel;
|
||||
public final String allowedBitDepths;
|
||||
public final String name;
|
||||
public final String descriptino;
|
||||
|
||||
PngColourType(int code, int componentsPerPixel, String allowedBitDepths, String name, String descriptino) {
|
||||
this.code = code;
|
||||
this.componentsPerPixel = componentsPerPixel;
|
||||
this.allowedBitDepths = allowedBitDepths;
|
||||
this.name = name;
|
||||
this.descriptino = descriptino;
|
||||
}
|
||||
|
||||
public boolean isIndexed() {
|
||||
return (code & 0x01) > 0;
|
||||
}
|
||||
|
||||
public boolean hasAlpha() {
|
||||
return (code & 0x04) > 0;
|
||||
}
|
||||
|
||||
public boolean supportsSubByteDepth() {
|
||||
return code == 0 || code == 3;
|
||||
}
|
||||
|
||||
public static PngColourType fromByte(byte b) throws PngException {
|
||||
switch (b) {
|
||||
case 0:
|
||||
return PNG_GREYSCALE;
|
||||
case 2:
|
||||
return PNG_TRUECOLOUR;
|
||||
case 3:
|
||||
return PNG_INDEXED_COLOUR;
|
||||
case 4:
|
||||
return PNG_GREYSCALE_WITH_ALPHA;
|
||||
case 6:
|
||||
return PNG_TRUECOLOUR_WITH_ALPHA;
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Valid PNG colour types are 0, 2, 3, 4, 6. Type '%d' is invalid", b));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
|
||||
public class PngConstants {
|
||||
|
||||
public static final int LENGTH_SIGNATURE = 8;
|
||||
public static final int LENGTH_fcTL_CHUNK = 4 + 4 + 4 + 4 + 4 + 2 + 2 + 1 + 1;
|
||||
public static final int LENGTH_acTL_CHUNK = 4 + 4;
|
||||
|
||||
public static final byte[] BYTES_SIGNATURE = new byte[]{
|
||||
(byte) 0x89, 'P', 'N', 'G', (byte) 0x0D, (byte) 0x0A, (byte) 0x1A, (byte) 0x0A
|
||||
};
|
||||
public static final int IHDR_VALUE = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R'; // 1229472850;
|
||||
public static final int PLTE_VALUE = 'P' << 24 | 'L' << 16 | 'T' << 8 | 'E';
|
||||
public static final int IDAT_VALUE = 'I' << 24 | 'D' << 16 | 'A' << 8 | 'T'; // 1229209940;
|
||||
public static final int IEND_VALUE = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D'; // 1229278788;
|
||||
public static final int gAMA_VALUE = 'g' << 24 | 'A' << 16 | 'M' << 8 | 'A'; // 1732332865;
|
||||
public static final int bKGD_VALUE = 'b' << 24 | 'K' << 16 | 'G' << 8 | 'D'; // 1649100612;
|
||||
public static final int tRNS_VALUE = 't' << 24 | 'R' << 16 | 'N' << 8 | 'S';
|
||||
public static final int acTL_VALUE = 'a' << 24 | 'c' << 16 | 'T' << 8 | 'L';
|
||||
public static final int fcTL_VALUE = 'f' << 24 | 'c' << 16 | 'T' << 8 | 'L';
|
||||
public static final int fdAT_VALUE = 'f' << 24 | 'd' << 16 | 'A' << 8 | 'T';
|
||||
|
||||
public static final int ERROR_NOT_PNG = 1;
|
||||
public static final int ERROR_EOF = 2;
|
||||
public static final int ERROR_EOF_EXPECTED = 3;
|
||||
public static final int ERROR_UNKNOWN_IO_FAILURE = 4;
|
||||
public static final int ERROR_FEATURE_NOT_SUPPORTED = 5;
|
||||
public static final int ERROR_INTEGRITY = 6;
|
||||
|
||||
public static final int ONE_K = 1 << 10;
|
||||
public static final int DEFAULT_TARGET_BUFFER_SIZE = 32 * ONE_K;
|
||||
|
||||
public static final byte[] GREY_PALETTE_16 = new byte[]{
|
||||
(byte) 0x00, // 0
|
||||
(byte) 0x11, // 1
|
||||
(byte) 0x22, // 2
|
||||
(byte) 0x33, // 3
|
||||
(byte) 0x44, // 4
|
||||
(byte) 0x55, // 5
|
||||
(byte) 0x66, // 6
|
||||
(byte) 0x77, // 7
|
||||
(byte) 0x88, // 8
|
||||
(byte) 0x99, // 9
|
||||
(byte) 0xaa, // 10
|
||||
(byte) 0xbb, // 11
|
||||
(byte) 0xcc, // 12
|
||||
(byte) 0xdd, // 13
|
||||
(byte) 0xee, // 14
|
||||
(byte) 0xff, // 15
|
||||
};
|
||||
|
||||
// public static final byte[] MASKS_1 = new byte[] {
|
||||
// (byte)0x01, // bit 0
|
||||
// (byte)0x02, // bit 1
|
||||
// (byte)0x04, // bit 2
|
||||
// (byte)0x08, // bit 3
|
||||
// (byte)0x10, // bit 4
|
||||
// (byte)0x20, // bit 5
|
||||
// (byte)0x40, // bit 6
|
||||
// (byte)0x80, // bit 7
|
||||
// };
|
||||
//
|
||||
// public static final byte[] MASKS_2 = new byte[] {
|
||||
// (byte)0x03, // bit 0-1
|
||||
// (byte)0x0C, // bit 2-3
|
||||
// (byte)0x30, // bit 4-5
|
||||
// (byte)0xC0, // bit 6-7
|
||||
// };
|
||||
//
|
||||
// public static final byte[] MASKS_4 = new byte[] {
|
||||
// (byte)0x0F, // bit 0-3
|
||||
// (byte)0xF0, // bit 4-7
|
||||
// };
|
||||
|
||||
public static final byte[] SHIFTS_1 = new byte[]{
|
||||
(byte) 0, // bit 0
|
||||
(byte) 1, // bit 1
|
||||
(byte) 2, // bit 2
|
||||
(byte) 3, // bit 3
|
||||
(byte) 4, // bit 4
|
||||
(byte) 5, // bit 5
|
||||
(byte) 6, // bit 6
|
||||
(byte) 7, // bit 7
|
||||
};
|
||||
|
||||
public static final byte[] SHIFTS_2 = new byte[]{
|
||||
(byte) 0, // bit 0-1
|
||||
(byte) 2, // bit 2-3
|
||||
(byte) 4, // bit 4-5
|
||||
(byte) 6 // bit 6-7
|
||||
};
|
||||
|
||||
public static final byte[] SHIFTS_4 = new byte[]{
|
||||
(byte) 0x00, // bit 0-3
|
||||
(byte) 0x04, // bit 4-7
|
||||
};
|
||||
|
||||
public static final int BIT_CHUNK_IS_ANCILLARY = 0x20000000;
|
||||
public static final int BIT_CHUNK_IS_PRIVATE = 0x00200000;
|
||||
public static final int BIT_CHUNK_IS_RESERVED = 0x00002000;
|
||||
public static final int BIT_CHUNK_IS_SAFE_TO_COPY = 0x00000020;
|
||||
|
||||
|
||||
// public static final byte[] SHIFTS_1 = new byte[] {
|
||||
// (byte)0x01, // bit 0
|
||||
// (byte)0x02, // bit 1
|
||||
// (byte)0x04, // bit 2
|
||||
// (byte)0x08, // bit 3
|
||||
// (byte)0x10, // bit 4
|
||||
// (byte)0x20, // bit 5
|
||||
// (byte)0x40, // bit 6
|
||||
// (byte)0x80, // bit 7
|
||||
// };
|
||||
|
||||
//public static final PngChunkCodes Foo = new PngChunkCodes(123, "Foo");
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
/**
|
||||
* Each "undo*" function reverses the effect of a filter on a given single scanline.
|
||||
*/
|
||||
public class PngFilter {
|
||||
|
||||
public static void undoSubFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
|
||||
// int ai = 0;
|
||||
// for (int i=pixelStart+filterUnit; i<pixelEnd; i++) {
|
||||
//
|
||||
// final int x = bytes[i];
|
||||
// final int a = bytes[pixelStart + ai];
|
||||
int ai = pixelStart;
|
||||
for (int i = pixelStart + filterUnit; i < pixelEnd; i++) {
|
||||
|
||||
final int x = bytes[i];
|
||||
final int a = bytes[ai];
|
||||
|
||||
//bytes[rowPosition] = (byte)((bytes[rowPosition] + left) & 0xff); // TODO & 0xff
|
||||
bytes[i] = (byte) ((x + a) & 0xff);
|
||||
|
||||
ai++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void undoUpFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
|
||||
// for (int i=1; i<bytesPerLine; i++) {
|
||||
// rowPosition++; // before the first op to skip filter byte
|
||||
// bytes[rowPosition] = (byte)((bytes[rowPosition] + previousRow[i]) & 0xff);
|
||||
// }
|
||||
int bi = 0;
|
||||
for (int i = pixelStart; i < pixelEnd; i++) {
|
||||
final int x = bytes[i];
|
||||
final int b = previousRow[bi];
|
||||
bytes[i] = (byte) ((x + b) & 0xff);
|
||||
bi++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void undoAverageFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
|
||||
int ai = pixelStart - filterUnit;
|
||||
int bi = 0;
|
||||
for (int i = pixelStart; i < pixelEnd; i++) {
|
||||
final int x = bytes[i];
|
||||
final int a = (ai < pixelStart) ? 0 : (0xff & bytes[ai]);
|
||||
final int b = (0xff & previousRow[bi]);
|
||||
// int ai = pixelStart;
|
||||
// int bi = 0;
|
||||
// for (int i=pixelStart+filterUnit; i<pixelEnd; i++) {
|
||||
// final int x = bytes[i];
|
||||
// final int a = bytes[ai];
|
||||
// final int b = previousRow[bi];
|
||||
|
||||
//bytes[i] = (byte)((x+((a+b)>>1))&0xff);
|
||||
//int z = x+(a+b)/2;
|
||||
int z = x + ((a + b) / 2);
|
||||
bytes[i] = (byte) (0xff & z);
|
||||
ai++;
|
||||
bi++;
|
||||
}
|
||||
|
||||
/*
|
||||
int bi = 0;
|
||||
for (int i=pixelStart; i<pixelStart+filterUnit; i++) {
|
||||
final int x = bytes[i];
|
||||
final int b = previousRow[bi];
|
||||
bytes[i] = (byte)((x + (b >> 1)) & 0xff);
|
||||
bi++;
|
||||
}
|
||||
|
||||
for (int i=pixelStart+filterUnit; i<pixelEnd; i++) {
|
||||
final int x = bytes[i];
|
||||
final int a = bytes[pixelStart+bi-filterUnit];
|
||||
final int b = previousRow[bi];
|
||||
bytes[i] = (byte)((x + ((a+b) >> 1)) & 0xff);
|
||||
bi++;
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* See http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html
|
||||
*
|
||||
*/
|
||||
public static void undoPaethFilter(byte[] bytes, int pixelStart, int pixelEnd, int filterUnit, byte[] previousRow) {
|
||||
//int scratch;
|
||||
//int previousLeft = 0;
|
||||
//int left = 0;
|
||||
|
||||
|
||||
int ai = pixelStart - filterUnit;
|
||||
int bi = 0;
|
||||
int ci = -filterUnit;
|
||||
//for (int i=0; i<bytesPerLine-1; i++) {
|
||||
//for (int i=filterUnit; i<bytesPerLine-1; i++) {
|
||||
for (int i = pixelStart; i < pixelEnd; i++) {
|
||||
|
||||
final int a, b, c, x;
|
||||
x = bytes[i];
|
||||
if (ai < pixelStart) {
|
||||
a = c = 0;
|
||||
} else {
|
||||
a = 0xff & bytes[ai];
|
||||
c = 0xff & previousRow[ci];
|
||||
}
|
||||
b = 0xff & previousRow[bi];
|
||||
final int p = a + b - c;
|
||||
final int pa = p >= a ? p - a : a - p;
|
||||
final int pb = p >= b ? p - b : b - p;
|
||||
final int pc = p >= c ? p - c : c - p;
|
||||
final int predicted = (pa <= pb && pa <= pc) ? a
|
||||
: (pb <= pc) ? b
|
||||
: c;
|
||||
|
||||
bytes[i] = (byte) ((x + predicted) & 0xff);
|
||||
ai++;
|
||||
bi++;
|
||||
ci++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.PngScanlineProcessor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A PngScanlineBuffer allows up-front once-off memory allocation that can be used
|
||||
* across an entire PNG (no matter how many frames) to accumulate and process image data.
|
||||
*/
|
||||
public class PngScanlineBuffer {
|
||||
|
||||
protected final byte[] bytes;
|
||||
protected final int filterUnit;
|
||||
|
||||
protected byte[] previousLine;
|
||||
protected int readPosition;
|
||||
protected int writePosition;
|
||||
//protected int numBytesPerLine;
|
||||
|
||||
public PngScanlineBuffer(int size, int maxBytesPerLine, int filterUnit) {
|
||||
this.bytes = new byte[size];
|
||||
this.previousLine = new byte[maxBytesPerLine];
|
||||
//this.scratchLine = new byte[maxBytesPerLine];
|
||||
//this.numBytesPerLine = numBytesPerLine;
|
||||
this.filterUnit = filterUnit;
|
||||
reset();
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getReadPosition() {
|
||||
return readPosition;
|
||||
}
|
||||
|
||||
public int getWritePosition() {
|
||||
return writePosition;
|
||||
}
|
||||
|
||||
public boolean canRead(int minNumBytes) {
|
||||
return availableRead() >= minNumBytes;
|
||||
}
|
||||
|
||||
public int availableRead() {
|
||||
return writePosition - readPosition;
|
||||
}
|
||||
|
||||
public int availableWrite() {
|
||||
return bytes.length - writePosition;
|
||||
}
|
||||
|
||||
public void skip(int numBytes) {
|
||||
if (numBytes < 0 || numBytes > availableRead()) {
|
||||
throw new IllegalArgumentException(String.format("Skip bytes must be 0 > n >= %d: %d", availableRead(), numBytes));
|
||||
}
|
||||
readPosition += numBytes;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.readPosition = 0;
|
||||
this.writePosition = 0;
|
||||
Arrays.fill(previousLine, (byte) 0);
|
||||
}
|
||||
|
||||
public void shift() {
|
||||
// It is difficult to say whether there is a more efficient approach - e.g. a for loop -
|
||||
// to shifting the bytes to the start of the array. For now, I consider System.arraycopy
|
||||
// to be "good enough".
|
||||
int len = availableRead();
|
||||
System.arraycopy(bytes, readPosition, bytes, 0, len);
|
||||
writePosition = len;
|
||||
readPosition = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the decompressed data into the buffer.
|
||||
*
|
||||
* @param is an InputStream yielding decompressed data.
|
||||
* @return count of how many bytes read, or -1 if EOF hit.
|
||||
* @throws IOException
|
||||
*/
|
||||
public int read(InputStream is) throws IOException {
|
||||
int r = is.read(bytes, writePosition, availableWrite());
|
||||
if (r > 0) {
|
||||
writePosition += r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public boolean decompress(InputStream inputStream, PngScanlineProcessor processor) throws IOException, PngException {
|
||||
int bytesPerLine = processor.getBytesPerLine();
|
||||
try (InputStream iis = processor.makeInflaterInputStream(inputStream)) {
|
||||
while (true) {
|
||||
// Ally says
|
||||
// I LOVE Y.O.U.
|
||||
|
||||
int len = read(iis);
|
||||
if (len <= 0) {
|
||||
return processor.isFinished();
|
||||
}
|
||||
|
||||
// Or could do: len -= bytesPerLine until len < bytesPerLine
|
||||
while (canRead(bytesPerLine)) {
|
||||
undoFilterScanline(bytesPerLine);
|
||||
processor.processScanline(bytes, readPosition + 1);//(), getReadPosition());
|
||||
skip(bytesPerLine);
|
||||
}
|
||||
shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void undoFilterScanline(int bytesPerLine) {
|
||||
int filterCode = bytes[readPosition];
|
||||
switch (filterCode) {
|
||||
case 0:
|
||||
// NOP
|
||||
break;
|
||||
case 1:
|
||||
PngFilter.undoSubFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
|
||||
break;
|
||||
case 2:
|
||||
PngFilter.undoUpFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
|
||||
break;
|
||||
case 3:
|
||||
PngFilter.undoAverageFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
|
||||
break;
|
||||
case 4:
|
||||
PngFilter.undoPaethFilter(bytes, readPosition + 1, readPosition + bytesPerLine, filterUnit, previousLine);
|
||||
break;
|
||||
default: // I toyed with an exception here. But why? Just treat as if no filter.
|
||||
//throw new IllegalArgumentException(String.format("Filter type %d invalid; valid is 0..4", filterCode);
|
||||
break;
|
||||
}
|
||||
|
||||
// when un-filtering scanlines, the previous row is a copy of the *un-filtered* row (not the original).
|
||||
System.arraycopy(bytes, readPosition + 1, previousLine, 0, bytesPerLine - 1); // keep a copy of the unmodified row
|
||||
}
|
||||
|
||||
public static PngScanlineBuffer from(PngHeader header) {
|
||||
return from(header, PngConstants.DEFAULT_TARGET_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
public static PngScanlineBuffer from(PngHeader header, int minBufferSize) {
|
||||
return new PngScanlineBuffer(sizeFrom(header, minBufferSize), header.bytesPerRow, header.filterOffset);
|
||||
}
|
||||
|
||||
public static int sizeFrom(PngHeader header, int minBufferSize) {
|
||||
int bytesPerRow = header.bytesPerRow;
|
||||
int bytesFullBitmap = bytesPerRow * header.height;
|
||||
if (bytesFullBitmap < minBufferSize) {
|
||||
return bytesFullBitmap;
|
||||
}
|
||||
int numRows = Math.max(1, minBufferSize / bytesPerRow);
|
||||
return numRows * bytesPerRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
/**
|
||||
* A bitmap where each pixel is represented by 8-bits for alpha, red, green and blue
|
||||
* respectively, and each pixel stored in a single 32-bit integer. This is expressly designed
|
||||
* to be compatible with the input array used to build Android Bitmap objects, though of course
|
||||
* its use is not limited to that.
|
||||
*/
|
||||
public final class Argb8888Bitmap {
|
||||
public final int[] array;
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
||||
public Argb8888Bitmap(int width, int height) {
|
||||
this(new int[width * height], width, height);
|
||||
}
|
||||
|
||||
public Argb8888Bitmap(int[] array, int width, int height) {//}, int x, int y) {
|
||||
this.array = array;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
// this.x = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Bitmap that shares the byte array of this bitmap.
|
||||
* This is useful when rendering a number of animation frames from an APNG file
|
||||
* and wanting to minimise memory allocation, while still wanting a "new" bitmap
|
||||
* to work on.
|
||||
*
|
||||
* @param width in pixels of "new" bitmap (actual pixel array is the exact array
|
||||
* from the original bitmap).
|
||||
* @param height in pixels of "new" bitmap (actual pixel array is the exact array
|
||||
* from the original bitmap).
|
||||
* @return new bitmap object sharing the same data array.
|
||||
*/
|
||||
public Argb8888Bitmap makeView(int width, int height) {//}, int x, int y) {
|
||||
if ((width * height) > (this.width * this.height)) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Requested width and height (%d x %d) exceeds maximum pixels allowed by host bitmap (%d x %d",
|
||||
width, height, this.width, this.height));
|
||||
}
|
||||
return new Argb8888Bitmap(array, width, height);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An Argb8888BitmapSequence object represents all bitmaps in a single PNG file,
|
||||
* whether it has one and only one default image, any number of frames in an animation
|
||||
* or a default image and a separate set of animation frames.
|
||||
* <p>
|
||||
* Note that instances of this class will hold an individual bitmap for every frame
|
||||
* and does <em>not</em> do composition of images in any way. Composition is done in
|
||||
* the japng_android library using an Android Canvas. This class is not used for that,
|
||||
* as the intermediate Argb8888Bitmap objects are not required, only one bitmap and
|
||||
* the output buffer is required during composition.
|
||||
*/
|
||||
public final class Argb8888BitmapSequence {
|
||||
|
||||
public final PngHeader header;
|
||||
public final Argb8888Bitmap defaultImage;
|
||||
|
||||
private boolean defaultImageIsSet = false;
|
||||
private PngAnimationControl animationControl;
|
||||
List<Frame> animationFrames;
|
||||
|
||||
public Argb8888BitmapSequence(PngHeader header) {
|
||||
this.header = header;
|
||||
this.defaultImage = new Argb8888Bitmap(header.width, header.height);
|
||||
}
|
||||
|
||||
public void receiveAnimationControl(PngAnimationControl animationControl) {
|
||||
this.animationControl = animationControl;
|
||||
this.animationFrames = new ArrayList<>(animationControl.numFrames);
|
||||
}
|
||||
|
||||
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
|
||||
defaultImageIsSet = true;
|
||||
}
|
||||
|
||||
public boolean hasDefaultImage() {
|
||||
return defaultImageIsSet;
|
||||
}
|
||||
|
||||
public boolean isAnimated() {
|
||||
return null != animationControl && animationControl.numFrames > 0;
|
||||
}
|
||||
|
||||
public PngAnimationControl getAnimationControl() {
|
||||
return animationControl;
|
||||
}
|
||||
|
||||
public List<Frame> getAnimationFrames() {
|
||||
return animationFrames;
|
||||
}
|
||||
|
||||
public static final class Frame {
|
||||
public final PngFrameControl control;
|
||||
public final Argb8888Bitmap bitmap;
|
||||
|
||||
public Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
|
||||
this.control = control;
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
|
||||
/**
|
||||
* Argb8888BitmapSequenceDirector instances direct an Argb8888Processor to build all frames
|
||||
* of an animation into an Argb8888BitmapSequence object.
|
||||
*/
|
||||
public class Argb8888BitmapSequenceDirector extends BasicArgb8888Director<Argb8888BitmapSequence> {
|
||||
Argb8888BitmapSequence bitmapSequence = null;
|
||||
PngFrameControl currentFrame = null;
|
||||
private PngHeader header;
|
||||
|
||||
@Override
|
||||
public void receiveHeader(PngHeader header, PngScanlineBuffer buffer) throws PngException {
|
||||
this.header = header;
|
||||
this.bitmapSequence = new Argb8888BitmapSequence(header);
|
||||
//this.header = header;
|
||||
//defaultImage = new Argb8888Bitmap(header.width, header.height);
|
||||
this.scanlineProcessor = Argb8888Processors.from(header, buffer, this.bitmapSequence.defaultImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantDefaultImage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantAnimationFrames() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor beforeDefaultImage() {
|
||||
return scanlineProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
|
||||
this.bitmapSequence.receiveDefaultImage(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveAnimationControl(PngAnimationControl control) {
|
||||
this.bitmapSequence.receiveAnimationControl(control);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control) {
|
||||
//throw new IllegalStateException("TODO up to here");
|
||||
//return null;
|
||||
currentFrame = control;
|
||||
|
||||
//System.out.println("Frame: "+control);
|
||||
|
||||
return scanlineProcessor.cloneWithNewBitmap(header.adjustFor(control)); // TODO: is this going to be a problem?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveFrameImage(Argb8888Bitmap bitmap) {
|
||||
if (null == currentFrame) {
|
||||
throw new IllegalStateException("Received a frame image with no frame control in place");
|
||||
}
|
||||
if (null == bitmapSequence.animationFrames) {
|
||||
throw new IllegalStateException("Received a frame image without animation control (or frame list?) in place");
|
||||
}
|
||||
bitmapSequence.animationFrames.add(new Argb8888BitmapSequence.Frame(currentFrame, bitmap));
|
||||
currentFrame = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888BitmapSequence getResult() {
|
||||
return bitmapSequence;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
|
||||
/**
|
||||
* Argb8888Director implementations "direct" a given Argb8888Processor how to
|
||||
* control the output. This allows the Argb8888Processor to transform pixels into
|
||||
* ARGB8888 format while allowing for radically different final output objects,
|
||||
* e.g. a single bitmap, a sequence of bitamps, an Android View or Drawable, etc.
|
||||
* <p>
|
||||
* TODO: not sure if this will stay in this form. Needs refinement.
|
||||
*/
|
||||
public interface Argb8888Director<ResultT> {
|
||||
|
||||
void receiveHeader(PngHeader header, PngScanlineBuffer buffer) throws PngException;
|
||||
|
||||
void receivePalette(Argb8888Palette palette);
|
||||
|
||||
void processTransparentPalette(byte[] bytes, int position, int length) throws PngException;
|
||||
|
||||
void processTransparentGreyscale(byte k1, byte k0) throws PngException;
|
||||
|
||||
void processTransparentTruecolour(byte r1, byte r0, byte g1, byte g0, byte b1, byte b0) throws PngException;
|
||||
|
||||
boolean wantDefaultImage();
|
||||
|
||||
boolean wantAnimationFrames();
|
||||
|
||||
Argb8888ScanlineProcessor beforeDefaultImage();
|
||||
|
||||
void receiveDefaultImage(Argb8888Bitmap bitmap);
|
||||
|
||||
void receiveAnimationControl(PngAnimationControl control);
|
||||
|
||||
Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control);
|
||||
|
||||
void receiveFrameImage(Argb8888Bitmap bitmap);
|
||||
|
||||
ResultT getResult();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
|
||||
/**
|
||||
* A palette implementation representing colours in ARGB8888 format as 32-bit integers.
|
||||
* <p>
|
||||
* Note that the transparency of individual entries in the palette is modified in-place
|
||||
* when the trNS chunk is processed.
|
||||
*
|
||||
* @see BasicArgb8888Director
|
||||
*/
|
||||
public class Argb8888Palette {
|
||||
protected static Argb8888Palette monochromePalette = null;
|
||||
protected static Argb8888Palette greyPalette2 = null;
|
||||
protected static Argb8888Palette greyPalette4 = null;
|
||||
protected static Argb8888Palette greyPalette8 = null;
|
||||
|
||||
/**
|
||||
* The colour array is public and mutable by design.
|
||||
*
|
||||
* @see this.get()
|
||||
*/
|
||||
public final int[] argbArray;
|
||||
|
||||
/**
|
||||
* Construct a new palette using the specific array passed as the argument as-is (not copied).
|
||||
*
|
||||
* @param argbArray array of colours to use as-is (it is not copied).
|
||||
*/
|
||||
public Argb8888Palette(int[] argbArray) {
|
||||
this.argbArray = argbArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a colour using a method. Users can also just use the argbArray directly.
|
||||
*
|
||||
* @param index of colour to retrieve
|
||||
* @return integer representing the colour at that index.
|
||||
*/
|
||||
public int get(int index) {
|
||||
return this.argbArray[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of colours in this palette, which is the same thing as the size of the array.
|
||||
*/
|
||||
public int size() {
|
||||
return argbArray.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new palette by reading the byte array (from the offset position and for length bytes)
|
||||
* as provided in a PLTE chunk.
|
||||
*
|
||||
* @param bytes array to read data from
|
||||
* @param position offset into bytes array to begin reading from
|
||||
* @param length number of bytes to read from bytes array
|
||||
* @return new palette formed by reading the bytes array.
|
||||
* @throws PngException
|
||||
*/
|
||||
public static Argb8888Palette fromPaletteBytes(byte[] bytes, int position, int length) throws PngException {
|
||||
int numColours = length / 3; // guaranteed to be divisible by 3
|
||||
int[] argbArray = new int[numColours];
|
||||
int srcIndex = position;
|
||||
int alpha = 0xff << 24;
|
||||
for (int destIndex = 0; destIndex < numColours; destIndex++) {
|
||||
final int r = bytes[srcIndex++] & 0xff;
|
||||
final int g = bytes[srcIndex++] & 0xff;
|
||||
final int b = bytes[srcIndex++] & 0xff;
|
||||
argbArray[destIndex] = alpha | r << 16 | g << 8 | b;
|
||||
}
|
||||
return new Argb8888Palette(argbArray);
|
||||
}
|
||||
|
||||
public static Argb8888Palette forGreyscale(int numEntries, int step) {
|
||||
int[] array = new int[numEntries];
|
||||
int alpha = 0xff << 24;
|
||||
int grey = 0;
|
||||
for (int i = 0; i < numEntries; i++) {
|
||||
array[i] = alpha | grey << 16 | grey << 8 | grey;
|
||||
grey = (grey + step) & 0xff;
|
||||
}
|
||||
return new Argb8888Palette(array);
|
||||
}
|
||||
|
||||
public static Argb8888Palette forGreyscale(int bitDepth) throws PngException {
|
||||
switch (bitDepth) {
|
||||
case 1:
|
||||
if (null == monochromePalette) {
|
||||
monochromePalette = forGreyscale(2, 0xff); // Worth it or not really?
|
||||
}
|
||||
return monochromePalette;
|
||||
case 2:
|
||||
if (null == greyPalette2) {
|
||||
greyPalette2 = forGreyscale(4, 0x55);
|
||||
}
|
||||
return greyPalette2;
|
||||
case 4:
|
||||
if (null == greyPalette4) {
|
||||
greyPalette4 = forGreyscale(16, 0x11);
|
||||
}
|
||||
return greyPalette4;
|
||||
case 8: // TODO: need??
|
||||
if (null == greyPalette8) {
|
||||
greyPalette8 = forGreyscale(256, 0x01);
|
||||
}
|
||||
return greyPalette8;
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngChunkCode;
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngGamma;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.map.PngChunkMap;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.PngChunkProcessor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Concrete implementation of a chunk processor designed to process pixels into
|
||||
* 32-bit ARGB8888 format.
|
||||
*/
|
||||
public class Argb8888Processor<ResultT> implements PngChunkProcessor<ResultT> { //PngChunkProcessor<Argb8888Bitmap> {
|
||||
|
||||
protected PngHeader header = null;
|
||||
protected PngScanlineBuffer scanlineReader = null;
|
||||
protected Argb8888Director<ResultT> builder = null;
|
||||
protected Argb8888ScanlineProcessor scanlineProcessor = null;
|
||||
|
||||
|
||||
public Argb8888Processor(Argb8888Director<ResultT> builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processHeader(PngHeader header) throws PngException {
|
||||
// super.processHeader(header);
|
||||
//// if (header.bitDepth != 1 || header.colourType != PngColourType.PNG_GREYSCALE) {
|
||||
//// throw new PngFeatureException("ARGB888 only supports 1-bit greyscale");
|
||||
//// }
|
||||
// if (header.isGreyscale()) {
|
||||
// palette = Argb8888Palette.forGreyscale(header.bitDepth);
|
||||
// }
|
||||
// scanlineReader = PngScanlineBuffer.from(header);
|
||||
this.header = header;
|
||||
this.scanlineReader = PngScanlineBuffer.from(header);
|
||||
this.builder.receiveHeader(this.header, this.scanlineReader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processGamma(PngGamma gamma) throws PngException {
|
||||
// No gamma processing is done at the moment.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processPalette(byte[] bytes, int position, int length) throws PngException {
|
||||
//palette = Argb8888Palette.fromPaletteBytes(bytes, position, length);
|
||||
//scanlineProcessor = Argb8888Processors.fromPalette(this.header, palette);
|
||||
builder.receivePalette(Argb8888Palette.fromPaletteBytes(bytes, position, length));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransparency(byte[] bytes, int position, int length) throws PngException {
|
||||
switch (header.colourType) {
|
||||
case PNG_GREYSCALE: // colour type 0
|
||||
// grey sample value (2 bytes)
|
||||
if (length != 2) {
|
||||
throw new PngIntegrityException(String.format("tRNS chunk for greyscale image must be exactly length=2, not %d", length));
|
||||
}
|
||||
builder.processTransparentGreyscale(bytes[0], bytes[1]);
|
||||
break;
|
||||
|
||||
case PNG_TRUECOLOUR: // colour type 2
|
||||
// red, green, blue samples, EACH with two bytes (16-bits)
|
||||
builder.processTransparentTruecolour(bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]);
|
||||
break;
|
||||
|
||||
case PNG_INDEXED_COLOUR: // colour type 3
|
||||
// This is a sequence of one-byte alpha values to apply to each palette entry starting at zero.
|
||||
// The number of entries may be less than the size of the palette, but not more.
|
||||
builder.processTransparentPalette(bytes, position, length);
|
||||
break;
|
||||
|
||||
case PNG_GREYSCALE_WITH_ALPHA:
|
||||
case PNG_TRUECOLOUR_WITH_ALPHA:
|
||||
default:
|
||||
throw new PngIntegrityException("Illegal to have tRNS chunk with image type " + header.colourType.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The only supported animation type is not "NOT_ANIMATED".
|
||||
*/
|
||||
// @Override
|
||||
// public PngAnimationType chooseApngImageType(PngAnimationType type, PngFrameControl currentFrame) throws PngException {
|
||||
// scanlineProcessor = Argb8888ScanlineProcessor.from(header, scanlineReader, currentFrame);
|
||||
// return PngAnimationType.NOT_ANIMATED;
|
||||
// }
|
||||
@Override
|
||||
public void processDefaultImageData(InputStream inputStream, PngChunkCode code, int position, int length) throws IOException, PngException {
|
||||
if (!builder.wantDefaultImage()) {
|
||||
inputStream.skip(length); // important!
|
||||
return;
|
||||
}
|
||||
|
||||
if (null == scanlineProcessor) { // TODO: is that a good enough metric? Or could be numIdat==0?
|
||||
scanlineProcessor = builder.beforeDefaultImage();
|
||||
if (null == scanlineProcessor) throw new IllegalStateException("Builder must create scanline processor");
|
||||
}
|
||||
|
||||
if (scanlineReader.decompress(inputStream, scanlineProcessor)) {
|
||||
// If here, the image is fully decompressed.
|
||||
builder.receiveDefaultImage(scanlineProcessor.getBitmap());
|
||||
scanlineProcessor = null;
|
||||
scanlineReader.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAnimationControl(PngAnimationControl animationControl) throws PngException {
|
||||
if (builder.wantAnimationFrames()) {
|
||||
builder.receiveAnimationControl(animationControl);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFrameControl(PngFrameControl frameControl) throws PngException {
|
||||
if (!builder.wantAnimationFrames()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null == scanlineProcessor) {
|
||||
// If here, the data in this frame image has not started
|
||||
scanlineProcessor = builder.receiveFrameControl(frameControl);
|
||||
if (null == scanlineProcessor)
|
||||
throw new IllegalStateException("Builder must create scanline processor for frame");
|
||||
} else {
|
||||
throw new IllegalStateException("received animation frame control but image data was in progress");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFrameImageData(InputStream inputStream, PngChunkCode code, int position, int length) throws IOException, PngException {
|
||||
//throw new PngFeatureException("PngArgb8888Processor does not support animation frames");
|
||||
if (!builder.wantAnimationFrames()) {
|
||||
inputStream.skip(length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (null == scanlineProcessor) {
|
||||
throw new IllegalStateException("received animation frame image data before frame control or without processor in place");
|
||||
}
|
||||
|
||||
if (scanlineReader.decompress(inputStream, scanlineProcessor)) {
|
||||
// If here, the image is fully decompressed.
|
||||
builder.receiveFrameImage(scanlineProcessor.getBitmap());
|
||||
scanlineReader.reset();
|
||||
scanlineProcessor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processChunkMapItem(PngChunkMap chunkMapItem) throws PngException {
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultT getResult() {
|
||||
//return scanlineProcessor.getBitmap();
|
||||
return builder.getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngConstants;
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngFeatureException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
|
||||
/**
|
||||
* A series of scanline processor implementations for different input pixel formats,
|
||||
* each able to transform the input into ARGB8888 output pixels.
|
||||
*/
|
||||
public class Argb8888Processors {
|
||||
|
||||
/**
|
||||
* Determine from the header the concrete Argb8888Processor implementation the
|
||||
* caller needs to transform the incoming pixels into the ARGB8888 pixel format.
|
||||
*
|
||||
* @param header of the image being loaded
|
||||
* @param scanlineReader the scanline buffer being used
|
||||
* @param bitmap the destination bitmap
|
||||
* @return a concrete Argb8888Processor to transform source pixels into the specific bitmap.
|
||||
* @throws PngException if there is a feature not supported or some specification break with the file.
|
||||
*/
|
||||
public static Argb8888ScanlineProcessor from(PngHeader header, PngScanlineBuffer scanlineReader, Argb8888Bitmap bitmap) throws PngException {
|
||||
|
||||
int bytesPerScanline = header.bytesPerRow;
|
||||
switch (header.colourType) {
|
||||
case PNG_GREYSCALE:
|
||||
switch (header.bitDepth) {
|
||||
case 1:
|
||||
return new IndexedColourBits(bytesPerScanline, bitmap, 7, 0x01, PngConstants.SHIFTS_1, Argb8888Palette.forGreyscale(1));
|
||||
case 2:
|
||||
return new IndexedColourBits(bytesPerScanline, bitmap, 3, 0x03, PngConstants.SHIFTS_2, Argb8888Palette.forGreyscale(2));
|
||||
case 4:
|
||||
return new IndexedColourBits(bytesPerScanline, bitmap, 1, 0x0F, PngConstants.SHIFTS_4, Argb8888Palette.forGreyscale(4));
|
||||
case 8:
|
||||
return new Greyscale8(bytesPerScanline, bitmap);
|
||||
case 16:
|
||||
throw new PngFeatureException("Greyscale supports 1, 2, 4, 8 but not 16.");
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Invalid greyscale bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
|
||||
}
|
||||
|
||||
case PNG_GREYSCALE_WITH_ALPHA:
|
||||
switch (header.bitDepth) {
|
||||
case 4:
|
||||
return new Greyscale4Alpha(bytesPerScanline, bitmap);
|
||||
case 8:
|
||||
return new Greyscale8Alpha(bytesPerScanline, bitmap);
|
||||
case 16:
|
||||
return new Greyscale16Alpha(bytesPerScanline, bitmap);
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Invalid greyscale-with-alpha bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
|
||||
}
|
||||
|
||||
case PNG_INDEXED_COLOUR:
|
||||
switch (header.bitDepth) {
|
||||
case 1:
|
||||
return new IndexedColourBits(bytesPerScanline, bitmap, 7, 0x01, PngConstants.SHIFTS_1);
|
||||
case 2:
|
||||
return new IndexedColourBits(bytesPerScanline, bitmap, 3, 0x03, PngConstants.SHIFTS_2);
|
||||
case 4:
|
||||
return new IndexedColourBits(bytesPerScanline, bitmap, 1, 0x0F, PngConstants.SHIFTS_4);
|
||||
case 8:
|
||||
return new IndexedColour8(bytesPerScanline, bitmap);
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Invalid indexed colour bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
|
||||
}
|
||||
|
||||
case PNG_TRUECOLOUR:
|
||||
switch (header.bitDepth) {
|
||||
case 8:
|
||||
return new Truecolour8(bytesPerScanline, bitmap);
|
||||
case 16:
|
||||
return new Truecolour16(bytesPerScanline, bitmap);
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Invalid truecolour bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
|
||||
|
||||
}
|
||||
|
||||
case PNG_TRUECOLOUR_WITH_ALPHA:
|
||||
switch (header.bitDepth) {
|
||||
case 8:
|
||||
return new Truecolour8Alpha(bytesPerScanline, bitmap);
|
||||
case 16:
|
||||
return new Truecolour16Alpha(bytesPerScanline, bitmap);
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Invalid truecolour with alpha bit-depth: %d", header.bitDepth)); // TODO: should be in header parse.
|
||||
|
||||
}
|
||||
|
||||
default:
|
||||
throw new PngFeatureException("ARGB8888 doesn't support PNG mode " + header.colourType.name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms 1-, 2-, 4-bit indexed colour source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class IndexedColourBits extends Argb8888ScanlineProcessor {
|
||||
|
||||
private int highBit;
|
||||
private int mask;
|
||||
private byte[] shifts;
|
||||
|
||||
public IndexedColourBits(int bytesPerScanline, Argb8888Bitmap bitmap, int highBit, int mask, byte[] shifts) {
|
||||
this(bytesPerScanline, bitmap, highBit, mask, shifts, null);
|
||||
}
|
||||
|
||||
public IndexedColourBits(int bytesPerScanline, Argb8888Bitmap bitmap, int highBit, int mask, byte[] shifts, Argb8888Palette palette) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
this.highBit = highBit;
|
||||
this.mask = mask;
|
||||
this.shifts = shifts;
|
||||
this.palette = palette;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
int writePosition = this.y * width;
|
||||
int lastWritePosition = writePosition + width;
|
||||
int bit = highBit;
|
||||
|
||||
while (writePosition < lastWritePosition) {
|
||||
//final int index=(srcBytes[srcPosition+x] & masks[bit]) >> shifts[bit];
|
||||
//final int index = (srcBytes[srcPosition])
|
||||
//final int v = srcBytes[srcPosition];
|
||||
final int index = mask & (srcBytes[srcPosition] >> shifts[bit]);
|
||||
|
||||
int dest;
|
||||
if (palette != null) {
|
||||
dest = palette.argbArray[index];
|
||||
} else {
|
||||
dest = 0;
|
||||
}
|
||||
destArray[writePosition++] = dest;
|
||||
if (bit == 0) {
|
||||
srcPosition++;
|
||||
bit = highBit;
|
||||
} else {
|
||||
bit--;
|
||||
}
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new IndexedColourBits(bytesPerRow, bitmap, highBit, mask, shifts, palette);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case implementation to transform 8-bit indexed colour source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class IndexedColour8 extends Argb8888ScanlineProcessor {
|
||||
|
||||
public IndexedColour8(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
|
||||
final int index = 0xff & srcBytes[srcPosition++]; // TODO: need to use transparency and background chunks
|
||||
|
||||
int dest;
|
||||
if (palette != null) {
|
||||
dest = palette.argbArray[index];
|
||||
} else {
|
||||
dest = 0;
|
||||
}
|
||||
destArray[writePosition++] = dest;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new IndexedColour8(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms 4-bit greyscale with alpha source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Greyscale4Alpha extends Argb8888ScanlineProcessor {
|
||||
|
||||
public Greyscale4Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int v = srcBytes[srcPosition++];
|
||||
final int k = PngConstants.GREY_PALETTE_16[0x0f & (v >> 4)];
|
||||
final int a = PngConstants.GREY_PALETTE_16[0x0f & v];
|
||||
final int c = a << 24 | k << 16 | k << 8 | k;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Greyscale4Alpha(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms 8-bit greyscale source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Greyscale8 extends Argb8888ScanlineProcessor {
|
||||
|
||||
boolean haveTransparent = false;
|
||||
int transparentSample = 0;
|
||||
|
||||
public Greyscale8(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int sample = 0xff & srcBytes[srcPosition++];
|
||||
final int k = (haveTransparent && sample == transparentSample) ? transparentSample : sample;
|
||||
final int c = alpha | k << 16 | k << 8 | k;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransparentGreyscale(byte k1, byte k0) {
|
||||
// According to below, when image is less than 16-bits per pixel, use least significant byte
|
||||
// http://www.w3.org/TR/PNG/#11transinfo
|
||||
haveTransparent = true;
|
||||
transparentSample = 0xff & k0; // NOT k1 according to http://www.w3.org/TR/PNG/#11transinfo
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Greyscale8(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms 8-bit greyscale with alpha source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Greyscale8Alpha extends Argb8888ScanlineProcessor {
|
||||
|
||||
public Greyscale8Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int k = 0xff & srcBytes[srcPosition++];
|
||||
final int a = 0xff & srcBytes[srcPosition++];
|
||||
final int c = a << 24 | k << 16 | k << 8 | k;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Greyscale8Alpha(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms 16-bit greyscale with alpha source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Greyscale16Alpha extends Argb8888ScanlineProcessor {
|
||||
|
||||
public Greyscale16Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int k = 0xff & srcBytes[srcPosition++];
|
||||
srcPosition++; // skip the least-significant byte of the grey
|
||||
final int a = 0xff & srcBytes[srcPosition++];
|
||||
srcPosition++; // skip the least-significant byte of the alpha
|
||||
final int c = a << 24 | k << 16 | k << 8 | k;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Greyscale16Alpha(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms true-colour (RGB) 8-bit source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Truecolour8 extends Argb8888ScanlineProcessor {
|
||||
public Truecolour8(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int r = 0xff & srcBytes[srcPosition++];
|
||||
final int g = 0xff & srcBytes[srcPosition++];
|
||||
final int b = 0xff & srcBytes[srcPosition++];
|
||||
final int c = alpha | r << 16 | g << 8 | b;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Truecolour8(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms true-colour with alpha (RGBA) 8-bit source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Truecolour8Alpha extends Argb8888ScanlineProcessor {
|
||||
|
||||
public Truecolour8Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int r = 0xff & srcBytes[srcPosition++];
|
||||
final int g = 0xff & srcBytes[srcPosition++];
|
||||
final int b = 0xff & srcBytes[srcPosition++];
|
||||
final int a = 0xff & srcBytes[srcPosition++];
|
||||
final int c = a << 24 | r << 16 | g << 8 | b;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Truecolour8Alpha(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms true-colour (RGB) 16-bit source pixels to ARGB8888 pixels.
|
||||
*/
|
||||
public static class Truecolour16 extends Argb8888ScanlineProcessor {
|
||||
public Truecolour16(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int r = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // skip the byte just read and the least significant byte of the next
|
||||
final int g = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // ditto
|
||||
final int b = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // ditto again
|
||||
final int c = alpha | r << 16 | g << 8 | b;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Truecolour16(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms true-colour with alpha (RGBA) 16-bit source pixels to ARGB8888 pixels.
|
||||
* <p>
|
||||
* Note that the simpler method of resampling the colour is done, namely discard the LSB.
|
||||
*/
|
||||
public static class Truecolour16Alpha extends Argb8888ScanlineProcessor {
|
||||
public Truecolour16Alpha(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline, bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
//final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int r = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // skip the byte just read and the least significant byte of the next
|
||||
final int g = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // ditto
|
||||
final int b = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // ditto again
|
||||
final int alpha = 0xff & srcBytes[srcPosition];
|
||||
srcPosition += 2; // skip the byte just read and the least significant byte of the next
|
||||
final int c = alpha << 24 | r << 16 | g << 8 | b;
|
||||
destArray[writePosition++] = c;
|
||||
}
|
||||
this.y++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
return new Truecolour16Alpha(bytesPerRow, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private Argb8888Processors() {
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.BasicScanlineProcessor;
|
||||
|
||||
/**
|
||||
* Base implementation that transforms scanlines of source pixels into scanlines of
|
||||
* ARGB8888 (32-bit integer) pixels in a destination Argb8888Bitmap object.
|
||||
* <p>
|
||||
* Note: I wonder if a better name is ScanlineConverter or PixelTransformer.
|
||||
*
|
||||
* @see Argb8888Processors
|
||||
*/
|
||||
public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
|
||||
protected Argb8888Bitmap bitmap;
|
||||
protected Argb8888Palette palette;
|
||||
protected int y = 0;
|
||||
|
||||
// public Argb8888ScanlineProcessor(PngHeader header) {
|
||||
// this(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
|
||||
// }
|
||||
|
||||
public Argb8888ScanlineProcessor(int bytesPerScanline, Argb8888Bitmap bitmap) {
|
||||
super(bytesPerScanline);
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
|
||||
// public Argb8888ScanlineProcessor(PngHeader header, PngScanlineBuffer scanlineReader, PngFrameControl currentFrame) {
|
||||
// super(header, scanlineReader, currentFrame);
|
||||
// bitmap = new Argb8888Bitmap(this.header.width, this.header.height);
|
||||
// }
|
||||
|
||||
public Argb8888Bitmap getBitmap() {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public Argb8888Palette getPalette() {
|
||||
return palette;
|
||||
}
|
||||
|
||||
public void setPalette(Argb8888Palette palette) {
|
||||
this.palette = palette;
|
||||
}
|
||||
|
||||
/**
|
||||
* When processing a sequence of frames a caller may want to write to a completely
|
||||
* new bitmap for every frame or re-use the bytes in an existing bitmap.
|
||||
*
|
||||
* @param header of image to use as basis for calculating image size.
|
||||
* @return processor that will write to a new bitmap.
|
||||
*/
|
||||
public Argb8888ScanlineProcessor cloneWithNewBitmap(PngHeader header) {
|
||||
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
|
||||
if (this.palette != null) {
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray));
|
||||
}
|
||||
return cloned;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When processing a sequence of frames a caller may want to write to a completely
|
||||
* new bitmap for every frame or re-use the bytes in an existing bitmap.
|
||||
*
|
||||
* @param header of image to use as basis for calculating image size.
|
||||
* @return processor that will write to the same bitmap as the current processor.
|
||||
*/
|
||||
public Argb8888ScanlineProcessor cloneWithSharedBitmap(PngHeader header) {
|
||||
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, bitmap.makeView(header.width, header.height));
|
||||
if (this.palette != null) {
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray));
|
||||
}
|
||||
return cloned;
|
||||
|
||||
}
|
||||
|
||||
// public Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap) {
|
||||
// throw new IllegalStateException("TODO: override");
|
||||
// }
|
||||
abstract Argb8888ScanlineProcessor clone(int bytesPerRow, Argb8888Bitmap bitmap);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
|
||||
/**
|
||||
* Common functionality for Argb8888Director implementations.
|
||||
*/
|
||||
public abstract class BasicArgb8888Director<ResultT> implements Argb8888Director<ResultT> {
|
||||
protected Argb8888ScanlineProcessor scanlineProcessor;
|
||||
|
||||
@Override
|
||||
public void receivePalette(Argb8888Palette palette) {
|
||||
scanlineProcessor.setPalette(palette);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransparentPalette(byte[] bytes, int position, int length) throws PngException {
|
||||
Argb8888Palette palette = scanlineProcessor.getPalette();
|
||||
if (null == palette) {
|
||||
throw new PngIntegrityException("Received tRNS data but no palette is in place");
|
||||
}
|
||||
if (length <= 0 || length > palette.size()) {
|
||||
throw new PngIntegrityException(String.format("Received tRNS data length is invalid. Should be >1 && < %d but is %d", palette.size(), length));
|
||||
}
|
||||
for (int i = 0; i < length; i++) {
|
||||
final int alpha = 0xff & bytes[position + i];
|
||||
palette.argbArray[i] = alpha << 24 | palette.argbArray[i] & 0x00FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransparentGreyscale(byte k1, byte k0) throws PngException {
|
||||
scanlineProcessor.processTransparentGreyscale(k1, k0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransparentTruecolour(byte r1, byte r0, byte g1, byte g0, byte b1, byte b0) throws PngException {
|
||||
scanlineProcessor.processTransparentTruecolour(r1, r0, g1, g0, b1, b0);
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngScanlineBuffer;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngAnimationControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.chunks.PngHeader;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
|
||||
/**
|
||||
* This will build a single bitmap: the default image within the PNG file.
|
||||
* Any animation data (or any other data) will not be processed.
|
||||
*/
|
||||
public class DefaultImageArgb8888Director extends BasicArgb8888Director<Argb8888Bitmap> {
|
||||
|
||||
protected Argb8888Bitmap defaultImage;
|
||||
|
||||
@Override
|
||||
public void receiveHeader(PngHeader header, PngScanlineBuffer buffer) throws PngException {
|
||||
defaultImage = new Argb8888Bitmap(header.width, header.height);
|
||||
scanlineProcessor = Argb8888Processors.from(header, buffer, defaultImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantDefaultImage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantAnimationFrames() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor beforeDefaultImage() {
|
||||
return scanlineProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveAnimationControl(PngAnimationControl control) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888ScanlineProcessor receiveFrameControl(PngFrameControl control) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveFrameImage(Argb8888Bitmap bitmap) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Argb8888Bitmap getResult() {
|
||||
return defaultImage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.chunks;
|
||||
|
||||
/**
|
||||
* A PngAnimationControl object contains data parsed from the ``acTL``
|
||||
* animation control chunk of an animated PNG file.
|
||||
*/
|
||||
public class PngAnimationControl {
|
||||
public final int numFrames;
|
||||
public final int numPlays;
|
||||
|
||||
public PngAnimationControl(int numFrames, int numPlays) {
|
||||
this.numFrames = numFrames;
|
||||
this.numPlays = numPlays;
|
||||
}
|
||||
|
||||
public boolean loopForever() {
|
||||
return 0 == numPlays;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PngAnimationControl{" +
|
||||
"numFrames=" + numFrames +
|
||||
", numPlays=" + numPlays +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.chunks;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.map.PngChunkMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A PngFrameControl object contains data parsed from the ``fcTL`` chunk data
|
||||
* in an animated PNG File.
|
||||
* <p>
|
||||
* See https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
|
||||
* <pre>
|
||||
* 0 sequence_number (unsigned int) Sequence number of the animation chunk, starting from 0
|
||||
* 4 width (unsigned int) Width of the following frame
|
||||
* 8 height (unsigned int) Height of the following frame
|
||||
* 12 x_offset (unsigned int) X position at which to render the following frame
|
||||
* 16 y_offset (unsigned int) Y position at which to render the following frame
|
||||
* 20 delay_num (unsigned short) Frame delay fraction numerator
|
||||
* 22 delay_den (unsigned short) Frame delay fraction denominator
|
||||
* 24 dispose_op (byte) Type of frame area disposal to be done after rendering this frame
|
||||
* 25 blend_op (byte) Type of frame area rendering for this frame
|
||||
* </pre>
|
||||
* <p>
|
||||
* Delay denominator: from spec, "if denominator is zero it should be treated as 100ths of second".
|
||||
* <pre>
|
||||
* dispose op:
|
||||
* value
|
||||
* 0 APNG_DISPOSE_OP_NONE
|
||||
* 1 APNG_DISPOSE_OP_BACKGROUND
|
||||
* 2 APNG_DISPOSE_OP_PREVIOUS
|
||||
*
|
||||
* blend op:
|
||||
* value
|
||||
* 0 APNG_BLEND_OP_SOURCE
|
||||
* 1 APNG_BLEND_OP_OVER
|
||||
* </pre>
|
||||
*/
|
||||
public class PngFrameControl {
|
||||
public final int sequenceNumber;
|
||||
public final int width;
|
||||
public final int height;
|
||||
public final int xOffset;
|
||||
public final int yOffset;
|
||||
public final short delayNumerator;
|
||||
public final short delayDenominator;
|
||||
public final byte disposeOp;
|
||||
public final byte blendOp;
|
||||
List<PngChunkMap> imageChunks = new ArrayList<>(1); // TODO: this may be removed
|
||||
|
||||
public PngFrameControl(int sequenceNumber, int width, int height, int xOffset, int yOffset, short delayNumerator, short delayDenominator, byte disposeOp, byte blendOp) {
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.xOffset = xOffset;
|
||||
this.yOffset = yOffset;
|
||||
this.delayNumerator = delayNumerator;
|
||||
this.delayDenominator = delayDenominator == 0 ? 100 : delayDenominator; // APNG spec says zero === 100.
|
||||
this.disposeOp = disposeOp;
|
||||
this.blendOp = blendOp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return number of milliseconds to show this frame for
|
||||
*/
|
||||
public int getDelayMilliseconds() {
|
||||
if (delayDenominator == 1000) {
|
||||
return delayNumerator;
|
||||
} else {
|
||||
// if denom is 100 then need to multiple by 10
|
||||
float f = 1000 / delayDenominator; // 1000/100 -> 10
|
||||
return (int) (delayNumerator * f);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: can this be removed?
|
||||
public void appendImageData(PngChunkMap chunkMap) {
|
||||
imageChunks.add(chunkMap);
|
||||
}
|
||||
|
||||
// TODO: this may be removed
|
||||
public List<PngChunkMap> getImageChunks() {
|
||||
return imageChunks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PngFrameControl{" +
|
||||
"sequenceNumber=" + sequenceNumber +
|
||||
", width=" + width +
|
||||
", height=" + height +
|
||||
", xOffset=" + xOffset +
|
||||
", yOffset=" + yOffset +
|
||||
", delayNumerator=" + delayNumerator +
|
||||
", delayDenominator=" + delayDenominator +
|
||||
", disposeOp=" + disposeOp +
|
||||
", blendOp=" + blendOp +
|
||||
'}';
|
||||
}
|
||||
|
||||
// mainly for ease in unit testing
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PngFrameControl that = (PngFrameControl) o;
|
||||
|
||||
if (sequenceNumber != that.sequenceNumber) return false;
|
||||
if (width != that.width) return false;
|
||||
if (height != that.height) return false;
|
||||
if (xOffset != that.xOffset) return false;
|
||||
if (yOffset != that.yOffset) return false;
|
||||
if (delayNumerator != that.delayNumerator) return false;
|
||||
if (delayDenominator != that.delayDenominator) return false;
|
||||
if (disposeOp != that.disposeOp) return false;
|
||||
return blendOp == that.blendOp;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = sequenceNumber;
|
||||
result = 31 * result + width;
|
||||
result = 31 * result + height;
|
||||
result = 31 * result + xOffset;
|
||||
result = 31 * result + yOffset;
|
||||
result = 31 * result + (int) delayNumerator;
|
||||
result = 31 * result + (int) delayDenominator;
|
||||
result = 31 * result + (int) disposeOp;
|
||||
result = 31 * result + (int) blendOp;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.chunks;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A PngGamma object represents data parsed from a ``gAMA`` chunk.
|
||||
*/
|
||||
public class PngGamma {
|
||||
public final int imageGamma;
|
||||
|
||||
public PngGamma(int imageGamma) {
|
||||
this.imageGamma = imageGamma;
|
||||
}
|
||||
|
||||
public static PngGamma from(DataInputStream dis) throws IOException {
|
||||
return new PngGamma(dis.readInt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copy from https://github.com/aellerton/japng
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.chunks;
|
||||
|
||||
import org.jackhuang.hmcl.ui.image.apng.PngColourType;
|
||||
import org.jackhuang.hmcl.ui.image.apng.reader.PngReadHelper;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngFeatureException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by aellerton on 10/05/2015.
|
||||
*/
|
||||
public class PngHeader {
|
||||
/**
|
||||
* Number of pixels (columns) wide.
|
||||
*/
|
||||
public final int width;
|
||||
|
||||
/**
|
||||
* Number of pixels (rows) high.
|
||||
*/
|
||||
public final int height;
|
||||
|
||||
/**
|
||||
* The bitDepth is the number of bits for each <em>channel</em> of a given pixel.
|
||||
* A better name might be "bitsPerPixelChannel" but the name "bitDepth" is used
|
||||
* throughout the PNG specification.
|
||||
* <p>
|
||||
* A truecolour image with a bitDepth of 8 means that the red channel of a pixel
|
||||
* has 8 bits (so 256 levels of red), green has 8 bits (256 levels of green), and
|
||||
* blue has 8 bits (so 256 levels of green). That means the total bitsPerPixel
|
||||
* for that bitmap will be 8+8+8 = 24.
|
||||
* <p>
|
||||
* A truecolour <em>with alpha</em> image with bitDepth of 8 will be the same
|
||||
* except every alpha element of every pixel will have 8 bits (so 256 levels of
|
||||
* alpha transparency), meaning that the total bitsPerPixel for that bitmap will
|
||||
* be 8+8+8+8=32.
|
||||
* <p>
|
||||
* A truecolour with alpha image with <em>bitDepth of 16</em> means that each of
|
||||
* red, green blue and alpha have 16-bits respectively, meaning that the total
|
||||
* bitsPerPixel will be 16+16+16+16 = 64.
|
||||
* <p>
|
||||
* A greyscale image (no alpha) with bitDepth of 16 has only a grey channel for
|
||||
* each pixel, so the bitsPerPixel will also be 16.
|
||||
* <p>
|
||||
* But a greyscale image <em>with alpha</em> with a bitDepth of 16 has a grey
|
||||
* channel and an alpha channel, each with 16 bits so the bitsPerPixel will be
|
||||
* 16+16=32.
|
||||
* <p>
|
||||
* As for palette-based images...
|
||||
* <ul>
|
||||
* <li>A monochrome image or image with 2 colour palette has bitDepth=1.</li>
|
||||
* <li>An image with 4 colour palette has bitDepth=2.</li>
|
||||
* <li>An image with 8 colour palette has bitDepth=3.</li>
|
||||
* <li>An image with 16 colour palette has bitDepth=4.</li>
|
||||
* <li>A greyscale image with 16 levels of gray <em>and an alpha channel</em>
|
||||
* has bitDepth=4 and bitsPerPixel=8 because the gray and the alpha channel
|
||||
* each have 4 bits.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see #bitsPerPixel
|
||||
*/
|
||||
public final byte bitDepth;
|
||||
|
||||
/**
|
||||
* Every PNG image must be exactly one of the standard types as defined by the
|
||||
* PNG specification. Better names might have been "imageType" or "imageFormat"
|
||||
* but the name "colourType" is used throughout the PNG spec.
|
||||
*/
|
||||
public final PngColourType colourType;
|
||||
|
||||
/**
|
||||
* Compression type of the file.
|
||||
* In practice this is redundant: it may be zip and nothing else.
|
||||
*/
|
||||
public final byte compressionMethod;
|
||||
|
||||
/**
|
||||
* Filter method used by the file.
|
||||
* In practice this is redundant because the filter types are set in the
|
||||
* specification and have never been (and never will be) extended.
|
||||
*/
|
||||
public final byte filterMethod;
|
||||
|
||||
/**
|
||||
* An image is either interlaced or not interlaced.
|
||||
* At the time of writing only non-interlaced is supported by this library.
|
||||
*/
|
||||
public final byte interlaceMethod;
|
||||
|
||||
/**
|
||||
* The number of bits that comprise a single pixel in this bitmap (or every
|
||||
* frame if animated). This is distinct from bitDepth.
|
||||
*
|
||||
* @see #bitDepth
|
||||
*/
|
||||
public final int bitsPerPixel;
|
||||
public final int bytesPerRow;
|
||||
public final int filterOffset;
|
||||
|
||||
public PngHeader(int width, int height, byte bitDepth, PngColourType colourType) {
|
||||
this(width, height, bitDepth, colourType, (byte) 0, (byte) 0, (byte) 0);
|
||||
}
|
||||
|
||||
public PngHeader(int width, int height, byte bitDepth, PngColourType colourType, byte compressionMethod, byte filterMethod, byte interlaceMethod) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.bitDepth = bitDepth;
|
||||
this.colourType = colourType;
|
||||
this.compressionMethod = compressionMethod;
|
||||
this.filterMethod = filterMethod;
|
||||
this.interlaceMethod = interlaceMethod;
|
||||
this.bitsPerPixel = bitDepth * colourType.componentsPerPixel;
|
||||
this.bytesPerRow = PngReadHelper.calculateBytesPerRow(width, bitDepth, colourType, interlaceMethod);
|
||||
//this.filterOffset = this.bitsPerPixel < 8 ? 1 : this.bitsPerPixel>>3; // minimum of 1 byte. RGB888 will be 3 bytes. RGBAFFFF is 8 bytes.
|
||||
this.filterOffset = (this.bitsPerPixel + 7) >> 3; // libpng
|
||||
|
||||
// from pypng
|
||||
// # Derived values
|
||||
// # http://www.w3.org/TR/PNG/#6Colour-values
|
||||
// colormap = bool(self.color_type & 1)
|
||||
// greyscale = not (self.color_type & 2)
|
||||
// alpha = bool(self.color_type & 4)
|
||||
// color_planes = (3,1)[greyscale or colormap]
|
||||
// planes = color_planes + alpha
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PngHeader{" +
|
||||
"width=" + width +
|
||||
", height=" + height +
|
||||
", bitDepth=" + bitDepth +
|
||||
", colourType=" + colourType +
|
||||
", compressionMethod=" + compressionMethod +
|
||||
", filterMethod=" + filterMethod +
|
||||
", interlaceMethod=" + interlaceMethod +
|
||||
'}';
|
||||
}
|
||||
|
||||
public boolean isInterlaced() {
|
||||
return interlaceMethod == 1;
|
||||
}
|
||||
|
||||
public boolean isZipCompression() {
|
||||
return compressionMethod == 0;
|
||||
}
|
||||
|
||||
public boolean isGreyscale() {
|
||||
return colourType == PngColourType.PNG_GREYSCALE | colourType == PngColourType.PNG_GREYSCALE_WITH_ALPHA;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the image type indicates there is an alpha value for every pixel.
|
||||
* Note that this take into account any transparency or background chunk.
|
||||
*/
|
||||
public boolean hasAlphaChannel() {
|
||||
return colourType == PngColourType.PNG_GREYSCALE_WITH_ALPHA | colourType == PngColourType.PNG_TRUECOLOUR_WITH_ALPHA;
|
||||
}
|
||||
|
||||
public static PngHeader makeTruecolour(int width, int height) {
|
||||
return new PngHeader(width, height, (byte) 8, PngColourType.PNG_TRUECOLOUR);
|
||||
}
|
||||
|
||||
public static PngHeader makeTruecolourAlpha(int width, int height) {
|
||||
return new PngHeader(width, height, (byte) 8, PngColourType.PNG_TRUECOLOUR_WITH_ALPHA);
|
||||
}
|
||||
|
||||
public PngHeader adjustFor(PngFrameControl frame) {
|
||||
if (frame == null) {
|
||||
return this;
|
||||
} else {
|
||||
return new PngHeader(frame.width, frame.height, this.bitDepth, this.colourType, this.compressionMethod, this.filterMethod, this.interlaceMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public static void checkHeaderParameters(int width, int height, byte bitDepth, PngColourType colourType, byte compressionMethod, byte filterMethod, byte interlaceMethod) throws PngException {
|
||||
|
||||
switch (bitDepth) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 4:
|
||||
case 8:
|
||||
case 16:
|
||||
break; // all fine
|
||||
default:
|
||||
throw new PngIntegrityException("Invalid bit depth " + bitDepth);
|
||||
}
|
||||
|
||||
// thanks to pypng
|
||||
if (colourType.isIndexed() && bitDepth > 8) {
|
||||
throw new PngIntegrityException(String.format(
|
||||
"Indexed images (colour type %d) cannot have bitdepth > 8 (bit depth %d)." +
|
||||
" See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 .", colourType.code, bitDepth));
|
||||
}
|
||||
|
||||
if (bitDepth < 8 && !colourType.supportsSubByteDepth()) {
|
||||
throw new PngIntegrityException(String.format(
|
||||
"Illegal combination of bit depth (%d) and colour type (%d)." +
|
||||
" See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 .", colourType.code, bitDepth));
|
||||
}
|
||||
|
||||
if (interlaceMethod != 0) {
|
||||
throw new PngFeatureException("Interlaced images are not yet supported");
|
||||
}
|
||||
}
|
||||
|
||||
public static PngHeader from(DataInput dis) throws IOException, PngException {
|
||||
int width = dis.readInt();
|
||||
int height = dis.readInt();
|
||||
byte bitDepth = dis.readByte();
|
||||
PngColourType colourType = PngColourType.fromByte(dis.readByte());
|
||||
byte compressionMethod = dis.readByte();
|
||||
byte filterMethod = dis.readByte();
|
||||
byte interlaceMethod = dis.readByte();
|
||||
checkHeaderParameters(width, height, bitDepth, colourType, compressionMethod, filterMethod, interlaceMethod);
|
||||
return new PngHeader(width, height, bitDepth, colourType, compressionMethod, filterMethod, interlaceMethod);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 456 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user